querly 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +11 -10
- data/exe/querly-pp +7 -0
- data/lib/querly.rb +1 -1
- data/lib/querly/analyzer.rb +8 -31
- data/lib/querly/check.rb +104 -0
- data/lib/querly/cli.rb +16 -6
- data/lib/querly/cli/console.rb +2 -2
- data/lib/querly/cli/rules.rb +77 -0
- data/lib/querly/cli/test.rb +4 -5
- data/lib/querly/config.rb +70 -38
- data/lib/querly/node_pair.rb +14 -0
- data/lib/querly/pattern/expr.rb +38 -10
- data/lib/querly/pattern/parser.y +35 -8
- data/lib/querly/pp/cli.rb +74 -0
- data/lib/querly/rule.rb +54 -12
- data/lib/querly/script_enumerator.rb +7 -3
- data/lib/querly/version.rb +1 -1
- data/querly.gemspec +3 -2
- data/sample.yaml +29 -5
- metadata +25 -5
- data/lib/querly/tagging.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 23d5bf2430a932df4208453cc4847d06915a5140
|
4
|
+
data.tar.gz: 6be0f56ed32a3bded3ef3b9241633faedfc83c3a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 68bfb69f1c7628a24f2738bf5be828ce7b31d181a82f550ad1ed54b451d7612fa2ff7f4e6a6b6bf229dbfe147c13ff2a0656436c6b2680cfc800bd70106a34c3
|
7
|
+
data.tar.gz: 9794d372352bd5cff6704b93604ea6b94942a6e4eaf882602788b0660c579e80bfae6369843b5eb75cb55ae3d97848123e52517b4acbb825d1f961e731acacc0
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Change Log
|
2
|
+
|
3
|
+
## 0.2.0 (2016-11-24)
|
4
|
+
|
5
|
+
* Remove `tagging` section from config
|
6
|
+
* Add `check` section to select rules to check
|
7
|
+
* Add `import` section to load rules from other file
|
8
|
+
* Add `querly rules` sub command to print loaded rules
|
9
|
+
* Add *with block* and *without block* pattern (`foo {}` / `foo !{}`)
|
10
|
+
* Add *some of receiver chain* pattern (`...`)
|
11
|
+
* Fix keyword args pattern matching bug
|
12
|
+
|
13
|
+
## 0.1.0
|
14
|
+
|
15
|
+
* First release.
|
data/README.md
CHANGED
@@ -1,22 +1,23 @@
|
|
1
|
-
# Querly -
|
1
|
+
# Querly - Pattern Based Checking Tool for Ruby
|
2
2
|
|
3
3
|
[](https://travis-ci.org/soutaro/querly)
|
4
4
|
|
5
5
|
Querly is a query language and tool to find out method calls from Ruby programs.
|
6
|
-
|
6
|
+
Define rules to check your program with patterns to find out *bad* pieces.
|
7
|
+
Querly finds out matching pieces from your program.
|
7
8
|
|
8
9
|
## Overview
|
9
10
|
|
10
|
-
Your project
|
11
|
+
Your project may have many local rules:
|
11
12
|
|
12
|
-
* Should not use `Customer#update_mail`
|
13
|
+
* Should not use `Customer#update_mail` and use 30x faster `Customer.update_all_email` instead (Slower `#update_mail` is left just for existing code, but new code should not use it)
|
13
14
|
* Should not use `root_url` without `locale:` parameter
|
14
15
|
* Should not use `Net::HTTP` for Web API calls, but use `HTTPClient`
|
15
16
|
|
16
17
|
These local rule violations will be found during code review.
|
17
|
-
Reviewers will ask commiter to revise
|
18
|
+
Reviewers will ask commiter to revise; commiter will fix; fine.
|
18
19
|
Really?
|
19
|
-
It is boring and time
|
20
|
+
It is boring and time-consuming.
|
20
21
|
We need some automation!
|
21
22
|
|
22
23
|
However, that rules cannot be the standard.
|
@@ -43,7 +44,7 @@ rules:
|
|
43
44
|
message: Use HTTPClient to make HTTP request
|
44
45
|
```
|
45
46
|
|
46
|
-
Write down your local rules, and let Querly
|
47
|
+
Write down your local rules, and let Querly check conformance with them.
|
47
48
|
Focus on spec, design, UX, and other important things during code review!
|
48
49
|
|
49
50
|
## Installation
|
@@ -66,8 +67,8 @@ Copy the following YAML and paste as `querly.yml` in your project's repo.
|
|
66
67
|
rules:
|
67
68
|
- id: sample.debug_print
|
68
69
|
pattern:
|
69
|
-
- p
|
70
|
-
- pp
|
70
|
+
- self.p
|
71
|
+
- self.pp
|
71
72
|
message: Delete debug print
|
72
73
|
```
|
73
74
|
|
@@ -120,7 +121,7 @@ There will be many false positives, and *querly warning free code* does not make
|
|
120
121
|
|
121
122
|
* TODO: support to ignore warnings through magic comments in code
|
122
123
|
|
123
|
-
|
124
|
+
Querly is not to ensure *there is nothing wrong in the code*, but just tells you *code fragments you should review with special care*.
|
124
125
|
I believe it still improves your software development productivity.
|
125
126
|
|
126
127
|
### Incoming updates?
|
data/exe/querly-pp
ADDED
data/lib/querly.rb
CHANGED
data/lib/querly/analyzer.rb
CHANGED
@@ -1,13 +1,11 @@
|
|
1
1
|
module Querly
|
2
2
|
class Analyzer
|
3
|
-
attr_reader :
|
3
|
+
attr_reader :config
|
4
4
|
attr_reader :scripts
|
5
|
-
attr_reader :taggings
|
6
5
|
|
7
|
-
def initialize(
|
8
|
-
@
|
6
|
+
def initialize(config:)
|
7
|
+
@config = config
|
9
8
|
@scripts = []
|
10
|
-
@taggings = taggings
|
11
9
|
end
|
12
10
|
|
13
11
|
#
|
@@ -15,14 +13,11 @@ module Querly
|
|
15
13
|
#
|
16
14
|
def run
|
17
15
|
scripts.each do |script|
|
18
|
-
|
19
|
-
|
20
|
-
each_subnode script.root_pair do |node_pair|
|
16
|
+
rules = config.rules_for_path(script.path)
|
17
|
+
script.root_pair.each_subpair do |node_pair|
|
21
18
|
rules.each do |rule|
|
22
|
-
if
|
23
|
-
|
24
|
-
yield script, rule, node_pair
|
25
|
-
end
|
19
|
+
if rule.patterns.any? {|pattern| test_pair(node_pair, pattern) }
|
20
|
+
yield script, rule, node_pair
|
26
21
|
end
|
27
22
|
end
|
28
23
|
end
|
@@ -31,7 +26,7 @@ module Querly
|
|
31
26
|
|
32
27
|
def find(pattern)
|
33
28
|
scripts.each do |script|
|
34
|
-
|
29
|
+
script.root_pair.each_subpair do |node_pair|
|
35
30
|
if test_pair(node_pair, pattern)
|
36
31
|
yield script, node_pair
|
37
32
|
end
|
@@ -42,23 +37,5 @@ module Querly
|
|
42
37
|
def test_pair(node_pair, pattern)
|
43
38
|
pattern.expr =~ node_pair && pattern.test_kind(node_pair)
|
44
39
|
end
|
45
|
-
|
46
|
-
def applicable_rule?(tagging, rule)
|
47
|
-
if tagging && !rule.tags.empty?
|
48
|
-
tagging.tags_set.any? {|set| set.subset?(rule.tags) }
|
49
|
-
else
|
50
|
-
true
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def each_subnode(node_pair, &block)
|
55
|
-
return unless node_pair.node
|
56
|
-
|
57
|
-
yield node_pair
|
58
|
-
|
59
|
-
node_pair.children.each do |child|
|
60
|
-
each_subnode child, &block
|
61
|
-
end
|
62
|
-
end
|
63
40
|
end
|
64
41
|
end
|
data/lib/querly/check.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
module Querly
|
2
|
+
class Check
|
3
|
+
Query = Struct.new(:opr, :tags, :identifier) do
|
4
|
+
def apply(current:, all:)
|
5
|
+
case opr
|
6
|
+
when :append
|
7
|
+
current.union(all.select {|rule| match?(rule) })
|
8
|
+
when :except
|
9
|
+
current.reject {|rule| match?(rule) }.to_set
|
10
|
+
when :only
|
11
|
+
all.select {|rule| match?(rule) }.to_set
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def match?(rule)
|
16
|
+
rule.match?(identifier: identifier, tags: tags)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :patterns
|
21
|
+
attr_reader :rules
|
22
|
+
|
23
|
+
def initialize(pattern:, rules:)
|
24
|
+
@rules = rules
|
25
|
+
|
26
|
+
@has_trailing_slash = pattern.end_with?("/")
|
27
|
+
@has_middle_slash = /\/./ =~ pattern
|
28
|
+
|
29
|
+
@patterns = []
|
30
|
+
|
31
|
+
pattern.sub!(/\A\//, '')
|
32
|
+
|
33
|
+
case
|
34
|
+
when has_trailing_slash? && has_middle_slash?
|
35
|
+
patterns << File.join(pattern, "**")
|
36
|
+
when has_trailing_slash?
|
37
|
+
patterns << File.join(pattern, "**")
|
38
|
+
patterns << File.join("**", pattern, "**")
|
39
|
+
when has_middle_slash?
|
40
|
+
patterns << pattern
|
41
|
+
patterns << File.join(pattern, "**")
|
42
|
+
else
|
43
|
+
patterns << pattern
|
44
|
+
patterns << File.join("**", pattern)
|
45
|
+
patterns << File.join(pattern, "**")
|
46
|
+
patterns << File.join("**", pattern, "**")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def has_trailing_slash?
|
51
|
+
@has_trailing_slash
|
52
|
+
end
|
53
|
+
|
54
|
+
def has_middle_slash?
|
55
|
+
@has_middle_slash
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.load(hash)
|
59
|
+
pattern = hash["path"]
|
60
|
+
|
61
|
+
rules = Array(hash["rules"]).map do |rule|
|
62
|
+
case rule
|
63
|
+
when String
|
64
|
+
parse_rule_query(:append, rule)
|
65
|
+
when Hash
|
66
|
+
case
|
67
|
+
when rule["append"]
|
68
|
+
parse_rule_query(:append, rule["append"])
|
69
|
+
when rule["except"]
|
70
|
+
parse_rule_query(:except, rule["except"])
|
71
|
+
when rule["only"]
|
72
|
+
parse_rule_query(:only, rule["only"])
|
73
|
+
else
|
74
|
+
parse_rule_query(:append, rule)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
self.new(pattern: pattern, rules: rules)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.parse_rule_query(opr, query)
|
83
|
+
case query
|
84
|
+
when String
|
85
|
+
Query.new(opr, nil, query)
|
86
|
+
when Hash
|
87
|
+
if query['tags']
|
88
|
+
ts = query['tags']
|
89
|
+
if ts.is_a?(String)
|
90
|
+
ts = ts.split
|
91
|
+
end
|
92
|
+
tags = Set.new(ts)
|
93
|
+
end
|
94
|
+
identifier = query['id']
|
95
|
+
|
96
|
+
Query.new(opr, tags, identifier)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def match?(path:)
|
101
|
+
patterns.any? {|pat| File.fnmatch?(pat, path.to_s) }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/lib/querly/cli.rb
CHANGED
@@ -5,6 +5,7 @@ module Querly
|
|
5
5
|
class CLI < Thor
|
6
6
|
desc "check [paths]", "Check paths based on configuration"
|
7
7
|
option :config, default: "querly.yml"
|
8
|
+
option :root
|
8
9
|
option :format, default: "text", type: :string, enum: %w(text json)
|
9
10
|
def check(*paths)
|
10
11
|
require 'querly/cli/formatter'
|
@@ -26,18 +27,20 @@ Specify configuration file by --config option.
|
|
26
27
|
exit 1
|
27
28
|
end
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
30
|
+
root_option = options[:root]
|
31
|
+
root_path = root_option ? Pathname(root_option).realpath : config_path.parent.realpath
|
32
|
+
|
33
|
+
config = begin
|
34
|
+
yaml = YAML.load(config_path.read)
|
35
|
+
Config.load(yaml, config_path: config_path, root_dir: root_path, stderr: STDERR)
|
32
36
|
rescue => exn
|
33
37
|
formatter.config_error config_path, exn
|
34
38
|
exit
|
35
39
|
end
|
36
40
|
|
37
|
-
analyzer = Analyzer.new(
|
38
|
-
analyzer.rules.concat config.rules
|
41
|
+
analyzer = Analyzer.new(config: config)
|
39
42
|
|
40
|
-
ScriptEnumerator.new(paths: paths.map {|path| Pathname(path) },
|
43
|
+
ScriptEnumerator.new(paths: paths.map {|path| Pathname(path) }, config: config).each do |path, script|
|
41
44
|
case script
|
42
45
|
when Script
|
43
46
|
analyzer.scripts << script
|
@@ -71,6 +74,13 @@ Specify configuration file by --config option.
|
|
71
74
|
Test.new(config_path: config_path).run
|
72
75
|
end
|
73
76
|
|
77
|
+
desc "rules", "Print loaded rules"
|
78
|
+
option :config, default: "querly.yml"
|
79
|
+
def rules(*ids)
|
80
|
+
require "querly/cli/rules"
|
81
|
+
Rules.new(config_path: config_path, ids: ids).run
|
82
|
+
end
|
83
|
+
|
74
84
|
private
|
75
85
|
|
76
86
|
def config_path
|
data/lib/querly/cli/console.rb
CHANGED
@@ -33,9 +33,9 @@ Querly #{VERSION}, interactive console
|
|
33
33
|
def analyzer
|
34
34
|
return @analyzer if @analyzer
|
35
35
|
|
36
|
-
@analyzer = Analyzer.new(
|
36
|
+
@analyzer = Analyzer.new(config: nil)
|
37
37
|
|
38
|
-
ScriptEnumerator.new(paths: paths).each do |path, script|
|
38
|
+
ScriptEnumerator.new(paths: paths, config: nil).each do |path, script|
|
39
39
|
case script
|
40
40
|
when Script
|
41
41
|
@analyzer.scripts << script
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Querly
|
2
|
+
class CLI
|
3
|
+
class Rules
|
4
|
+
attr_reader :config_path
|
5
|
+
attr_reader :stdout
|
6
|
+
attr_reader :ids
|
7
|
+
|
8
|
+
def initialize(config_path:, ids:, stdout: STDOUT)
|
9
|
+
@config_path = config_path
|
10
|
+
@stdout = stdout
|
11
|
+
@ids = ids
|
12
|
+
end
|
13
|
+
|
14
|
+
def config
|
15
|
+
yaml = YAML.load(config_path.read)
|
16
|
+
@config ||= Config.load(yaml, config_path: config_path, root_dir: config_path.parent.realpath)
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
rules = config.rules.select {|rule| test_rule(rule) }
|
21
|
+
stdout.puts YAML.dump(rules.map {|rule| rule_to_yaml(rule) })
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_rule(rule)
|
25
|
+
if ids.empty?
|
26
|
+
true
|
27
|
+
else
|
28
|
+
ids.any? {|id| rule.match?(identifier: id) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def rule_to_yaml(rule)
|
33
|
+
{ "id" => rule.id }.tap do |hash|
|
34
|
+
singleton rule.sources do |a|
|
35
|
+
hash["pattern"] = a
|
36
|
+
end
|
37
|
+
|
38
|
+
singleton rule.messages do |a|
|
39
|
+
hash["message"] = a
|
40
|
+
end
|
41
|
+
|
42
|
+
empty rule.tags do |a|
|
43
|
+
hash["tags"] = a
|
44
|
+
end
|
45
|
+
|
46
|
+
singleton rule.justifications do |a|
|
47
|
+
hash["justification"] = a
|
48
|
+
end
|
49
|
+
|
50
|
+
singleton rule.before_examples do |a|
|
51
|
+
hash["before"] = a
|
52
|
+
end
|
53
|
+
|
54
|
+
singleton rule.after_examples do |a|
|
55
|
+
hash["after"] = a
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def empty(array)
|
61
|
+
unless array.empty?
|
62
|
+
yield array.to_a
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def singleton(array)
|
67
|
+
empty(array) do
|
68
|
+
if array.length == 1
|
69
|
+
yield array.first
|
70
|
+
else
|
71
|
+
yield array.to_a
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/querly/cli/test.rb
CHANGED
@@ -92,12 +92,12 @@ module Querly
|
|
92
92
|
end
|
93
93
|
|
94
94
|
def test_pattern(pattern, example, expected:)
|
95
|
-
analyzer = Analyzer.new(
|
95
|
+
analyzer = Analyzer.new(config: nil)
|
96
96
|
|
97
97
|
found = false
|
98
98
|
|
99
99
|
node = Parser::CurrentRuby.parse(example)
|
100
|
-
|
100
|
+
NodePair.new(node: node).each_subpair do |pair|
|
101
101
|
if analyzer.test_pair(pair, pattern)
|
102
102
|
found = true
|
103
103
|
end
|
@@ -108,9 +108,8 @@ module Querly
|
|
108
108
|
|
109
109
|
def load_config
|
110
110
|
if config_path.file?
|
111
|
-
|
112
|
-
|
113
|
-
config
|
111
|
+
yaml = YAML.load(config_path.read)
|
112
|
+
Config.load(yaml, config_path: config_path, root_dir: config_path.parent.realpath, stderr: STDERR)
|
114
113
|
end
|
115
114
|
end
|
116
115
|
end
|
data/lib/querly/config.rb
CHANGED
@@ -1,55 +1,87 @@
|
|
1
1
|
module Querly
|
2
2
|
class Config
|
3
3
|
attr_reader :rules
|
4
|
-
attr_reader :paths
|
5
|
-
attr_reader :taggings
|
6
4
|
attr_reader :preprocessors
|
5
|
+
attr_reader :root_dir
|
6
|
+
attr_reader :checks
|
7
|
+
attr_reader :rules_cache
|
7
8
|
|
8
|
-
def initialize()
|
9
|
-
@rules =
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@
|
9
|
+
def initialize(rules:, preprocessors:, root_dir:, checks:)
|
10
|
+
@rules = rules
|
11
|
+
@root_dir = root_dir
|
12
|
+
@preprocessors = preprocessors
|
13
|
+
@checks = checks
|
14
|
+
@rules_cache = {}
|
13
15
|
end
|
14
16
|
|
15
|
-
def
|
16
|
-
|
17
|
+
def self.load(hash, config_path:, root_dir:, stderr: STDERR)
|
18
|
+
Factory.new(hash, config_path: config_path, root_dir: root_dir, stderr: stderr).config
|
19
|
+
end
|
17
20
|
|
18
|
-
|
19
|
-
|
20
|
-
load_taggings(content)
|
21
|
-
load_preprocessors(content["preprocessor"] || {})
|
21
|
+
def all_rules
|
22
|
+
@all_rules ||= Set.new(rules)
|
22
23
|
end
|
23
24
|
|
24
|
-
def
|
25
|
-
|
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
|
25
|
+
def relative_path_from_root(path)
|
26
|
+
path.absolute? ? path.relative_path_from(root_dir) : path.cleanpath
|
41
27
|
end
|
42
28
|
|
43
|
-
def
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
29
|
+
def rules_for_path(path)
|
30
|
+
relative_path = relative_path_from_root(path)
|
31
|
+
matching_checks = checks.select {|check| check.match?(path: relative_path) }
|
32
|
+
|
33
|
+
if rules_cache.key?(matching_checks)
|
34
|
+
rules_cache[matching_checks]
|
35
|
+
else
|
36
|
+
matching_checks.flat_map(&:rules).inject(all_rules) do |rules, query|
|
37
|
+
query.apply(current: rules, all: all_rules)
|
38
|
+
end.tap do |rules|
|
39
|
+
rules_cache[matching_checks] = rules
|
40
|
+
end
|
41
|
+
end
|
48
42
|
end
|
49
43
|
|
50
|
-
|
51
|
-
|
52
|
-
|
44
|
+
class Factory
|
45
|
+
attr_reader :yaml
|
46
|
+
attr_reader :root_dir
|
47
|
+
attr_reader :stderr
|
48
|
+
attr_reader :config_path
|
49
|
+
|
50
|
+
def initialize(yaml, config_path:, root_dir:, stderr: STDERR)
|
51
|
+
@yaml = yaml
|
52
|
+
@config_path = config_path
|
53
|
+
@root_dir = root_dir
|
54
|
+
@stderr = stderr
|
55
|
+
end
|
56
|
+
|
57
|
+
def config
|
58
|
+
if yaml["tagging"]
|
59
|
+
stderr.puts "tagging is deprecated and ignored"
|
60
|
+
end
|
61
|
+
|
62
|
+
rules = Array(yaml["rules"]).map {|hash| Rule.load(hash) }
|
63
|
+
preprocessors = (yaml["preprocessor"] || {}).each.with_object({}) do |(key, value), hash|
|
64
|
+
hash[key] = Preprocessor.new(ext: key, command: value)
|
65
|
+
end
|
66
|
+
|
67
|
+
imports = Array(yaml["import"])
|
68
|
+
imports.each do |import|
|
69
|
+
if import["load"]
|
70
|
+
load_pattern = Pathname(import["load"])
|
71
|
+
load_pattern = config_path.parent + load_pattern if load_pattern.relative?
|
72
|
+
|
73
|
+
Pathname.glob(load_pattern.to_s) do |path|
|
74
|
+
stderr.puts "Loading rules from #{path}..."
|
75
|
+
YAML.load(path.read).each do |hash|
|
76
|
+
rules << Rule.load(hash)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
checks = Array(yaml["check"]).map {|hash| Check.load(hash) }
|
83
|
+
|
84
|
+
Config.new(rules: rules, preprocessors: preprocessors, checks: checks, root_dir: root_dir)
|
53
85
|
end
|
54
86
|
end
|
55
87
|
end
|
data/lib/querly/node_pair.rb
CHANGED
@@ -17,5 +17,19 @@ module Querly
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
20
|
+
|
21
|
+
def each_subpair(&block)
|
22
|
+
if block_given?
|
23
|
+
return unless node
|
24
|
+
|
25
|
+
yield self
|
26
|
+
|
27
|
+
children.each do |child|
|
28
|
+
child.each_subpair &block
|
29
|
+
end
|
30
|
+
else
|
31
|
+
enum_for :each_subpair
|
32
|
+
end
|
33
|
+
end
|
20
34
|
end
|
21
35
|
end
|
data/lib/querly/pattern/expr.rb
CHANGED
@@ -128,6 +128,9 @@ module Querly
|
|
128
128
|
true
|
129
129
|
end
|
130
130
|
|
131
|
+
when :regexp
|
132
|
+
type == :regexp
|
133
|
+
|
131
134
|
end
|
132
135
|
end
|
133
136
|
end
|
@@ -136,18 +139,20 @@ module Querly
|
|
136
139
|
attr_reader :name
|
137
140
|
attr_reader :receiver
|
138
141
|
attr_reader :args
|
142
|
+
attr_reader :block
|
139
143
|
|
140
|
-
def initialize(receiver:, name:, args: Argument::AnySeq.new)
|
144
|
+
def initialize(receiver:, name:, block:, args: Argument::AnySeq.new)
|
141
145
|
@name = name
|
142
146
|
@receiver = receiver
|
143
147
|
@args = args
|
148
|
+
@block = block
|
144
149
|
end
|
145
150
|
|
146
151
|
def =~(pair)
|
147
152
|
# Skip send node with block
|
148
153
|
if pair.node.type == :send && pair.parent
|
149
154
|
if pair.parent.node.type == :block
|
150
|
-
if pair.parent.node.children.first
|
155
|
+
if pair.parent.node.children.first.equal? pair.node
|
151
156
|
return false
|
152
157
|
end
|
153
158
|
end
|
@@ -157,6 +162,9 @@ module Querly
|
|
157
162
|
end
|
158
163
|
|
159
164
|
def test_node(node)
|
165
|
+
return false if block == true && node.type != :block
|
166
|
+
return false if block == false && node.type == :block
|
167
|
+
|
160
168
|
node = node.children.first if node&.type == :block
|
161
169
|
|
162
170
|
case node&.type
|
@@ -173,15 +181,19 @@ module Querly
|
|
173
181
|
|
174
182
|
case args
|
175
183
|
when Argument::AnySeq
|
176
|
-
if args.tail
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
hash
|
182
|
-
|
184
|
+
if args.tail
|
185
|
+
if first_node
|
186
|
+
case
|
187
|
+
when nodes.last.type == :kwsplat
|
188
|
+
true
|
189
|
+
when nodes.last.type == :hash && args.tail.is_a?(Argument::KeyValue)
|
190
|
+
hash = hash_node_to_hash(nodes.last)
|
191
|
+
test_hash_args(hash, args.tail)
|
192
|
+
else
|
193
|
+
test_hash_args({}, args.tail)
|
194
|
+
end
|
183
195
|
else
|
184
|
-
|
196
|
+
test_hash_args({}, args.tail)
|
185
197
|
end
|
186
198
|
else
|
187
199
|
true
|
@@ -243,6 +255,22 @@ module Querly
|
|
243
255
|
end
|
244
256
|
end
|
245
257
|
|
258
|
+
class ReceiverContext < Base
|
259
|
+
attr_reader :receiver
|
260
|
+
|
261
|
+
def initialize(receiver:)
|
262
|
+
@receiver = receiver
|
263
|
+
end
|
264
|
+
|
265
|
+
def test_node(node)
|
266
|
+
if receiver.test_node(node)
|
267
|
+
true
|
268
|
+
else
|
269
|
+
node&.type == :send && test_node(node.children[0])
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
246
274
|
class Vcall < Base
|
247
275
|
attr_reader :name
|
248
276
|
|
data/lib/querly/pattern/parser.y
CHANGED
@@ -22,6 +22,7 @@ expr: constant { result = Expr::Constant.new(path: val[0]) }
|
|
22
22
|
| FLOAT { result = Expr::Literal.new(type: :float, value: val[0]) }
|
23
23
|
| SYMBOL { result = Expr::Literal.new(type: :symbol, value: val[0]) }
|
24
24
|
| NUMBER { result = Expr::Literal.new(type: :number, value: val[0]) }
|
25
|
+
| REGEXP { result = Expr::Literal.new(type: :regexp, value: nil) }
|
25
26
|
| DSTR { result = Expr::Dstr.new() }
|
26
27
|
| UNDERBAR { result = Expr::Any.new }
|
27
28
|
| NIL { result = Expr::Nil.new }
|
@@ -64,14 +65,34 @@ constant: UIDENT { result = [val[0]] }
|
|
64
65
|
| UIDENT COLONCOLON constant { result = [val[0]] + val[2] }
|
65
66
|
|
66
67
|
send: LIDENT { result = Expr::Vcall.new(name: val[0]) }
|
67
|
-
| method_name { result = Expr::Send.new(receiver: Expr::Any.new, name: val[0]) }
|
68
|
-
| method_name_or_ident LPAREN args RPAREN { result = Expr::Send.new(receiver: Expr::Any.new,
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
|
73
|
-
|
74
|
-
|
68
|
+
| method_name { result = Expr::Send.new(receiver: Expr::Any.new, name: val[0], block: nil) }
|
69
|
+
| method_name_or_ident LPAREN args RPAREN block { result = Expr::Send.new(receiver: Expr::Any.new,
|
70
|
+
name: val[0],
|
71
|
+
args: val[2],
|
72
|
+
block: val[4]) }
|
73
|
+
| receiver method_name_or_ident block { result = Expr::Send.new(receiver: val[0],
|
74
|
+
name: val[1],
|
75
|
+
args: Argument::AnySeq.new,
|
76
|
+
block: val[2]) }
|
77
|
+
| receiver method_name_or_ident block { result = Expr::Send.new(receiver: val[0],
|
78
|
+
name: val[1],
|
79
|
+
args: Argument::AnySeq.new,
|
80
|
+
block: val[2]) }
|
81
|
+
| receiver method_name_or_ident LPAREN args RPAREN block { result = Expr::Send.new(receiver: val[0],
|
82
|
+
name: val[1],
|
83
|
+
args: val[3],
|
84
|
+
block: val[5]) }
|
85
|
+
| receiver method_name_or_ident LPAREN args RPAREN block { result = Expr::Send.new(receiver: val[0],
|
86
|
+
name: val[1],
|
87
|
+
args: val[3],
|
88
|
+
block: val[5]) }
|
89
|
+
|
90
|
+
receiver: expr DOT { result = val[0] }
|
91
|
+
| expr DOTDOTDOT { result = Expr::ReceiverContext.new(receiver: val[0]) }
|
92
|
+
|
93
|
+
block: { result = nil }
|
94
|
+
| WITH_BLOCK { result = true }
|
95
|
+
| WITHOUT_BLOCK { result = false }
|
75
96
|
|
76
97
|
end
|
77
98
|
|
@@ -116,9 +137,15 @@ def next_token
|
|
116
137
|
[:SYMBOL, nil]
|
117
138
|
when input.scan(/:number:/)
|
118
139
|
[:NUMBER, nil]
|
140
|
+
when input.scan(/:regexp:/)
|
141
|
+
[:REGEXP, nil]
|
119
142
|
when input.scan(/:\w+/)
|
120
143
|
s = input.matched
|
121
144
|
[:SYMBOL, s[1, s.size - 1].to_sym]
|
145
|
+
when input.scan(/{}/)
|
146
|
+
[:WITH_BLOCK, nil]
|
147
|
+
when input.scan(/!{}/)
|
148
|
+
[:WITHOUT_BLOCK, nil]
|
122
149
|
when input.scan(/[+-]?[0-9]+\.[0-9]/)
|
123
150
|
[:FLOAT, input.matched.to_f]
|
124
151
|
when input.scan(/[+-]?[0-9]+/)
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require "optparse"
|
2
|
+
|
3
|
+
module Querly
|
4
|
+
module PP
|
5
|
+
class CLI
|
6
|
+
attr_reader :argv
|
7
|
+
attr_reader :command
|
8
|
+
attr_reader :load_paths
|
9
|
+
attr_reader :requires
|
10
|
+
|
11
|
+
attr_reader :stdin
|
12
|
+
attr_reader :stderr
|
13
|
+
attr_reader :stdout
|
14
|
+
|
15
|
+
def initialize(argv, stdin: STDIN, stdout: STDOUT, stderr: STDERR)
|
16
|
+
@argv = argv
|
17
|
+
@stdin = stdin
|
18
|
+
@stdout = stdout
|
19
|
+
@stderr = stderr
|
20
|
+
|
21
|
+
@load_paths = []
|
22
|
+
@requires = []
|
23
|
+
|
24
|
+
OptionParser.new do |opts|
|
25
|
+
opts.banner = "Usage: #{opts.program_name} pp-name [options]"
|
26
|
+
opts.on("-I dir") {|path| load_paths << path }
|
27
|
+
opts.on("-r lib") {|rq| requires << rq }
|
28
|
+
end.permute!(argv)
|
29
|
+
|
30
|
+
@command = argv.shift&.to_sym
|
31
|
+
end
|
32
|
+
|
33
|
+
def load_libs
|
34
|
+
load_paths.each do |path|
|
35
|
+
$LOAD_PATH << path
|
36
|
+
end
|
37
|
+
|
38
|
+
requires.each do |lib|
|
39
|
+
require lib
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
def run
|
45
|
+
available_commands = [:haml]
|
46
|
+
|
47
|
+
if available_commands.include?(command)
|
48
|
+
send :"run_#{command}"
|
49
|
+
else
|
50
|
+
stderr.puts "Unknown command: #{command}"
|
51
|
+
stderr.puts " available commands: #{available_commands.join(", ")}"
|
52
|
+
exit 1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def run_haml
|
57
|
+
require "haml"
|
58
|
+
load_libs
|
59
|
+
|
60
|
+
source = stdin.read
|
61
|
+
|
62
|
+
options = Haml::Options.new
|
63
|
+
|
64
|
+
parser = Haml::Parser.new(source, options)
|
65
|
+
parser.parse
|
66
|
+
|
67
|
+
compiler = Haml::Compiler.new(options)
|
68
|
+
compiler.compile(parser.root)
|
69
|
+
|
70
|
+
stdout.print compiler.precompiled
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/querly/rule.rb
CHANGED
@@ -2,25 +2,67 @@ module Querly
|
|
2
2
|
class Rule
|
3
3
|
attr_reader :id
|
4
4
|
attr_reader :patterns
|
5
|
-
|
6
5
|
attr_reader :messages
|
6
|
+
|
7
|
+
attr_reader :sources
|
7
8
|
attr_reader :justifications
|
8
9
|
attr_reader :before_examples
|
9
10
|
attr_reader :after_examples
|
10
11
|
attr_reader :tags
|
11
|
-
attr_reader :scope
|
12
12
|
|
13
|
-
def initialize(id:,
|
13
|
+
def initialize(id:, messages:, patterns:, sources:, tags:, before_examples:, after_examples:, justifications:)
|
14
14
|
@id = id
|
15
|
-
@
|
16
|
-
|
17
|
-
@
|
18
|
-
@
|
19
|
-
@
|
20
|
-
@
|
21
|
-
@
|
22
|
-
|
23
|
-
|
15
|
+
@patterns = patterns
|
16
|
+
@sources = sources
|
17
|
+
@messages = messages
|
18
|
+
@justifications = justifications
|
19
|
+
@before_examples = before_examples
|
20
|
+
@after_examples = after_examples
|
21
|
+
@tags = tags
|
22
|
+
end
|
23
|
+
|
24
|
+
def match?(identifier: nil, tags: nil)
|
25
|
+
if identifier
|
26
|
+
unless id == identifier || id.start_with?(identifier + ".")
|
27
|
+
return false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
if tags
|
32
|
+
unless tags.subset?(self.tags)
|
33
|
+
return false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
class InvalidRuleHashError < StandardError; end
|
41
|
+
|
42
|
+
def self.load(hash)
|
43
|
+
id = hash["id"]
|
44
|
+
raise InvalidRuleHashError, "id is missing" unless id
|
45
|
+
|
46
|
+
srcs = Array(hash["pattern"])
|
47
|
+
raise InvalidRuleHashError, "pattern is missing" if srcs.empty?
|
48
|
+
patterns = srcs.map {|src| Pattern::Parser.parse(src) }
|
49
|
+
|
50
|
+
messages = Array(hash["message"])
|
51
|
+
raise InvalidRuleHashError, "message is missing" if messages.empty?
|
52
|
+
|
53
|
+
tags = Set.new(Array(hash["tags"]))
|
54
|
+
before_examples = Array(hash["before"])
|
55
|
+
after_examples = Array(hash["after"])
|
56
|
+
justifications = Array(hash["justification"])
|
57
|
+
|
58
|
+
Rule.new(id: id,
|
59
|
+
messages: messages,
|
60
|
+
patterns: patterns,
|
61
|
+
sources: srcs,
|
62
|
+
tags: tags,
|
63
|
+
before_examples: before_examples,
|
64
|
+
after_examples: after_examples,
|
65
|
+
justifications: justifications)
|
24
66
|
end
|
25
67
|
end
|
26
68
|
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
module Querly
|
2
2
|
class ScriptEnumerator
|
3
3
|
attr_reader :paths
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :config
|
5
5
|
|
6
|
-
def initialize(paths:,
|
6
|
+
def initialize(paths:, config:)
|
7
7
|
@paths = paths
|
8
|
-
@
|
8
|
+
@config = config
|
9
9
|
end
|
10
10
|
|
11
11
|
def each(&block)
|
@@ -54,6 +54,10 @@ module Querly
|
|
54
54
|
yield(path, script)
|
55
55
|
end
|
56
56
|
|
57
|
+
def preprocessors
|
58
|
+
config&.preprocessors || {}
|
59
|
+
end
|
60
|
+
|
57
61
|
def enumerate_files_in_dir(path, &block)
|
58
62
|
if path.basename.to_s =~ /\A\.[^\.]+/
|
59
63
|
# skip hidden paths
|
data/lib/querly/version.rb
CHANGED
data/querly.gemspec
CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ["Soutaro Matsumoto"]
|
10
10
|
spec.email = ["matsumoto@soutaro.com"]
|
11
11
|
|
12
|
-
spec.summary = %q{
|
13
|
-
spec.description = %q{Querly is a query language and tool to find out method calls from Ruby programs.
|
12
|
+
spec.summary = %q{Pattern Based Checking Tool for Ruby}
|
13
|
+
spec.description = %q{Querly is a query language and tool to find out method calls from Ruby programs. Define rules to check your program with patterns to find out *bad* pieces. Querly finds out matching pieces from your program.}
|
14
14
|
spec.homepage = "https://github.com/soutaro/querly"
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`
|
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.add_development_dependency "rake", "~> 10.0"
|
26
26
|
spec.add_development_dependency "minitest", "~> 5.0"
|
27
27
|
spec.add_development_dependency "racc", "= 1.4.14"
|
28
|
+
spec.add_development_dependency "unification_assertion", "0.0.1"
|
28
29
|
|
29
30
|
spec.add_dependency 'thor', "~> 0.19"
|
30
31
|
spec.add_dependency "parser", "~> 2.3.1"
|
data/sample.yaml
CHANGED
@@ -118,10 +118,34 @@ rules:
|
|
118
118
|
tags:
|
119
119
|
- test
|
120
120
|
- capybara
|
121
|
+
- id: sample.order-group
|
122
|
+
pattern: order...group
|
123
|
+
before:
|
124
|
+
- records.where.order.group
|
125
|
+
- records.order.where.group
|
126
|
+
message: |
|
127
|
+
Using both group and order may generate broken SQL
|
128
|
+
|
121
129
|
preprocessor:
|
122
130
|
.slim: slimrb --compile
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
131
|
+
|
132
|
+
import:
|
133
|
+
- load: rules/*.yml
|
134
|
+
|
135
|
+
check:
|
136
|
+
- path: /
|
137
|
+
rules:
|
138
|
+
- except: minitest
|
139
|
+
- path: /test
|
140
|
+
rules:
|
141
|
+
- minitest
|
142
|
+
- except:
|
143
|
+
tags: capybara minitest
|
144
|
+
- path: /test/integration
|
145
|
+
rules:
|
146
|
+
- append:
|
147
|
+
tags: capybara minitest
|
148
|
+
- path: /features/step_definitions
|
149
|
+
rules:
|
150
|
+
- append:
|
151
|
+
tags: capybara minitest
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: querly
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Soutaro Matsumoto
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-11-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - '='
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 1.4.14
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: unification_assertion
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.0.1
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.0.1
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: thor
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -109,27 +123,33 @@ dependencies:
|
|
109
123
|
- !ruby/object:Gem::Version
|
110
124
|
version: '2.1'
|
111
125
|
description: Querly is a query language and tool to find out method calls from Ruby
|
112
|
-
programs.
|
126
|
+
programs. Define rules to check your program with patterns to find out *bad* pieces.
|
127
|
+
Querly finds out matching pieces from your program.
|
113
128
|
email:
|
114
129
|
- matsumoto@soutaro.com
|
115
130
|
executables:
|
116
131
|
- querly
|
132
|
+
- querly-pp
|
117
133
|
extensions: []
|
118
134
|
extra_rdoc_files: []
|
119
135
|
files:
|
120
136
|
- ".gitignore"
|
121
137
|
- ".travis.yml"
|
138
|
+
- CHANGELOG.md
|
122
139
|
- Gemfile
|
123
140
|
- README.md
|
124
141
|
- Rakefile
|
125
142
|
- bin/console
|
126
143
|
- bin/setup
|
127
144
|
- exe/querly
|
145
|
+
- exe/querly-pp
|
128
146
|
- lib/querly.rb
|
129
147
|
- lib/querly/analyzer.rb
|
148
|
+
- lib/querly/check.rb
|
130
149
|
- lib/querly/cli.rb
|
131
150
|
- lib/querly/cli/console.rb
|
132
151
|
- lib/querly/cli/formatter.rb
|
152
|
+
- lib/querly/cli/rules.rb
|
133
153
|
- lib/querly/cli/test.rb
|
134
154
|
- lib/querly/config.rb
|
135
155
|
- lib/querly/node_pair.rb
|
@@ -138,11 +158,11 @@ files:
|
|
138
158
|
- lib/querly/pattern/kind.rb
|
139
159
|
- lib/querly/pattern/parser.rb
|
140
160
|
- lib/querly/pattern/parser.y
|
161
|
+
- lib/querly/pp/cli.rb
|
141
162
|
- lib/querly/preprocessor.rb
|
142
163
|
- lib/querly/rule.rb
|
143
164
|
- lib/querly/script.rb
|
144
165
|
- lib/querly/script_enumerator.rb
|
145
|
-
- lib/querly/tagging.rb
|
146
166
|
- lib/querly/version.rb
|
147
167
|
- querly.gemspec
|
148
168
|
- sample.yaml
|
@@ -168,5 +188,5 @@ rubyforge_project:
|
|
168
188
|
rubygems_version: 2.5.1
|
169
189
|
signing_key:
|
170
190
|
specification_version: 4
|
171
|
-
summary:
|
191
|
+
summary: Pattern Based Checking Tool for Ruby
|
172
192
|
test_files: []
|
data/lib/querly/tagging.rb
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
module Querly
|
2
|
-
class Tagging
|
3
|
-
attr_reader :path_pattern
|
4
|
-
attr_reader :tags_set
|
5
|
-
|
6
|
-
def initialize(path_pattern:, tags_set:)
|
7
|
-
@path_pattern = path_pattern
|
8
|
-
@tags_set = tags_set
|
9
|
-
end
|
10
|
-
|
11
|
-
def applicable?(script)
|
12
|
-
return true unless path_pattern
|
13
|
-
|
14
|
-
pattern_components = path_pattern.split('/')
|
15
|
-
|
16
|
-
script_path = if script.path.absolute?
|
17
|
-
script.path
|
18
|
-
else
|
19
|
-
script.path.realpath
|
20
|
-
end
|
21
|
-
path_components = script_path.to_s.split(File::Separator)
|
22
|
-
|
23
|
-
path_components.each_cons(pattern_components.size) do |slice|
|
24
|
-
if slice == pattern_components
|
25
|
-
return true
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
false
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|