mirror of
				https://gitea.invidious.io/iv-org/shard-ameba.git
				synced 2024-08-15 00:53:29 +00:00 
			
		
		
		
	Merge pull request #320 from crystal-ameba/Sija/unused-block-argument
Add `Lint/UnusedBlockArgument` rule
This commit is contained in:
		
						commit
						e6ebca7a5b
					
				
					 8 changed files with 205 additions and 19 deletions
				
			
		|  | @ -115,21 +115,21 @@ module Ameba::Rule::Lint | ||||||
|       subject.catch(s).should be_valid |       subject.catch(s).should be_valid | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it "reports if block arg is not used" do |     it "doesn't report if block arg is not used" do | ||||||
|       s = Source.new %( |       s = Source.new %( | ||||||
|         def method(&block) |         def method(&block) | ||||||
|         end |         end | ||||||
|       ) |       ) | ||||||
|       subject.catch(s).should_not be_valid |       subject.catch(s).should be_valid | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it "reports if unused and there is yield" do |     it "doesn't report if unused and there is yield" do | ||||||
|       s = Source.new %( |       s = Source.new %( | ||||||
|         def method(&block) |         def method(&block) | ||||||
|           yield 1 |           yield 1 | ||||||
|         end |         end | ||||||
|       ) |       ) | ||||||
|       subject.catch(s).should_not be_valid |       subject.catch(s).should be_valid | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it "doesn't report if it's an anonymous block" do |     it "doesn't report if it's an anonymous block" do | ||||||
|  |  | ||||||
							
								
								
									
										110
									
								
								spec/ameba/rule/lint/unused_block_argument_spec.cr
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								spec/ameba/rule/lint/unused_block_argument_spec.cr
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,110 @@ | ||||||
|  | require "../../../spec_helper" | ||||||
|  | 
 | ||||||
|  | module Ameba::Rule::Lint | ||||||
|  |   subject = UnusedBlockArgument.new | ||||||
|  | 
 | ||||||
|  |   describe UnusedBlockArgument do | ||||||
|  |     it "doesn't report if it is an instance var argument" do | ||||||
|  |       s = Source.new %( | ||||||
|  |         class A | ||||||
|  |           def initialize(&@callback) | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       ) | ||||||
|  |       subject.catch(s).should be_valid | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it "doesn't report if anonymous" do | ||||||
|  |       s = Source.new %( | ||||||
|  |         def method(a, b, c, &) | ||||||
|  |         end | ||||||
|  |       ) | ||||||
|  |       subject.catch(s).should be_valid | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it "doesn't report if argument name starts with a `_`" do | ||||||
|  |       s = Source.new %( | ||||||
|  |         def method(a, b, c, &_block) | ||||||
|  |         end | ||||||
|  |       ) | ||||||
|  |       subject.catch(s).should be_valid | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it "doesn't report if it is a block and used" do | ||||||
|  |       s = Source.new %( | ||||||
|  |         def method(a, b, c, &block) | ||||||
|  |           block.call | ||||||
|  |         end | ||||||
|  |       ) | ||||||
|  |       subject.catch(s).should be_valid | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it "reports if block arg is not used" do | ||||||
|  |       s = Source.new %( | ||||||
|  |         def method(a, b, c, &block) | ||||||
|  |         end | ||||||
|  |       ) | ||||||
|  |       subject.catch(s).should_not be_valid | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it "reports if unused and there is yield" do | ||||||
|  |       s = Source.new %( | ||||||
|  |         def method(a, b, c, &block) | ||||||
|  |           3.times do |i| | ||||||
|  |             i.try do | ||||||
|  |               yield i | ||||||
|  |             end | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       ) | ||||||
|  |       subject.catch(s).should_not be_valid | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it "doesn't report if anonymous and there is yield" do | ||||||
|  |       s = Source.new %( | ||||||
|  |         def method(a, b, c, &) | ||||||
|  |           yield 1 | ||||||
|  |         end | ||||||
|  |       ) | ||||||
|  |       subject.catch(s).should be_valid | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it "doesn't report if variable is referenced implicitly" do | ||||||
|  |       s = Source.new %( | ||||||
|  |         class Bar < Foo | ||||||
|  |           def method(a, b, c, &block) | ||||||
|  |             super | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       ) | ||||||
|  |       subject.catch(s).should be_valid | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context "super" do | ||||||
|  |       it "reports if variable is not referenced implicitly by super" do | ||||||
|  |         s = Source.new %( | ||||||
|  |           class Bar < Foo | ||||||
|  |             def method(a, b, c, &block) | ||||||
|  |               super a, b, c | ||||||
|  |             end | ||||||
|  |           end | ||||||
|  |         ) | ||||||
|  |         subject.catch(s).should_not be_valid | ||||||
|  |         s.issues.first.message.should eq "Unused block argument `block`. If it's necessary, " \ | ||||||
|  |                                          "use `_block` as an argument name to indicate " \ | ||||||
|  |                                          "that it won't be used." | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context "macro" do | ||||||
|  |       it "doesn't report if it is a used macro block argument" do | ||||||
|  |         s = Source.new %( | ||||||
|  |           macro my_macro(&block) | ||||||
|  |             {% block %} | ||||||
|  |           end | ||||||
|  |         ) | ||||||
|  |         subject.catch(s).should be_valid | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -4,6 +4,9 @@ module Ameba::AST | ||||||
|   # Represents a context of the local variable visibility. |   # Represents a context of the local variable visibility. | ||||||
|   # This is where the local variables belong to. |   # This is where the local variables belong to. | ||||||
|   class Scope |   class Scope | ||||||
|  |     # Whether the scope yields. | ||||||
|  |     setter yields = false | ||||||
|  | 
 | ||||||
|     # Link to local variables |     # Link to local variables | ||||||
|     getter variables = [] of Variable |     getter variables = [] of Variable | ||||||
| 
 | 
 | ||||||
|  | @ -143,6 +146,14 @@ module Ameba::AST | ||||||
|       end || variable.used_in_macro? |       end || variable.used_in_macro? | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     # Returns `true` if current scope (or any of inner scopes) yields, | ||||||
|  |     # `false` otherwise. | ||||||
|  |     def yields?(check_inner_scopes = true) | ||||||
|  |       return true if @yields | ||||||
|  |       return inner_scopes.any?(&.yields?) if check_inner_scopes | ||||||
|  |       false | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     # Returns `true` if current scope is a def, `false` otherwise. |     # Returns `true` if current scope is a def, `false` otherwise. | ||||||
|     def def? |     def def? | ||||||
|       node.is_a?(Crystal::Def) |       node.is_a?(Crystal::Def) | ||||||
|  |  | ||||||
|  | @ -19,8 +19,8 @@ module Ameba::AST | ||||||
|       MacroFor, |       MacroFor, | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     SUPER_NODE_NAME  = "super" |     SPECIAL_NODE_NAMES = %w[super previous_def] | ||||||
|     RECORD_NODE_NAME = "record" |     RECORD_NODE_NAME   = "record" | ||||||
| 
 | 
 | ||||||
|     @scope_queue = [] of Scope |     @scope_queue = [] of Scope | ||||||
|     @current_scope : Scope |     @current_scope : Scope | ||||||
|  | @ -64,6 +64,11 @@ module Ameba::AST | ||||||
|       end |       end | ||||||
|     {% end %} |     {% end %} | ||||||
| 
 | 
 | ||||||
|  |     # :nodoc: | ||||||
|  |     def visit(node : Crystal::Yield) | ||||||
|  |       @current_scope.yields = true | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     # :nodoc: |     # :nodoc: | ||||||
|     def visit(node : Crystal::Def) |     def visit(node : Crystal::Def) | ||||||
|       node.name == "->" || on_scope_enter(node) |       node.name == "->" || on_scope_enter(node) | ||||||
|  | @ -134,7 +139,7 @@ module Ameba::AST | ||||||
|     def visit(node : Crystal::Call) |     def visit(node : Crystal::Call) | ||||||
|       case |       case | ||||||
|       when @current_scope.def? |       when @current_scope.def? | ||||||
|         if node.name == SUPER_NODE_NAME && node.args.empty? |         if node.name.in?(SPECIAL_NODE_NAMES) && node.args.empty? | ||||||
|           @current_scope.arguments.each do |arg| |           @current_scope.arguments.each do |arg| | ||||||
|             variable = arg.variable |             variable = arg.variable | ||||||
|             variable.reference(variable.node, @current_scope).explicit = false |             variable.reference(variable.node, @current_scope).explicit = false | ||||||
|  |  | ||||||
|  | @ -50,6 +50,10 @@ module Ameba::Rule::Lint | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def test(source, node : Crystal::Def, scope : AST::Scope) |     def test(source, node : Crystal::Def, scope : AST::Scope) | ||||||
|  |       # `Lint/UnusedBlockArgument` rule covers this case explicitly | ||||||
|  |       if block_arg = node.block_arg | ||||||
|  |         scope.arguments.reject!(&.node.== block_arg) | ||||||
|  |       end | ||||||
|       ignore_defs? || find_unused_arguments source, scope |       ignore_defs? || find_unused_arguments source, scope | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										62
									
								
								src/ameba/rule/lint/unused_block_argument.cr
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/ameba/rule/lint/unused_block_argument.cr
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | ||||||
|  | module Ameba::Rule::Lint | ||||||
|  |   # A rule that reports unused block arguments. | ||||||
|  |   # For example, this is considered invalid: | ||||||
|  |   # | ||||||
|  |   # ``` | ||||||
|  |   # def foo(a, b, &block) | ||||||
|  |   #   a + b | ||||||
|  |   # end | ||||||
|  |   # | ||||||
|  |   # def bar(&block) | ||||||
|  |   #   yield 42 | ||||||
|  |   # end | ||||||
|  |   # ``` | ||||||
|  |   # | ||||||
|  |   # and should be written as: | ||||||
|  |   # | ||||||
|  |   # ``` | ||||||
|  |   # def foo(a, b, &_block) | ||||||
|  |   #   a + b | ||||||
|  |   # end | ||||||
|  |   # | ||||||
|  |   # def bar(&) | ||||||
|  |   #   yield 42 | ||||||
|  |   # end | ||||||
|  |   # ``` | ||||||
|  |   # | ||||||
|  |   # YAML configuration example: | ||||||
|  |   # | ||||||
|  |   # ``` | ||||||
|  |   # Lint/UnusedBlockArgument: | ||||||
|  |   #   Enabled: true | ||||||
|  |   # ``` | ||||||
|  |   class UnusedBlockArgument < Base | ||||||
|  |     properties do | ||||||
|  |       description "Disallows unused block arguments" | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     MSG_UNUSED = "Unused block argument `%1$s`. If it's necessary, use `_%1$s` " \ | ||||||
|  |                  "as an argument name to indicate that it won't be used." | ||||||
|  | 
 | ||||||
|  |     MSG_YIELDED = "Use `&` as an argument name to indicate that it won't be referenced." | ||||||
|  | 
 | ||||||
|  |     def test(source) | ||||||
|  |       AST::ScopeVisitor.new self, source | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def test(source, node : Crystal::Def, scope : AST::Scope) | ||||||
|  |       return unless block_arg = node.block_arg | ||||||
|  |       return unless block_arg = scope.arguments.find(&.node.== block_arg) | ||||||
|  | 
 | ||||||
|  |       return if block_arg.anonymous? | ||||||
|  |       return if scope.references?(block_arg.variable) | ||||||
|  | 
 | ||||||
|  |       if scope.yields? | ||||||
|  |         issue_for block_arg.node, MSG_YIELDED | ||||||
|  |       else | ||||||
|  |         return if block_arg.ignored? | ||||||
|  |         issue_for block_arg.node, MSG_UNUSED % block_arg.name | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -58,16 +58,12 @@ module Ameba::Rule::Style | ||||||
| 
 | 
 | ||||||
|     MSG = "Type name should be camelcased: %s, but it was %s" |     MSG = "Type name should be camelcased: %s, but it was %s" | ||||||
| 
 | 
 | ||||||
|     private def check_node(source, node) |     def test(source, node : Crystal::Alias | Crystal::ClassDef | Crystal::ModuleDef | Crystal::LibDef | Crystal::EnumDef) | ||||||
|       name = node.name.to_s |       name = node.name.to_s | ||||||
|       expected = name.camelcase |       expected = name.camelcase | ||||||
|       return if name == expected |       return if name == expected | ||||||
| 
 | 
 | ||||||
|       issue_for node, MSG % {expected, name} |       issue_for node, MSG % {expected, name} | ||||||
|     end |     end | ||||||
| 
 |  | ||||||
|     def test(source, node : Crystal::Alias | Crystal::ClassDef | Crystal::ModuleDef | Crystal::LibDef | Crystal::EnumDef) |  | ||||||
|       check_node(source, node) |  | ||||||
|     end |  | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -29,18 +29,16 @@ module Ameba::Rule::Style | ||||||
| 
 | 
 | ||||||
|     MSG = "Var name should be underscore-cased: %s, not %s" |     MSG = "Var name should be underscore-cased: %s, not %s" | ||||||
| 
 | 
 | ||||||
|     private def check_node(source, node) |  | ||||||
|       return if (expected = node.name.underscore) == node.name |  | ||||||
| 
 |  | ||||||
|       issue_for node, MSG % {expected, node.name} |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     def test(source : Source) |     def test(source : Source) | ||||||
|       VarVisitor.new self, source |       VarVisitor.new self, source | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def test(source, node : Crystal::Var | Crystal::InstanceVar | Crystal::ClassVar) |     def test(source, node : Crystal::Var | Crystal::InstanceVar | Crystal::ClassVar) | ||||||
|       check_node source, node |       name = node.name.to_s | ||||||
|  |       expected = name.underscore | ||||||
|  |       return if name == expected | ||||||
|  | 
 | ||||||
|  |       issue_for node, MSG % {expected, name} | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     private class VarVisitor < AST::NodeVisitor |     private class VarVisitor < AST::NodeVisitor | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue