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.
@@ -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