querly 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/soutaro/querly.svg?branch=master)](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
|