querly 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|