Merge pull request #205 from crystal-ameba/master-into-develop

Merge master into develop
This commit is contained in:
Vitalii Elenhaupt 2021-02-04 08:00:11 +02:00 committed by GitHub
commit 7697663fec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 193 additions and 18 deletions

View file

@ -3,12 +3,6 @@ name: CI
on: on:
push: push:
pull_request: pull_request:
branches:
# Branches from forks have the form 'user:branch-name' so we only run
# this job on pull_request events for branches that look like fork
# branches. Without this we would end up running this job twice for non
# forked PRs, once for the push and then once for opening the PR.
- "**:**"
schedule: schedule:
- cron: "0 3 * * 1" # Every monday at 3 AM - cron: "0 3 * * 1" # Every monday at 3 AM

View file

@ -1,5 +1,5 @@
name: ameba name: ameba
version: 0.13.3 version: 0.13.4
authors: authors:
- Vitalii Elenhaupt <velenhaupt@gmail.com> - Vitalii Elenhaupt <velenhaupt@gmail.com>

View file

@ -47,6 +47,68 @@ module Ameba::AST
end end
end end
describe "#references?" do
it "returns true if current scope references variable" do
nodes = as_nodes %(
def method
a = 2
block do
3.times { |i| a = a + i }
end
end
)
scope = Scope.new nodes.def_nodes.first
var_node = nodes.var_nodes.first
scope.add_variable var_node
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
variable = Variable.new(var_node, scope)
variable.reference nodes.var_nodes.first, scope.inner_scopes.first
scope.references?(variable).should be_true
end
it "returns false if inner scopes are not checked" do
nodes = as_nodes %(
def method
a = 2
block do
3.times { |i| a = a + i }
end
end
)
scope = Scope.new nodes.def_nodes.first
var_node = nodes.var_nodes.first
scope.add_variable var_node
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
variable = Variable.new(var_node, scope)
variable.reference nodes.var_nodes.first, scope.inner_scopes.first
scope.references?(variable, check_inner_scopes: false).should be_false
end
it "returns false if current scope does not reference variable" do
nodes = as_nodes %(
def method
a = 2
block do
b = 3
3.times { |i| b = b + i }
end
end
)
scope = Scope.new nodes.def_nodes.first
var_node = nodes.var_nodes.first
scope.add_variable var_node
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
variable = Variable.new(var_node, scope)
scope.inner_scopes.first.references?(variable).should be_false
end
end
describe "#add_variable" do describe "#add_variable" do
it "adds a new variable to the scope" do it "adds a new variable to the scope" do
scope = Scope.new as_node("") scope = Scope.new as_node("")

View file

@ -944,6 +944,108 @@ module Ameba::Rule::Lint
) )
subject.catch(s).should be_valid subject.catch(s).should be_valid
end end
it "doesn't report if assignment is referenced in a macro expression as string" do
s = Source.new %(
foo = 1
puts {{ "foo".id }}
)
subject.catch(s).should be_valid
end
it "doesn't report if assignement is referenced in for macro in exp" do
s = Source.new %(
foo = 22
{% for x in %w(foo) %}
add({{x.id}})
{% end %}
)
subject.catch(s).should be_valid
end
it "doesn't report if assignement is referenced in for macro in body" do
s = Source.new %(
foo = 22
{% for x in %w(bar) %}
puts {{ "foo".id }}
{% end %}
)
subject.catch(s).should be_valid
end
it "doesn't report if assignement is referenced in if macro in cond" do
s = Source.new %(
foo = 22
{% if "foo".id %}
{% end %}
)
subject.catch(s).should be_valid
end
it "doesn't report if assignement is referenced in if macro in then" do
s = Source.new %(
foo = 22
{% if true %}
puts {{ "foo".id }}
{% end %}
)
subject.catch(s).should be_valid
end
it "doesn't report if assignement is referenced in if macro in else" do
s = Source.new %(
foo = 22
{% if true %}
{% else %}
puts {{ "foo".id }}
{% end %}
)
subject.catch(s).should be_valid
end
end
it "does not report if variable is referenced and there is a deep level scope" do
s = Source.new %(
response = JSON.build do |json|
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
anything
end
end
end
end
end
end
end
end
end
end
end
end
end
end
end
end
response = JSON.parse(response)
response
)
subject.catch(s).should be_valid
end end
context "uninitialized" do context "uninitialized" do

View file

@ -134,10 +134,10 @@ module Ameba::AST
# Returns true if current scope (or any of inner scopes) references variable, # Returns true if current scope (or any of inner scopes) references variable,
# false if not. # false if not.
def references?(variable : Variable) def references?(variable : Variable, check_inner_scopes = true)
variable.references.any? do |reference| variable.references.any? do |reference|
reference.scope == self || return true if reference.scope == self
inner_scopes.any?(&.references?(variable)) check_inner_scopes && inner_scopes.any?(&.references?(variable))
end || variable.used_in_macro? end || variable.used_in_macro?
end end

View file

@ -110,19 +110,20 @@ module Ameba::AST
# ``` # ```
def captured_by_block?(scope = @scope) def captured_by_block?(scope = @scope)
scope.inner_scopes.each do |inner_scope| scope.inner_scopes.each do |inner_scope|
return true if inner_scope.block? && inner_scope.references?(self) return true if inner_scope.block? && inner_scope.references?(self, check_inner_scopes: false)
return true if captured_by_block?(inner_scope) return true if captured_by_block?(inner_scope)
end end
false false
end end
# Returns true if current variable potentially referenced in a macro literal, # Returns true if current variable potentially referenced in a macro,
# false if not. # false if not.
def used_in_macro?(scope = @scope) def used_in_macro?(scope = @scope)
scope.inner_scopes.each do |inner_scope| scope.inner_scopes.each do |inner_scope|
return true if MacroLiteralFinder.new(inner_scope.node).references? node return true if MacroReferenceFinder.new(inner_scope.node, node.name).references
end end
return true if MacroReferenceFinder.new(scope.node, node.name).references
return true if (outer_scope = scope.outer_scope) && used_in_macro?(outer_scope) return true if (outer_scope = scope.outer_scope) && used_in_macro?(outer_scope)
false false
end end
@ -164,10 +165,10 @@ module Ameba::AST
var_location.column_number < node_location.column_number) var_location.column_number < node_location.column_number)
end end
private class MacroLiteralFinder < Crystal::Visitor private class MacroReferenceFinder < Crystal::Visitor
@macro_literals = [] of Crystal::MacroLiteral property references = false
def initialize(node) def initialize(node, @reference : String = reference)
node.accept self node.accept self
end end
@ -180,7 +181,23 @@ module Ameba::AST
end end
def visit(node : Crystal::MacroLiteral) def visit(node : Crystal::MacroLiteral)
@macro_literals << node !(self.references ||= node.value.includes?(@reference))
end
def visit(node : Crystal::MacroExpression)
!(self.references ||= node.exp.to_s.includes?(@reference))
end
def visit(node : Crystal::MacroFor)
exp, body = node.exp, node.body
!(self.references ||= exp.to_s.includes?(@reference) ||
body.to_s.includes?(@reference))
end
def visit(node : Crystal::MacroIf)
!(self.references ||= node.cond.to_s.includes?(@reference) ||
node.then.to_s.includes?(@reference) ||
node.else.to_s.includes?(@reference))
end end
end end

View file

@ -38,7 +38,7 @@ module Ameba::Rule::Lint
def test(source, node, scope : AST::Scope) def test(source, node, scope : AST::Scope)
scope.variables.each do |var| scope.variables.each do |var|
next if var.captured_by_block? || var.used_in_macro? || var.ignored? next if var.ignored? || var.used_in_macro? || var.captured_by_block?
var.assignments.each do |assign| var.assignments.each do |assign|
next if assign.referenced? || assign.transformed? next if assign.referenced? || assign.transformed?