mirror of
				https://gitea.invidious.io/iv-org/shard-ameba.git
				synced 2024-08-15 00:53:29 +00:00 
			
		
		
		
	Add Style/GuardClause rule (#254)
				
					
				
			This commit is contained in:
		
							parent
							
								
									c7f1f94cca
								
							
						
					
					
						commit
						f288cc3c4f
					
				
					 18 changed files with 657 additions and 76 deletions
				
			
		
							
								
								
									
										411
									
								
								spec/ameba/rule/style/guard_clause_spec.cr
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										411
									
								
								spec/ameba/rule/style/guard_clause_spec.cr
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,411 @@ | |||
| require "../../../spec_helper" | ||||
| 
 | ||||
| module Ameba | ||||
|   subject = Rule::Style::GuardClause.new | ||||
| 
 | ||||
|   def it_reports_body(body, *, line = __LINE__) | ||||
|     rule = Rule::Style::GuardClause.new | ||||
| 
 | ||||
|     it "reports an issue if method body is if / unless without else" do | ||||
|       source = expect_issue rule, <<-CRYSTAL, line: line | ||||
|         def func | ||||
|           if something | ||||
|         # ^^ error: Use a guard clause (`return unless something`) instead of wrapping the code inside a conditional expression. | ||||
|             #{body} | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         def func | ||||
|           unless something | ||||
|         # ^^^^^^ error: Use a guard clause (`return if something`) instead of wrapping the code inside a conditional expression. | ||||
|             #{body} | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
| 
 | ||||
|       expect_correction source, <<-CRYSTAL, line: line | ||||
|         def func | ||||
|           return unless something | ||||
|             #{body} | ||||
|          #{trailing_whitespace} | ||||
|         end | ||||
| 
 | ||||
|         def func | ||||
|           return if something | ||||
|             #{body} | ||||
|          #{trailing_whitespace} | ||||
|         end | ||||
|         CRYSTAL | ||||
|     end | ||||
| 
 | ||||
|     it "reports an issue if method body ends with if / unless without else" do | ||||
|       source = expect_issue rule, <<-CRYSTAL, line: line | ||||
|         def func | ||||
|           test | ||||
|           if something | ||||
|         # ^^ error: Use a guard clause (`return unless something`) instead of wrapping the code inside a conditional expression. | ||||
|             #{body} | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         def func | ||||
|           test | ||||
|           unless something | ||||
|         # ^^^^^^ error: Use a guard clause (`return if something`) instead of wrapping the code inside a conditional expression. | ||||
|             #{body} | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
| 
 | ||||
|       expect_correction source, <<-CRYSTAL, line: line | ||||
|         def func | ||||
|           test | ||||
|           return unless something | ||||
|             #{body} | ||||
|          #{trailing_whitespace} | ||||
|         end | ||||
| 
 | ||||
|         def func | ||||
|           test | ||||
|           return if something | ||||
|             #{body} | ||||
|          #{trailing_whitespace} | ||||
|         end | ||||
|         CRYSTAL | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def it_reports_control_expression(kw, *, line = __LINE__) | ||||
|     rule = Rule::Style::GuardClause.new | ||||
| 
 | ||||
|     it "reports an issue with #{kw} in the if branch" do | ||||
|       source = expect_issue rule, <<-CRYSTAL, line: line | ||||
|         def func | ||||
|           if something | ||||
|         # ^^ error: Use a guard clause (`#{kw} if something`) instead of wrapping the code inside a conditional expression. | ||||
|             #{kw} | ||||
|           else | ||||
|             puts "hello" | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
| 
 | ||||
|       expect_no_corrections source, line: line | ||||
|     end | ||||
| 
 | ||||
|     it "reports an issue with #{kw} in the else branch" do | ||||
|       source = expect_issue rule, <<-CRYSTAL, line: line | ||||
|         def func | ||||
|           if something | ||||
|         # ^^ error: Use a guard clause (`#{kw} unless something`) instead of wrapping the code inside a conditional expression. | ||||
|           puts "hello" | ||||
|           else | ||||
|             #{kw} | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
| 
 | ||||
|       expect_no_corrections source, line: line | ||||
|     end | ||||
| 
 | ||||
|     it "doesn't report an issue if condition has multiple lines" do | ||||
|       expect_no_issues rule, <<-CRYSTAL, line: line | ||||
|         def func | ||||
|           if something && | ||||
|                something_else | ||||
|             #{kw} | ||||
|           else | ||||
|             puts "hello" | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
|     end | ||||
| 
 | ||||
|     it "does not report an issue if #{kw} is inside elsif" do | ||||
|       expect_no_issues rule, <<-CRYSTAL, line: line | ||||
|         def func | ||||
|           if something | ||||
|             a | ||||
|           elsif something_else | ||||
|             #{kw} | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
|     end | ||||
| 
 | ||||
|     it "does not report an issue if #{kw} is inside if..elsif..else..end" do | ||||
|       expect_no_issues rule, <<-CRYSTAL, line: line | ||||
|         def func | ||||
|           if something | ||||
|             a | ||||
|           elsif something_else | ||||
|             b | ||||
|           else | ||||
|             #{kw} | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
|     end | ||||
| 
 | ||||
|     it "doesn't report an issue if control flow expr has multiple lines" do | ||||
|       expect_no_issues rule, <<-CRYSTAL, line: line | ||||
|         def func | ||||
|           if something | ||||
|             #{kw} \\ | ||||
|                   "blah blah blah" \\ | ||||
|                   "blah blah blah" | ||||
|           else | ||||
|             puts "hello" | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
|     end | ||||
| 
 | ||||
|     it "reports an issue if non-control-flow branch has multiple lines" do | ||||
|       source = expect_issue rule, <<-CRYSTAL, line: line | ||||
|         def func | ||||
|           if something | ||||
|         # ^^ error: Use a guard clause (`#{kw} if something`) instead of wrapping the code inside a conditional expression. | ||||
|             #{kw} | ||||
|           else | ||||
|             puts "hello" \\ | ||||
|                  "blah blah blah" | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
| 
 | ||||
|       expect_no_corrections source, line: line | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe Rule::Style::GuardClause do | ||||
|     it_reports_body "work" | ||||
|     it_reports_body "# TODO" | ||||
| 
 | ||||
|     pending "does not report an issue if `else` branch is present but empty" do | ||||
|       expect_no_issues subject, <<-CRYSTAL | ||||
|         def method | ||||
|           if bar = foo | ||||
|             puts bar | ||||
|           else | ||||
|             # nothing | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
|     end | ||||
| 
 | ||||
|     it "does not report an issue if body is if..elsif..end" do | ||||
|       expect_no_issues subject, <<-CRYSTAL | ||||
|         def func | ||||
|           if something | ||||
|             a | ||||
|           elsif something_else | ||||
|             b | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
|     end | ||||
| 
 | ||||
|     it "doesn't report an issue if condition has multiple lines" do | ||||
|       expect_no_issues subject, <<-CRYSTAL | ||||
|         def func | ||||
|           if something && | ||||
|                something_else | ||||
|             work | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         def func | ||||
|           unless something && | ||||
|                    something_else | ||||
|             work | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
|     end | ||||
| 
 | ||||
|     it "accepts a method body that is if / unless with else" do | ||||
|       expect_no_issues subject, <<-CRYSTAL | ||||
|         def func | ||||
|           if something | ||||
|             work | ||||
|           else | ||||
|             test | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         def func | ||||
|           unless something | ||||
|             work | ||||
|           else | ||||
|             test | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
|     end | ||||
| 
 | ||||
|     it "reports an issue when using `|| raise` in `then` branch" do | ||||
|       source = expect_issue subject, <<-CRYSTAL | ||||
|         def func | ||||
|           if something | ||||
|         # ^^ error: Use a guard clause (`work || raise("message") if something`) instead of [...] | ||||
|             work || raise("message") | ||||
|           else | ||||
|             test | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
| 
 | ||||
|       expect_no_corrections source | ||||
|     end | ||||
| 
 | ||||
|     it "reports an issue when using `|| raise` in `else` branch" do | ||||
|       source = expect_issue subject, <<-CRYSTAL | ||||
|         def func | ||||
|           if something | ||||
|         # ^^ error: Use a guard clause (`test || raise("message") unless something`) instead of [...] | ||||
|             work | ||||
|           else | ||||
|             test || raise("message") | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
| 
 | ||||
|       expect_no_corrections source | ||||
|     end | ||||
| 
 | ||||
|     it "reports an issue when using `&& return` in `then` branch" do | ||||
|       source = expect_issue subject, <<-CRYSTAL | ||||
|         def func | ||||
|           if something | ||||
|         # ^^ error: Use a guard clause (`work && return if something`) instead of wrapping the code inside a conditional expression. | ||||
|             work && return | ||||
|           else | ||||
|             test | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
| 
 | ||||
|       expect_no_corrections source | ||||
|     end | ||||
| 
 | ||||
|     it "reports an issue when using `&& return` in `else` branch" do | ||||
|       source = expect_issue subject, <<-CRYSTAL | ||||
|         def func | ||||
|           if something | ||||
|         # ^^ error: Use a guard clause (`test && return unless something`) instead of wrapping the code inside a conditional expression. | ||||
|             work | ||||
|           else | ||||
|             test && return | ||||
|           end | ||||
|         end | ||||
|         CRYSTAL | ||||
| 
 | ||||
|       expect_no_corrections source | ||||
|     end | ||||
| 
 | ||||
|     it "accepts a method body that does not end with if / unless" do | ||||
|       expect_no_issues subject, <<-CRYSTAL | ||||
|         def func | ||||
|           if something | ||||
|             work | ||||
|           end | ||||
|           test | ||||
|         end | ||||
| 
 | ||||
|         def func | ||||
|           unless something | ||||
|             work | ||||
|           end | ||||
|           test | ||||
|         end | ||||
|         CRYSTAL | ||||
|     end | ||||
| 
 | ||||
|     it "accepts a method body that is a modifier if / unless" do | ||||
|       expect_no_issues subject, <<-CRYSTAL | ||||
|         def func | ||||
|           work if something | ||||
|         end | ||||
| 
 | ||||
|         def func | ||||
|           work unless something | ||||
|         end | ||||
|         CRYSTAL | ||||
|     end | ||||
| 
 | ||||
|     it "accepts a method with empty parentheses as its body" do | ||||
|       expect_no_issues subject, <<-CRYSTAL | ||||
|         def func | ||||
|           () | ||||
|         end | ||||
|         CRYSTAL | ||||
|     end | ||||
| 
 | ||||
|     it "does not report an issue when assigning the result of a guard condition with `else`" do | ||||
|       expect_no_issues subject, <<-CRYSTAL | ||||
|         def func | ||||
|           result = | ||||
|             if something | ||||
|               work || raise("message") | ||||
|             else | ||||
|               test | ||||
|             end | ||||
|         end | ||||
|         CRYSTAL | ||||
|     end | ||||
| 
 | ||||
|     it_reports_control_expression "return" | ||||
|     it_reports_control_expression "next" | ||||
|     it_reports_control_expression "break" | ||||
|     it_reports_control_expression %(raise "error") | ||||
| 
 | ||||
|     context "method in module" do | ||||
|       it "reports an issue for instance method" do | ||||
|         source = expect_issue subject, <<-CRYSTAL | ||||
|           module CopTest | ||||
|             def test | ||||
|               if something | ||||
|             # ^^ error: Use a guard clause (`return unless something`) instead of wrapping the code inside a conditional expression. | ||||
|                 work | ||||
|               end | ||||
|             end | ||||
|           end | ||||
|           CRYSTAL | ||||
| 
 | ||||
|         expect_correction source, <<-CRYSTAL | ||||
|           module CopTest | ||||
|             def test | ||||
|               return unless something | ||||
|                 work | ||||
|              #{trailing_whitespace} | ||||
|             end | ||||
|           end | ||||
|           CRYSTAL | ||||
|       end | ||||
| 
 | ||||
|       it "reports an issue for singleton methods" do | ||||
|         source = expect_issue subject, <<-CRYSTAL | ||||
|           module CopTest | ||||
|             def self.test | ||||
|               if something && something_else | ||||
|             # ^^ error: Use a guard clause (`return unless something && something_else`) instead of [...] | ||||
|                 work | ||||
|               end | ||||
|             end | ||||
|           end | ||||
|           CRYSTAL | ||||
| 
 | ||||
|         expect_correction source, <<-CRYSTAL | ||||
|           module CopTest | ||||
|             def self.test | ||||
|               return unless something && something_else | ||||
|                 work | ||||
|              #{trailing_whitespace} | ||||
|             end | ||||
|           end | ||||
|           CRYSTAL | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -29,12 +29,11 @@ module Ameba::AST | |||
|     # Assignment.new(node, variable, scope) | ||||
|     # ``` | ||||
|     def initialize(@node, @variable, @scope) | ||||
|       if scope = @variable.scope | ||||
|         @branch = Branch.of(@node, scope) | ||||
|         @referenced = true if @variable.special? || | ||||
|                               @variable.scope.type_definition? || | ||||
|                               referenced_in_loop? | ||||
|       end | ||||
|       return unless scope = @variable.scope | ||||
|       @branch = Branch.of(@node, scope) | ||||
|       @referenced = true if @variable.special? || | ||||
|                             @variable.scope.type_definition? || | ||||
|                             referenced_in_loop? | ||||
|     end | ||||
| 
 | ||||
|     def referenced_in_loop? | ||||
|  |  | |||
|  | @ -48,14 +48,19 @@ module Ameba::AST | |||
|       # A visit callback for `Crystal::{{name}}` node. | ||||
|       # Returns true meaning that child nodes will be traversed as well. | ||||
|       def visit(node : Crystal::{{name}}) | ||||
|         return false if skip?(node) | ||||
| 
 | ||||
|         @rule.test @source, node | ||||
|         true | ||||
|       end | ||||
|     {% end %} | ||||
| 
 | ||||
|     def visit(node) | ||||
|       return true unless skip = @skip | ||||
|       !skip.includes?(node.class) | ||||
|       !skip?(node) | ||||
|     end | ||||
| 
 | ||||
|     private def skip?(node) | ||||
|       !!@skip.try(&.includes?(node.class)) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -23,9 +23,8 @@ module Ameba::AST | |||
|       @scope_queue << @current_scope | ||||
| 
 | ||||
|       # go up if this is not a top level scope | ||||
|       if outer_scope = @current_scope.outer_scope | ||||
|         @current_scope = outer_scope | ||||
|       end | ||||
|       return unless outer_scope = @current_scope.outer_scope | ||||
|       @current_scope = outer_scope | ||||
|     end | ||||
| 
 | ||||
|     private def on_assign_end(target, node) | ||||
|  | @ -132,9 +131,8 @@ module Ameba::AST | |||
| 
 | ||||
|     # :nodoc: | ||||
|     def visit(node : Crystal::TypeDeclaration) | ||||
|       if !@current_scope.type_definition? && (var = node.var).is_a?(Crystal::Var) | ||||
|         @current_scope.add_variable var | ||||
|       end | ||||
|       return if @current_scope.type_definition? || !(var = node.var).is_a?(Crystal::Var) | ||||
|       @current_scope.add_variable var | ||||
|     end | ||||
| 
 | ||||
|     # :nodoc: | ||||
|  |  | |||
|  | @ -68,9 +68,8 @@ class Ameba::Config | |||
|     @excluded = load_array_section(config, "Excluded") | ||||
|     @globs = load_array_section(config, "Globs", DEFAULT_GLOBS) | ||||
| 
 | ||||
|     if formatter_name = load_formatter_name(config) | ||||
|       self.formatter = formatter_name | ||||
|     end | ||||
|     return unless formatter_name = load_formatter_name(config) | ||||
|     self.formatter = formatter_name | ||||
|   end | ||||
| 
 | ||||
|   # Loads YAML configuration file by `path`. | ||||
|  | @ -124,11 +123,10 @@ class Ameba::Config | |||
|   # config.formatter = :progress | ||||
|   # ``` | ||||
|   def formatter=(name : String | Symbol) | ||||
|     if f = AVAILABLE_FORMATTERS[name]? | ||||
|       @formatter = f.new | ||||
|     else | ||||
|     unless f = AVAILABLE_FORMATTERS[name]? | ||||
|       raise "Unknown formatter `#{name}`. Use one of #{Config.formatter_names}." | ||||
|     end | ||||
|     @formatter = f.new | ||||
|   end | ||||
| 
 | ||||
|   # Updates rule properties. | ||||
|  |  | |||
|  | @ -71,9 +71,8 @@ module Ameba::Formatter | |||
|     end | ||||
| 
 | ||||
|     private def finished_in_message(started, finished) | ||||
|       if started && finished | ||||
|         "Finished in #{to_human(finished - started)}".colorize(:default) | ||||
|       end | ||||
|       return unless started && finished | ||||
|       "Finished in #{to_human(finished - started)}".colorize(:default) | ||||
|     end | ||||
| 
 | ||||
|     private def to_human(span : Time::Span) | ||||
|  |  | |||
|  | @ -65,20 +65,18 @@ module Ameba | |||
|     # parse_inline_directive(line) # => nil | ||||
|     # ``` | ||||
|     def parse_inline_directive(line) | ||||
|       if directive = COMMENT_DIRECTIVE_REGEX.match(line) | ||||
|         return if commented_out?(line.gsub(directive[0], "")) | ||||
|         { | ||||
|           action: directive["action"], | ||||
|           rules:  directive["rules"].split(/[\s,]/, remove_empty: true), | ||||
|         } | ||||
|       end | ||||
|       return unless directive = COMMENT_DIRECTIVE_REGEX.match(line) | ||||
|       return if commented_out?(line.gsub(directive[0], "")) | ||||
|       { | ||||
|         action: directive["action"], | ||||
|         rules:  directive["rules"].split(/[\s,]/, remove_empty: true), | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|     # Returns true if the line at the given `line_number` is a comment. | ||||
|     def comment?(line_number : Int32) | ||||
|       if line = lines[line_number]? | ||||
|         comment?(line) | ||||
|       end | ||||
|       return unless line = lines[line_number]? | ||||
|       comment?(line) | ||||
|     end | ||||
| 
 | ||||
|     private def comment?(line : String) | ||||
|  |  | |||
|  | @ -24,13 +24,12 @@ module Ameba::Rule::Layout | |||
|       return if source_lines_size == 1 && last_source_line.empty? | ||||
| 
 | ||||
|       last_line_empty = last_source_line.empty? | ||||
|       if source_lines_size >= 1 && (source_lines.last(2).join.blank? || !last_line_empty) | ||||
|         if last_line_empty | ||||
|           issue_for({source_lines_size, 1}, MSG) | ||||
|         else | ||||
|           issue_for({source_lines_size, 1}, MSG_FINAL_NEWLINE) do |corrector| | ||||
|             corrector.insert_before({source_lines_size + 1, 1}, '\n') | ||||
|           end | ||||
|       return if source_lines_size.zero? || (source_lines.last(2).join.presence && last_line_empty) | ||||
|       if last_line_empty | ||||
|         issue_for({source_lines_size, 1}, MSG) | ||||
|       else | ||||
|         issue_for({source_lines_size, 1}, MSG_FINAL_NEWLINE) do |corrector| | ||||
|           corrector.insert_before({source_lines_size + 1, 1}, '\n') | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|  |  | |||
|  | @ -46,9 +46,8 @@ module Ameba::Rule::Lint | |||
|     end | ||||
| 
 | ||||
|     def test(source, node : Crystal::Expressions) | ||||
|       if node.expressions.size == 1 && node.expressions.first.nop? | ||||
|         issue_for node, MSG_EXRS | ||||
|       end | ||||
|       return unless node.expressions.size == 1 && node.expressions.first.nop? | ||||
|       issue_for node, MSG_EXRS | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -55,9 +55,8 @@ module Ameba::Rule::Lint | |||
|     end | ||||
| 
 | ||||
|     def test(source, node, flow_expression : AST::FlowExpression) | ||||
|       if unreachable_node = flow_expression.unreachable_nodes.first? | ||||
|         issue_for unreachable_node, MSG | ||||
|       end | ||||
|       return unless unreachable_node = flow_expression.unreachable_nodes.first? | ||||
|       issue_for unreachable_node, MSG | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -21,10 +21,9 @@ module Ameba::Rule::Metrics | |||
|     def test(source, node : Crystal::Def) | ||||
|       complexity = AST::CountingVisitor.new(node).count | ||||
| 
 | ||||
|       if complexity > max_complexity && (location = node.name_location) | ||||
|         issue_for location, name_end_location(node), | ||||
|           MSG % {complexity, max_complexity} | ||||
|       end | ||||
|       return unless complexity > max_complexity && (location = node.name_location) | ||||
|       issue_for location, name_end_location(node), | ||||
|         MSG % {complexity, max_complexity} | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -29,14 +29,13 @@ module Ameba::Rule::Style | |||
|     MSG = "Constant name should be screaming-cased: %s, not %s" | ||||
| 
 | ||||
|     def test(source, node : Crystal::Assign) | ||||
|       if (target = node.target).is_a?(Crystal::Path) | ||||
|         name = target.names.first | ||||
|         expected = name.upcase | ||||
|       return unless (target = node.target).is_a?(Crystal::Path) | ||||
|       name = target.names.first | ||||
|       expected = name.upcase | ||||
| 
 | ||||
|         return if name.in?(expected, name.camelcase) | ||||
|       return if name.in?(expected, name.camelcase) | ||||
| 
 | ||||
|         issue_for target, MSG % {expected, name} | ||||
|       end | ||||
|       issue_for target, MSG % {expected, name} | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
							
								
								
									
										183
									
								
								src/ameba/rule/style/guard_clause.cr
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								src/ameba/rule/style/guard_clause.cr
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,183 @@ | |||
| module Ameba::Rule::Style | ||||
|   # Use a guard clause instead of wrapping the code inside a conditional | ||||
|   # expression | ||||
|   # | ||||
|   # ``` | ||||
|   # # bad | ||||
|   # def test | ||||
|   #   if something | ||||
|   #     work | ||||
|   #   end | ||||
|   # end | ||||
|   # | ||||
|   # # good | ||||
|   # def test | ||||
|   #   return unless something | ||||
|   # | ||||
|   #   work | ||||
|   # end | ||||
|   # | ||||
|   # # also good | ||||
|   # def test | ||||
|   #   work if something | ||||
|   # end | ||||
|   # | ||||
|   # # bad | ||||
|   # if something | ||||
|   #   raise "exception" | ||||
|   # else | ||||
|   #   ok | ||||
|   # end | ||||
|   # | ||||
|   # # good | ||||
|   # raise "exception" if something | ||||
|   # ok | ||||
|   # | ||||
|   # # bad | ||||
|   # if something | ||||
|   #   foo || raise("exception") | ||||
|   # else | ||||
|   #   ok | ||||
|   # end | ||||
|   # | ||||
|   # # good | ||||
|   # foo || raise("exception") if something | ||||
|   # ok | ||||
|   # ``` | ||||
|   # | ||||
|   # YAML configuration example: | ||||
|   # | ||||
|   # ``` | ||||
|   # Style/GuardClause: | ||||
|   #   Enabled: true | ||||
|   # ``` | ||||
|   class GuardClause < Base | ||||
|     include AST::Util | ||||
| 
 | ||||
|     properties do | ||||
|       enabled false | ||||
|       description "Check for conditionals that can be replaced with guard clauses." | ||||
|     end | ||||
| 
 | ||||
|     MSG = "Use a guard clause (`%s`) instead of wrapping the " \ | ||||
|           "code inside a conditional expression." | ||||
| 
 | ||||
|     def test(source) | ||||
|       AST::NodeVisitor.new self, source, skip: [Crystal::Assign] | ||||
|     end | ||||
| 
 | ||||
|     def test(source, node : Crystal::Def) | ||||
|       final_expression = | ||||
|         if (body = node.body).is_a?(Crystal::Expressions) | ||||
|           body.last | ||||
|         else | ||||
|           body | ||||
|         end | ||||
| 
 | ||||
|       case final_expression | ||||
|       when Crystal::If     then check_ending_if(source, final_expression) | ||||
|       when Crystal::Unless then check_ending_if(source, final_expression) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def test(source, node : Crystal::If | Crystal::Unless) | ||||
|       return if accepted_form?(source, node, ending: false) | ||||
| 
 | ||||
|       case | ||||
|       when guard_clause = guard_clause(node.then) | ||||
|         parent, conditional_keyword = node.then, keyword(node) | ||||
|       when guard_clause = guard_clause(node.else) | ||||
|         parent, conditional_keyword = node.else, opposite_keyword(node) | ||||
|       end | ||||
| 
 | ||||
|       return unless guard_clause && parent && conditional_keyword | ||||
| 
 | ||||
|       report_issue(source, node, guard_clause_source(source, guard_clause, parent), conditional_keyword) | ||||
|     end | ||||
| 
 | ||||
|     private def check_ending_if(source, node) | ||||
|       return if accepted_form?(source, node, ending: true) | ||||
| 
 | ||||
|       report_issue(source, node, "return", opposite_keyword(node)) | ||||
|     end | ||||
| 
 | ||||
|     private def report_issue(source, node, scope_exiting_keyword, conditional_keyword) | ||||
|       return unless keyword_loc = node.location | ||||
|       return unless cond_code = node_source(node.cond, source.lines) | ||||
| 
 | ||||
|       keyword_end_loc = keyword_loc.adjust(column_number: keyword(node).size - 1) | ||||
| 
 | ||||
|       example = "#{scope_exiting_keyword} #{conditional_keyword} #{cond_code}" | ||||
|       # TODO: check if example is too long for single line | ||||
| 
 | ||||
|       if node.else.is_a?(Crystal::Nop) | ||||
|         return unless end_end_loc = node.end_location | ||||
| 
 | ||||
|         end_loc = end_end_loc.adjust(column_number: {{1 - "end".size}}) | ||||
| 
 | ||||
|         issue_for keyword_loc, keyword_end_loc, MSG % example do |corrector| | ||||
|           corrector.replace(keyword_loc, keyword_end_loc, "#{scope_exiting_keyword} #{conditional_keyword}") | ||||
|           corrector.remove(end_loc, end_end_loc) | ||||
|         end | ||||
|       else | ||||
|         issue_for keyword_loc, keyword_end_loc, MSG % example | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     private def keyword(node : Crystal::If) | ||||
|       "if" | ||||
|     end | ||||
| 
 | ||||
|     private def keyword(node : Crystal::Unless) | ||||
|       "unless" | ||||
|     end | ||||
| 
 | ||||
|     private def opposite_keyword(node : Crystal::If) | ||||
|       "unless" | ||||
|     end | ||||
| 
 | ||||
|     private def opposite_keyword(node : Crystal::Unless) | ||||
|       "if" | ||||
|     end | ||||
| 
 | ||||
|     private def accepted_form?(source, node, ending) | ||||
|       return true if node.is_a?(Crystal::If) && node.ternary? | ||||
|       return true unless cond_loc = node.cond.location | ||||
|       return true unless cond_end_loc = node.cond.end_location | ||||
|       return true unless cond_loc.line_number == cond_end_loc.line_number | ||||
|       return true unless (then_loc = node.then.location).nil? || cond_loc < then_loc | ||||
| 
 | ||||
|       if ending | ||||
|         !node.else.is_a?(Crystal::Nop) | ||||
|       else | ||||
|         return true if node.else.is_a?(Crystal::Nop) | ||||
|         return true unless code = node_source(node, source.lines) | ||||
| 
 | ||||
|         code.starts_with?("elsif") | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     private def guard_clause(node) | ||||
|       node = node.right if node.is_a?(Crystal::And) || node.is_a?(Crystal::Or) | ||||
| 
 | ||||
|       return unless location = node.location | ||||
|       return unless end_location = node.end_location | ||||
|       return unless location.line_number == end_location.line_number | ||||
| 
 | ||||
|       case node | ||||
|       when Crystal::Call | ||||
|         node if node.obj.nil? && node.name == "raise" | ||||
|       when Crystal::Return, Crystal::Break, Crystal::Next | ||||
|         node | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def guard_clause_source(source, guard_clause, parent) | ||||
|       if parent.is_a?(Crystal::And) || parent.is_a?(Crystal::Or) | ||||
|         node_source(parent, source.lines) | ||||
|       else | ||||
|         node_source(guard_clause, source.lines) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -37,12 +37,11 @@ module Ameba::Rule::Style | |||
|     MSG = "Favour method name '%s?' over '%s'" | ||||
| 
 | ||||
|     def test(source, node : Crystal::Def) | ||||
|       if node.name =~ /^(is|has)_(\w+)\?/ | ||||
|         alternative = $2 | ||||
|         return unless alternative =~ /^[a-z][a-zA-Z_0-9]*$/ | ||||
|       return unless node.name =~ /^(is|has)_(\w+)\?/ | ||||
|       alternative = $2 | ||||
|       return unless alternative =~ /^[a-z][a-zA-Z_0-9]*$/ | ||||
| 
 | ||||
|         issue_for node, MSG % {alternative, node.name} | ||||
|       end | ||||
|       issue_for node, MSG % {alternative, node.name} | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -98,9 +98,8 @@ module Ameba::Rule::Style | |||
|     end | ||||
| 
 | ||||
|     private def begin_exprs_in_handler?(handler) | ||||
|       if (body = handler.body).is_a?(Crystal::Expressions) | ||||
|         body.expressions.first?.is_a?(Crystal::ExceptionHandler) | ||||
|       end | ||||
|       return unless (body = handler.body).is_a?(Crystal::Expressions) | ||||
|       body.expressions.first?.is_a?(Crystal::ExceptionHandler) | ||||
|     end | ||||
| 
 | ||||
|     private def def_redundant_begin_range(source, node) | ||||
|  |  | |||
|  | @ -221,9 +221,8 @@ module Ameba | |||
|     end | ||||
| 
 | ||||
|     private def check_unneeded_directives(source) | ||||
|       if (rule = @unneeded_disable_directive_rule) && rule.enabled | ||||
|         rule.test(source) | ||||
|       end | ||||
|       return unless (rule = @unneeded_disable_directive_rule) && rule.enabled | ||||
|       rule.test(source) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -124,9 +124,8 @@ class Ameba::Source | |||
|     end | ||||
| 
 | ||||
|     private def check_range_validity(begin_pos, end_pos) | ||||
|       if begin_pos < 0 || end_pos > code.size | ||||
|         raise IndexError.new("The range #{begin_pos}...#{end_pos} is outside the bounds of the source") | ||||
|       end | ||||
|       return unless begin_pos < 0 || end_pos > code.size | ||||
|       raise IndexError.new("The range #{begin_pos}...#{end_pos} is outside the bounds of the source") | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -161,13 +161,12 @@ module Ameba::Spec::ExpectIssue | |||
|                        line = __LINE__) | ||||
|     lines = code.split('\n') # must preserve trailing newline | ||||
|     _, actual_annotations = actual_annotations(rules, code, path, lines) | ||||
|     unless actual_annotations.to_s == code | ||||
|       fail <<-MSG, file, line | ||||
|         Expected no issues, but got: | ||||
|     return if actual_annotations.to_s == code | ||||
|     fail <<-MSG, file, line | ||||
|       Expected no issues, but got: | ||||
| 
 | ||||
|         #{actual_annotations} | ||||
|         MSG | ||||
|     end | ||||
|       #{actual_annotations} | ||||
|       MSG | ||||
|   end | ||||
| 
 | ||||
|   private def actual_annotations(rules, code, path, lines) | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue