querly 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/README.md +143 -0
- data/Rakefile +18 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/querly +8 -0
- data/lib/querly.rb +26 -0
- data/lib/querly/analyzer.rb +64 -0
- data/lib/querly/cli.rb +81 -0
- data/lib/querly/cli/console.rb +110 -0
- data/lib/querly/cli/formatter.rb +143 -0
- data/lib/querly/cli/test.rb +118 -0
- data/lib/querly/config.rb +56 -0
- data/lib/querly/node_pair.rb +21 -0
- data/lib/querly/pattern/argument.rb +61 -0
- data/lib/querly/pattern/expr.rb +301 -0
- data/lib/querly/pattern/kind.rb +78 -0
- data/lib/querly/pattern/parser.y +169 -0
- data/lib/querly/preprocessor.rb +27 -0
- data/lib/querly/rule.rb +26 -0
- data/lib/querly/script.rb +15 -0
- data/lib/querly/script_enumerator.rb +84 -0
- data/lib/querly/tagging.rb +32 -0
- data/lib/querly/version.rb +3 -0
- data/querly.gemspec +32 -0
- data/sample.yaml +127 -0
- metadata +172 -0
@@ -0,0 +1,143 @@
|
|
1
|
+
module Querly
|
2
|
+
class CLI
|
3
|
+
module Formatter
|
4
|
+
class Base
|
5
|
+
# Called when analyzer started
|
6
|
+
def start; end
|
7
|
+
|
8
|
+
# Called when config is successfully loaded
|
9
|
+
def config_load(config); end
|
10
|
+
|
11
|
+
# Called when failed to load config
|
12
|
+
# Exit(status == 0) after the call
|
13
|
+
def config_error(path, error); end
|
14
|
+
|
15
|
+
# Called when script is successfully loaded
|
16
|
+
def script_load(script); end
|
17
|
+
|
18
|
+
# Called when failed to load script
|
19
|
+
# Continue after the call
|
20
|
+
def script_error(path, error); end
|
21
|
+
|
22
|
+
# Called when issue is found
|
23
|
+
def issue_found(script, rule, pair); end
|
24
|
+
|
25
|
+
# Called on other error
|
26
|
+
# Abort(status != 0) after the call
|
27
|
+
def fatal_error(error)
|
28
|
+
STDERR.puts Rainbow("Fatal error: #{error}").red
|
29
|
+
STDERR.puts error.backtrace.inspect
|
30
|
+
end
|
31
|
+
|
32
|
+
# Called on exit/abort
|
33
|
+
def finish; end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Text < Base
|
37
|
+
def config_error(path, error)
|
38
|
+
STDERR.puts Rainbow("Failed to load configuration: #{path}").red
|
39
|
+
STDERR.puts error
|
40
|
+
STDERR.puts error.backtrace.inspect
|
41
|
+
end
|
42
|
+
|
43
|
+
def script_error(path, error)
|
44
|
+
STDERR.puts Rainbow("Failed to load script: #{path}").red
|
45
|
+
STDERR.puts error.inspect
|
46
|
+
end
|
47
|
+
|
48
|
+
def issue_found(script, rule, pair)
|
49
|
+
path = script.path.to_s
|
50
|
+
src = Rainbow(pair.node.loc.expression.source.split(/\n/).first).red
|
51
|
+
line = pair.node.loc.first_line
|
52
|
+
col = pair.node.loc.column
|
53
|
+
message = rule.messages.first.split(/\n/).first
|
54
|
+
|
55
|
+
STDOUT.puts "#{path}:#{line}:#{col}\t#{src}\t#{message}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class JSON < Base
|
60
|
+
def initialize
|
61
|
+
@issues = []
|
62
|
+
@script_errors = []
|
63
|
+
@config_errors = []
|
64
|
+
@fatal = nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def config_error(path, error)
|
68
|
+
@config_errors << [path, error]
|
69
|
+
end
|
70
|
+
|
71
|
+
def script_error(path, error)
|
72
|
+
@script_errors << [path, error]
|
73
|
+
end
|
74
|
+
|
75
|
+
def issue_found(script, rule, pair)
|
76
|
+
@issues << [script, rule, pair]
|
77
|
+
end
|
78
|
+
|
79
|
+
def finish
|
80
|
+
STDOUT.print as_json.to_json
|
81
|
+
end
|
82
|
+
|
83
|
+
def fatal_error(error)
|
84
|
+
super
|
85
|
+
@fatal = error
|
86
|
+
end
|
87
|
+
|
88
|
+
def as_json
|
89
|
+
case
|
90
|
+
when @fatal
|
91
|
+
# Fatal error found
|
92
|
+
{
|
93
|
+
fatal_error: {
|
94
|
+
message: @fatal.inspect,
|
95
|
+
backtrace: @fatal.backtrace
|
96
|
+
}
|
97
|
+
}
|
98
|
+
when !@config_errors.empty?
|
99
|
+
# Error found during config load
|
100
|
+
{
|
101
|
+
config_errors: @config_errors.map {|(path, error)|
|
102
|
+
{
|
103
|
+
path: path.to_s,
|
104
|
+
error: {
|
105
|
+
message: error.inspect,
|
106
|
+
backtrace: error.backtrace
|
107
|
+
}
|
108
|
+
}
|
109
|
+
}
|
110
|
+
}
|
111
|
+
else
|
112
|
+
# Successfully checked
|
113
|
+
{
|
114
|
+
issues: @issues.map {|(script, rule, pair)|
|
115
|
+
{
|
116
|
+
script: script.path.to_s,
|
117
|
+
rule: {
|
118
|
+
id: rule.id,
|
119
|
+
messages: rule.messages,
|
120
|
+
justifications: rule.justifications,
|
121
|
+
},
|
122
|
+
location: {
|
123
|
+
start: [pair.node.loc.first_line, pair.node.loc.column],
|
124
|
+
end: [pair.node.loc.last_line, pair.node.loc.last_column]
|
125
|
+
}
|
126
|
+
}
|
127
|
+
},
|
128
|
+
errors: @script_errors.map {|path, error|
|
129
|
+
{
|
130
|
+
path: path.to_s,
|
131
|
+
error: {
|
132
|
+
message: error.inspect,
|
133
|
+
backtrace: error.backtrace
|
134
|
+
}
|
135
|
+
}
|
136
|
+
}
|
137
|
+
}
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Querly
|
2
|
+
class CLI
|
3
|
+
class Test
|
4
|
+
attr_reader :config_path
|
5
|
+
attr_reader :stdout
|
6
|
+
attr_reader :stderr
|
7
|
+
|
8
|
+
def initialize(config_path:, stdout: STDOUT, stderr: STDERR)
|
9
|
+
@config_path = config_path
|
10
|
+
@stdout = stdout
|
11
|
+
@stderr = stderr
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
config = load_config
|
16
|
+
|
17
|
+
unless config
|
18
|
+
stdout.puts "There is nothing to test at #{config_path} ..."
|
19
|
+
stdout.puts "Make a configuration and run test again!"
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
validate_rule_uniqueness(config.rules)
|
24
|
+
validate_rule_patterns(config.rules)
|
25
|
+
rescue => exn
|
26
|
+
stderr.puts Rainbow("Fatal error:").red
|
27
|
+
stderr.puts exn.inspect
|
28
|
+
stderr.puts exn.backtrace.map {|x| " " + x }.join("\n")
|
29
|
+
end
|
30
|
+
|
31
|
+
def validate_rule_uniqueness(rules)
|
32
|
+
ids = Set.new
|
33
|
+
|
34
|
+
stdout.puts "Checking rule id uniqueness..."
|
35
|
+
|
36
|
+
duplications = 0
|
37
|
+
|
38
|
+
rules.each do |rule|
|
39
|
+
unless ids.add?(rule.id)
|
40
|
+
stdout.puts Rainbow(" Rule id #{rule.id} duplicated!").red
|
41
|
+
duplications += 1
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate_rule_patterns(rules)
|
47
|
+
stdout.puts "Checking rule patterns..."
|
48
|
+
|
49
|
+
tests = 0
|
50
|
+
false_positives = 0
|
51
|
+
false_negatives = 0
|
52
|
+
errors = 0
|
53
|
+
|
54
|
+
rules.each do |rule|
|
55
|
+
rule.before_examples.each.with_index do |example, example_index|
|
56
|
+
tests += 1
|
57
|
+
|
58
|
+
begin
|
59
|
+
unless rule.patterns.any? {|pat| test_pattern(pat, example, expected: true) }
|
60
|
+
stdout.puts(Rainbow(" #{rule.id}").red + ":\t#{example_index}th *before* example didn't match with any pattern")
|
61
|
+
false_negatives += 1
|
62
|
+
end
|
63
|
+
rescue Parser::SyntaxError
|
64
|
+
errors += 1
|
65
|
+
stdout.puts(Rainbow(" #{rule.id}").red + ":\tParsing failed for #{example_index}th *before* example")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
rule.after_examples.each.with_index do |example, example_index|
|
70
|
+
tests += 1
|
71
|
+
|
72
|
+
begin
|
73
|
+
unless rule.patterns.all? {|pat| test_pattern(pat, example, expected: false) }
|
74
|
+
stdout.puts(Rainbow(" #{rule.id}").red + ":\t#{example_index}th *after* example matched with some of patterns")
|
75
|
+
false_positives += 1
|
76
|
+
end
|
77
|
+
rescue Parser::SyntaxError
|
78
|
+
errors += 1
|
79
|
+
stdout.puts(Rainbow(" #{rule.id}") + ":\tParsing failed for #{example_index}th *after* example")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
stdout.puts "Tested #{rules.size} rules with #{tests} tests."
|
85
|
+
if false_positives > 0 || false_negatives > 0 || errors > 0
|
86
|
+
stdout.puts " #{false_positives} examples found which should not match, but matched"
|
87
|
+
stdout.puts " #{false_negatives} examples found which should match, but didn't"
|
88
|
+
stdout.puts " #{errors} examples raised error"
|
89
|
+
else
|
90
|
+
stdout.puts Rainbow(" All tests green!").green
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_pattern(pattern, example, expected:)
|
95
|
+
analyzer = Analyzer.new(taggings: [])
|
96
|
+
|
97
|
+
found = false
|
98
|
+
|
99
|
+
node = Parser::CurrentRuby.parse(example)
|
100
|
+
analyzer.each_subnode NodePair.new(node: node) do |pair|
|
101
|
+
if analyzer.test_pair(pair, pattern)
|
102
|
+
found = true
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
found == expected
|
107
|
+
end
|
108
|
+
|
109
|
+
def load_config
|
110
|
+
if config_path.file?
|
111
|
+
config = Config.new
|
112
|
+
config.add_file config_path
|
113
|
+
config
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Querly
|
2
|
+
class Config
|
3
|
+
attr_reader :rules
|
4
|
+
attr_reader :paths
|
5
|
+
attr_reader :taggings
|
6
|
+
attr_reader :preprocessors
|
7
|
+
|
8
|
+
def initialize()
|
9
|
+
@rules = []
|
10
|
+
@paths = []
|
11
|
+
@taggings = []
|
12
|
+
@preprocessors = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_file(path)
|
16
|
+
paths << path
|
17
|
+
|
18
|
+
content = YAML.load(path.read)
|
19
|
+
load_rules(content)
|
20
|
+
load_taggings(content)
|
21
|
+
load_preprocessors(content["preprocessor"] || {})
|
22
|
+
end
|
23
|
+
|
24
|
+
def load_rules(yaml)
|
25
|
+
yaml["rules"].each do |hash|
|
26
|
+
id = hash["id"]
|
27
|
+
patterns = Array(hash["pattern"]).map {|src| Pattern::Parser.parse(src) }
|
28
|
+
messages = Array(hash["message"])
|
29
|
+
justifications = Array(hash["justification"])
|
30
|
+
|
31
|
+
rule = Rule.new(id: id)
|
32
|
+
rule.patterns.concat patterns
|
33
|
+
rule.messages.concat messages
|
34
|
+
rule.justifications.concat justifications
|
35
|
+
Array(hash["tags"]).each {|tag| rule.tags << tag }
|
36
|
+
rule.before_examples.concat Array(hash["before"])
|
37
|
+
rule.after_examples.concat Array(hash["after"])
|
38
|
+
|
39
|
+
rules << rule
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def load_taggings(yaml)
|
44
|
+
@taggings = Array(yaml["tagging"]).map {|hash|
|
45
|
+
Tagging.new(path_pattern: hash["path"],
|
46
|
+
tags_set: Array(hash["tags"]).map {|string| Set.new(string.split) })
|
47
|
+
}.sort_by {|tagging| -tagging.path_pattern.size }
|
48
|
+
end
|
49
|
+
|
50
|
+
def load_preprocessors(preprocessors)
|
51
|
+
@preprocessors = preprocessors.each.with_object({}) do |(key, value), hash|
|
52
|
+
hash[key] = Preprocessor.new(ext: key, command: value)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Querly
|
2
|
+
class NodePair
|
3
|
+
attr_reader :node
|
4
|
+
attr_reader :parent
|
5
|
+
|
6
|
+
def initialize(node:, parent: nil)
|
7
|
+
@node = node
|
8
|
+
@parent = parent
|
9
|
+
end
|
10
|
+
|
11
|
+
def children
|
12
|
+
node.children.flat_map do |child|
|
13
|
+
if child.is_a?(Parser::AST::Node)
|
14
|
+
self.class.new(node: child, parent: self)
|
15
|
+
else
|
16
|
+
[]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Querly
|
2
|
+
module Pattern
|
3
|
+
module Argument
|
4
|
+
class Base
|
5
|
+
attr_reader :tail
|
6
|
+
|
7
|
+
def initialize(tail:)
|
8
|
+
@tail = tail
|
9
|
+
end
|
10
|
+
|
11
|
+
def ==(other)
|
12
|
+
other.class == self.class && other.attributes == attributes
|
13
|
+
end
|
14
|
+
|
15
|
+
def attributes
|
16
|
+
instance_variables.each.with_object({}) do |name, hash|
|
17
|
+
hash[name] = instance_variable_get(name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class AnySeq < Base
|
23
|
+
def initialize(tail: nil)
|
24
|
+
super(tail: tail)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Expr < Base
|
29
|
+
attr_reader :expr
|
30
|
+
|
31
|
+
def initialize(expr:, tail:)
|
32
|
+
@expr = expr
|
33
|
+
super(tail: tail)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class KeyValue < Base
|
38
|
+
attr_reader :key
|
39
|
+
attr_reader :value
|
40
|
+
attr_reader :negated
|
41
|
+
|
42
|
+
def initialize(key:, value:, tail:, negated: false)
|
43
|
+
@key = key
|
44
|
+
@value = value
|
45
|
+
@negated = negated
|
46
|
+
|
47
|
+
super(tail: tail)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class BlockPass < Base
|
52
|
+
attr_reader :expr
|
53
|
+
|
54
|
+
def initialize(expr:)
|
55
|
+
@expr = expr
|
56
|
+
super(tail: nil)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,301 @@
|
|
1
|
+
module Querly
|
2
|
+
module Pattern
|
3
|
+
module Expr
|
4
|
+
class Base
|
5
|
+
def =~(pair)
|
6
|
+
test_node(pair.node)
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_node(node)
|
10
|
+
false
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
other.class == self.class && other.attributes == attributes
|
15
|
+
end
|
16
|
+
|
17
|
+
def attributes
|
18
|
+
instance_variables.each.with_object({}) do |name, hash|
|
19
|
+
hash[name] = instance_variable_get(name)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Any < Base
|
25
|
+
def test_node(node)
|
26
|
+
true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Not < Base
|
31
|
+
attr_reader :pattern
|
32
|
+
|
33
|
+
def initialize(pattern:)
|
34
|
+
@pattern = pattern
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_node(node)
|
38
|
+
!pattern.test_node(node)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Constant < Base
|
43
|
+
attr_reader :path
|
44
|
+
|
45
|
+
def initialize(path:)
|
46
|
+
@path = path
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_node(node)
|
50
|
+
if path
|
51
|
+
test_constant node, path
|
52
|
+
else
|
53
|
+
node&.type == :const
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_constant(node, path)
|
58
|
+
if node
|
59
|
+
case node.type
|
60
|
+
when :const
|
61
|
+
parent = node.children[0]
|
62
|
+
name = node.children[1]
|
63
|
+
|
64
|
+
if name == path.last
|
65
|
+
path.count == 1 || test_constant(parent, path.take(path.count - 1))
|
66
|
+
end
|
67
|
+
when :cbase
|
68
|
+
path.empty?
|
69
|
+
end
|
70
|
+
else
|
71
|
+
path.empty?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class Nil < Base
|
77
|
+
def test_node(node)
|
78
|
+
node&.type == :nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class Literal < Base
|
83
|
+
attr_reader :type
|
84
|
+
attr_reader :value
|
85
|
+
|
86
|
+
def initialize(type:, value: nil)
|
87
|
+
@type = type
|
88
|
+
@value = value
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_node(node)
|
92
|
+
case node&.type
|
93
|
+
when :int
|
94
|
+
return false unless type == :int || type == :number
|
95
|
+
if value
|
96
|
+
value == node.children.first
|
97
|
+
else
|
98
|
+
true
|
99
|
+
end
|
100
|
+
|
101
|
+
when :float
|
102
|
+
return false unless type == :float || type == :number
|
103
|
+
if value
|
104
|
+
value == node.children.first
|
105
|
+
else
|
106
|
+
true
|
107
|
+
end
|
108
|
+
|
109
|
+
when :true
|
110
|
+
type == :bool && (value == nil || value == true)
|
111
|
+
|
112
|
+
when :false
|
113
|
+
type == :bool && (value == nil || value == false)
|
114
|
+
|
115
|
+
when :str
|
116
|
+
return false unless type == :string
|
117
|
+
if value
|
118
|
+
value == node.children.first
|
119
|
+
else
|
120
|
+
true
|
121
|
+
end
|
122
|
+
|
123
|
+
when :sym
|
124
|
+
return false unless type == :symbol
|
125
|
+
if value
|
126
|
+
value == node.children.first
|
127
|
+
else
|
128
|
+
true
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class Send < Base
|
136
|
+
attr_reader :name
|
137
|
+
attr_reader :receiver
|
138
|
+
attr_reader :args
|
139
|
+
|
140
|
+
def initialize(receiver:, name:, args: Argument::AnySeq.new)
|
141
|
+
@name = name
|
142
|
+
@receiver = receiver
|
143
|
+
@args = args
|
144
|
+
end
|
145
|
+
|
146
|
+
def =~(pair)
|
147
|
+
# Skip send node with block
|
148
|
+
if pair.node.type == :send && pair.parent
|
149
|
+
if pair.parent.node.type == :block
|
150
|
+
if pair.parent.node.children.first == pair.node
|
151
|
+
return false
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
test_node pair.node
|
157
|
+
end
|
158
|
+
|
159
|
+
def test_node(node)
|
160
|
+
node = node.children.first if node&.type == :block
|
161
|
+
|
162
|
+
case node&.type
|
163
|
+
when :send
|
164
|
+
return false unless name == node.children[1]
|
165
|
+
return false unless receiver.test_node(node.children[0])
|
166
|
+
return false unless test_args(node.children.drop(2), args)
|
167
|
+
true
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def test_args(nodes, args)
|
172
|
+
first_node = nodes.first
|
173
|
+
|
174
|
+
case args
|
175
|
+
when Argument::AnySeq
|
176
|
+
if args.tail && first_node
|
177
|
+
case
|
178
|
+
when nodes.last.type == :kwsplat
|
179
|
+
true
|
180
|
+
when nodes.last.type == :hash && args.tail.is_a?(Argument::KeyValue)
|
181
|
+
hash = hash_node_to_hash(nodes.last)
|
182
|
+
test_hash_args(hash, args.tail)
|
183
|
+
else
|
184
|
+
true
|
185
|
+
end
|
186
|
+
else
|
187
|
+
true
|
188
|
+
end
|
189
|
+
when Argument::Expr
|
190
|
+
if first_node
|
191
|
+
args.expr.test_node(nodes.first) && test_args(nodes.drop(1), args.tail)
|
192
|
+
end
|
193
|
+
when Argument::KeyValue
|
194
|
+
if first_node
|
195
|
+
types = nodes.map(&:type)
|
196
|
+
if types == [:hash]
|
197
|
+
hash = hash_node_to_hash(nodes.first)
|
198
|
+
test_hash_args(hash, args)
|
199
|
+
elsif types == [:hash, :kwsplat]
|
200
|
+
true
|
201
|
+
else
|
202
|
+
args.negated
|
203
|
+
end
|
204
|
+
else
|
205
|
+
test_hash_args({}, args)
|
206
|
+
end
|
207
|
+
when Argument::BlockPass
|
208
|
+
first_node&.type == :block_pass && args.expr.test_node(first_node.children.first)
|
209
|
+
when nil
|
210
|
+
nodes.empty?
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def hash_node_to_hash(node)
|
215
|
+
node.children.each.with_object({}) do |pair, h|
|
216
|
+
key = pair.children[0]
|
217
|
+
value = pair.children[1]
|
218
|
+
|
219
|
+
if key.type == :sym
|
220
|
+
h[key.children[0]] = value
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def test_hash_args(hash, args)
|
226
|
+
while args
|
227
|
+
if args.is_a?(Argument::KeyValue)
|
228
|
+
node = hash[args.key]
|
229
|
+
|
230
|
+
if !args.negated == !!(node && args.value.test_node(node))
|
231
|
+
hash.delete args.key
|
232
|
+
else
|
233
|
+
return false
|
234
|
+
end
|
235
|
+
else
|
236
|
+
break
|
237
|
+
end
|
238
|
+
|
239
|
+
args = args.tail
|
240
|
+
end
|
241
|
+
|
242
|
+
args.is_a?(Argument::AnySeq) || hash.empty?
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
class Vcall < Base
|
247
|
+
attr_reader :name
|
248
|
+
|
249
|
+
def initialize(name:)
|
250
|
+
@name = name
|
251
|
+
end
|
252
|
+
|
253
|
+
def =~(pair)
|
254
|
+
node = pair.node
|
255
|
+
|
256
|
+
if node.type == :lvar
|
257
|
+
# We don't want lvar without method call
|
258
|
+
# Skips when the node is not receiver of :send
|
259
|
+
parent_node = pair.parent&.node
|
260
|
+
if parent_node && parent_node.type == :send && parent_node.children.first.equal?(node)
|
261
|
+
test_node(node)
|
262
|
+
end
|
263
|
+
else
|
264
|
+
test_node(node)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def test_node(node)
|
269
|
+
case node&.type
|
270
|
+
when :send
|
271
|
+
node.children[1] == name
|
272
|
+
when :lvar
|
273
|
+
node.children.first == name
|
274
|
+
when :self
|
275
|
+
name == :self
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
class Dstr < Base
|
281
|
+
def test_node(node)
|
282
|
+
node&.type == :dstr
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
class Ivar < Base
|
287
|
+
attr_reader :name
|
288
|
+
|
289
|
+
def initialize(name:)
|
290
|
+
@name = name
|
291
|
+
end
|
292
|
+
|
293
|
+
def test_node(node)
|
294
|
+
if node&.type == :ivar
|
295
|
+
name.nil? || node.children.first == name
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|