querly 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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