querly 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/README.md +143 -0
- data/Rakefile +18 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/querly +8 -0
- data/lib/querly.rb +26 -0
- data/lib/querly/analyzer.rb +64 -0
- data/lib/querly/cli.rb +81 -0
- data/lib/querly/cli/console.rb +110 -0
- data/lib/querly/cli/formatter.rb +143 -0
- data/lib/querly/cli/test.rb +118 -0
- data/lib/querly/config.rb +56 -0
- data/lib/querly/node_pair.rb +21 -0
- data/lib/querly/pattern/argument.rb +61 -0
- data/lib/querly/pattern/expr.rb +301 -0
- data/lib/querly/pattern/kind.rb +78 -0
- data/lib/querly/pattern/parser.y +169 -0
- data/lib/querly/preprocessor.rb +27 -0
- data/lib/querly/rule.rb +26 -0
- data/lib/querly/script.rb +15 -0
- data/lib/querly/script_enumerator.rb +84 -0
- data/lib/querly/tagging.rb +32 -0
- data/lib/querly/version.rb +3 -0
- data/querly.gemspec +32 -0
- data/sample.yaml +127 -0
- metadata +172 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
module Querly
|
2
|
+
module Pattern
|
3
|
+
module Kind
|
4
|
+
class Base
|
5
|
+
attr_reader :expr
|
6
|
+
|
7
|
+
def initialize(expr:)
|
8
|
+
@expr = expr
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module Negatable
|
13
|
+
attr_reader :negated
|
14
|
+
|
15
|
+
def initialize(expr:, negated:)
|
16
|
+
@negated = negated
|
17
|
+
super(expr: expr)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Any < Base
|
22
|
+
def test_kind(pair)
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Conditional < Base
|
28
|
+
include Negatable
|
29
|
+
|
30
|
+
def test_kind(pair)
|
31
|
+
!negated == !!conditional?(pair)
|
32
|
+
end
|
33
|
+
|
34
|
+
def conditional?(pair)
|
35
|
+
node = pair.node
|
36
|
+
parent = pair.parent&.node
|
37
|
+
|
38
|
+
case parent&.type
|
39
|
+
when :if
|
40
|
+
node.equal? parent.children.first
|
41
|
+
when :while
|
42
|
+
node.equal? parent.children.first
|
43
|
+
when :and
|
44
|
+
node.equal? parent.children.first
|
45
|
+
when :or
|
46
|
+
node.equal? parent.children.first
|
47
|
+
else
|
48
|
+
false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Discarded < Base
|
54
|
+
include Negatable
|
55
|
+
|
56
|
+
def test_kind(pair)
|
57
|
+
!negated == !!discarded?(pair)
|
58
|
+
end
|
59
|
+
|
60
|
+
def discarded?(pair)
|
61
|
+
node = pair.node
|
62
|
+
parent = pair.parent&.node
|
63
|
+
|
64
|
+
case parent&.type
|
65
|
+
when :begin
|
66
|
+
if node.equal? parent.children.last
|
67
|
+
discarded? pair.parent
|
68
|
+
else
|
69
|
+
true
|
70
|
+
end
|
71
|
+
else
|
72
|
+
false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
class Querly::Pattern::Parser
|
2
|
+
prechigh
|
3
|
+
nonassoc EXCLAMATION
|
4
|
+
nonassoc LPAREN
|
5
|
+
left DOT
|
6
|
+
preclow
|
7
|
+
|
8
|
+
rule
|
9
|
+
|
10
|
+
target: kinded_expr
|
11
|
+
|
12
|
+
kinded_expr: expr { result = Kind::Any.new(expr: val[0]) }
|
13
|
+
| expr CONDITIONAL_KIND { result = Kind::Conditional.new(expr: val[0], negated: val[1]) }
|
14
|
+
| expr DISCARDED_KIND { result = Kind::Discarded.new(expr: val[0], negated: val[1]) }
|
15
|
+
|
16
|
+
expr: constant { result = Expr::Constant.new(path: val[0]) }
|
17
|
+
| send
|
18
|
+
| EXCLAMATION expr { result = Expr::Not.new(pattern: val[1]) }
|
19
|
+
| BOOL { result = Expr::Literal.new(type: :bool, value: val[0]) }
|
20
|
+
| STRING { result = Expr::Literal.new(type: :string, value: val[0]) }
|
21
|
+
| INT { result = Expr::Literal.new(type: :int, value: val[0]) }
|
22
|
+
| FLOAT { result = Expr::Literal.new(type: :float, value: val[0]) }
|
23
|
+
| SYMBOL { result = Expr::Literal.new(type: :symbol, value: val[0]) }
|
24
|
+
| NUMBER { result = Expr::Literal.new(type: :number, value: val[0]) }
|
25
|
+
| DSTR { result = Expr::Dstr.new() }
|
26
|
+
| UNDERBAR { result = Expr::Any.new }
|
27
|
+
| NIL { result = Expr::Nil.new }
|
28
|
+
| LPAREN expr RPAREN { result = val[1] }
|
29
|
+
| IVAR { result = Expr::Ivar.new(name: val[0]) }
|
30
|
+
|
31
|
+
args: { result = nil }
|
32
|
+
| expr { result = Argument::Expr.new(expr: val[0], tail: nil)}
|
33
|
+
| expr COMMA args { result = Argument::Expr.new(expr: val[0], tail: val[2]) }
|
34
|
+
| AMP expr { result = Argument::BlockPass.new(expr: val[1]) }
|
35
|
+
| kw_args
|
36
|
+
| DOTDOTDOT { result = Argument::AnySeq.new }
|
37
|
+
| DOTDOTDOT COMMA kw_args { result = Argument::AnySeq.new(tail: val[2]) }
|
38
|
+
|
39
|
+
kw_args: { result = nil }
|
40
|
+
| AMP expr { result = Argument::BlockPass.new(expr: val[1]) }
|
41
|
+
| DOTDOTDOT { result = Argument::AnySeq.new }
|
42
|
+
| key_value { result = Argument::KeyValue.new(key: val[0][:key],
|
43
|
+
value: val[0][:value],
|
44
|
+
tail: nil,
|
45
|
+
negated: val[0][:negated]) }
|
46
|
+
| key_value COMMA kw_args { result = Argument::KeyValue.new(key: val[0][:key],
|
47
|
+
value: val[0][:value],
|
48
|
+
tail: val[2],
|
49
|
+
negated: val[0][:negated]) }
|
50
|
+
|
51
|
+
key_value: keyword COLON expr { result = { key: val[0], value: val[2], negated: false } }
|
52
|
+
| EXCLAMATION keyword COLON expr { result = { key: val[1], value: val[3], negated: true } }
|
53
|
+
|
54
|
+
method_name: METHOD
|
55
|
+
| EXCLAMATION
|
56
|
+
|
57
|
+
method_name_or_ident: method_name
|
58
|
+
| LIDENT
|
59
|
+
| UIDENT
|
60
|
+
|
61
|
+
keyword: LIDENT | UIDENT
|
62
|
+
|
63
|
+
constant: UIDENT { result = [val[0]] }
|
64
|
+
| UIDENT COLONCOLON constant { result = [val[0]] + val[2] }
|
65
|
+
|
66
|
+
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
|
+
name: val[0],
|
70
|
+
args: val[2]) }
|
71
|
+
| expr DOT method_name_or_ident { result = Expr::Send.new(receiver: val[0], name: val[2], args: Argument::AnySeq.new) }
|
72
|
+
| expr DOT method_name_or_ident LPAREN args RPAREN { result = Expr::Send.new(receiver: val[0],
|
73
|
+
name: val[2],
|
74
|
+
args: val[4]) }
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
---- inner
|
79
|
+
|
80
|
+
require "strscan"
|
81
|
+
|
82
|
+
attr_reader :input
|
83
|
+
|
84
|
+
def initialize(input)
|
85
|
+
super()
|
86
|
+
@input = StringScanner.new(input)
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.parse(str)
|
90
|
+
self.new(str).do_parse
|
91
|
+
end
|
92
|
+
|
93
|
+
def next_token
|
94
|
+
input.scan(/\s+/)
|
95
|
+
|
96
|
+
case
|
97
|
+
when input.eos?
|
98
|
+
[false, false]
|
99
|
+
when input.scan(/true/)
|
100
|
+
[:BOOL, true]
|
101
|
+
when input.scan(/false/)
|
102
|
+
[:BOOL, false]
|
103
|
+
when input.scan(/nil/)
|
104
|
+
[:NIL, false]
|
105
|
+
when input.scan(/:string:/)
|
106
|
+
[:STRING, nil]
|
107
|
+
when input.scan(/:dstr:/)
|
108
|
+
[:DSTR, nil]
|
109
|
+
when input.scan(/:int:/)
|
110
|
+
[:INT, nil]
|
111
|
+
when input.scan(/:float:/)
|
112
|
+
[:FLOAT, nil]
|
113
|
+
when input.scan(/:bool:/)
|
114
|
+
[:BOOL, nil]
|
115
|
+
when input.scan(/:symbol:/)
|
116
|
+
[:SYMBOL, nil]
|
117
|
+
when input.scan(/:number:/)
|
118
|
+
[:NUMBER, nil]
|
119
|
+
when input.scan(/:\w+/)
|
120
|
+
s = input.matched
|
121
|
+
[:SYMBOL, s[1, s.size - 1].to_sym]
|
122
|
+
when input.scan(/[+-]?[0-9]+\.[0-9]/)
|
123
|
+
[:FLOAT, input.matched.to_f]
|
124
|
+
when input.scan(/[+-]?[0-9]+/)
|
125
|
+
[:INT, input.matched.to_i]
|
126
|
+
when input.scan(/\_/)
|
127
|
+
[:UNDERBAR, input.matched]
|
128
|
+
when input.scan(/[A-Z]\w*/)
|
129
|
+
[:UIDENT, input.matched.to_sym]
|
130
|
+
when input.scan(/[a-z_](\w)*(\?|\!|=)?/)
|
131
|
+
[:LIDENT, input.matched.to_sym]
|
132
|
+
when input.scan(/\(/)
|
133
|
+
[:LPAREN, input.matched]
|
134
|
+
when input.scan(/\)/)
|
135
|
+
[:RPAREN, input.matched]
|
136
|
+
when input.scan(/\.\.\./)
|
137
|
+
[:DOTDOTDOT, input.matched]
|
138
|
+
when input.scan(/\,/)
|
139
|
+
[:COMMA, input.matched]
|
140
|
+
when input.scan(/\./)
|
141
|
+
[:DOT, input.matched]
|
142
|
+
when input.scan(/\!/)
|
143
|
+
[:EXCLAMATION, input.matched.to_sym]
|
144
|
+
when input.scan(/\[conditional\]/)
|
145
|
+
[:CONDITIONAL_KIND, false]
|
146
|
+
when input.scan(/\[!conditional\]/)
|
147
|
+
[:CONDITIONAL_KIND, true]
|
148
|
+
when input.scan(/\[discarded\]/)
|
149
|
+
[:DISCARDED_KIND, false]
|
150
|
+
when input.scan(/\[!discarded\]/)
|
151
|
+
[:DISCARDED_KIND, true]
|
152
|
+
when input.scan(/\[\]=/)
|
153
|
+
[:METHOD, :"[]="]
|
154
|
+
when input.scan(/\[\]/)
|
155
|
+
[:METHOD, :"[]"]
|
156
|
+
when input.scan(/::/)
|
157
|
+
[:COLONCOLON, input.matched]
|
158
|
+
when input.scan(/:/)
|
159
|
+
[:COLON, input.matched]
|
160
|
+
when input.scan(/\*/)
|
161
|
+
[:STAR, "*"]
|
162
|
+
when input.scan(/@\w+/)
|
163
|
+
[:IVAR, input.matched.to_sym]
|
164
|
+
when input.scan(/@/)
|
165
|
+
[:IVAR, nil]
|
166
|
+
when input.scan(/&/)
|
167
|
+
[:AMP, nil]
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Querly
|
2
|
+
class Preprocessor
|
3
|
+
class Error < StandardError
|
4
|
+
attr_reader :command
|
5
|
+
attr_reader :status
|
6
|
+
|
7
|
+
def initialize(command:, status:)
|
8
|
+
@command = command
|
9
|
+
@status = status
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :ext
|
14
|
+
attr_reader :command
|
15
|
+
|
16
|
+
def initialize(ext:, command:)
|
17
|
+
@ext = ext
|
18
|
+
@command = command
|
19
|
+
end
|
20
|
+
|
21
|
+
def run!(source_code)
|
22
|
+
output, status = Open3.capture2({ 'RUBYOPT' => nil }, command, stdin_data: source_code)
|
23
|
+
raise Error.new(status: status, command: command) unless status.success?
|
24
|
+
output
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/querly/rule.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Querly
|
2
|
+
class Rule
|
3
|
+
attr_reader :id
|
4
|
+
attr_reader :patterns
|
5
|
+
|
6
|
+
attr_reader :messages
|
7
|
+
attr_reader :justifications
|
8
|
+
attr_reader :before_examples
|
9
|
+
attr_reader :after_examples
|
10
|
+
attr_reader :tags
|
11
|
+
attr_reader :scope
|
12
|
+
|
13
|
+
def initialize(id:, scope: :nil)
|
14
|
+
@id = id
|
15
|
+
@scope = scope
|
16
|
+
|
17
|
+
@patterns = []
|
18
|
+
@messages = []
|
19
|
+
@justifications = []
|
20
|
+
@before_examples = []
|
21
|
+
@after_examples = []
|
22
|
+
@tags = Set.new
|
23
|
+
@scope = scope
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Querly
|
2
|
+
class ScriptEnumerator
|
3
|
+
attr_reader :paths
|
4
|
+
attr_reader :preprocessors
|
5
|
+
|
6
|
+
def initialize(paths:, preprocessors: {})
|
7
|
+
@paths = paths
|
8
|
+
@preprocessors = preprocessors
|
9
|
+
end
|
10
|
+
|
11
|
+
def each(&block)
|
12
|
+
if block_given?
|
13
|
+
paths.each do |path|
|
14
|
+
case
|
15
|
+
when path.file?
|
16
|
+
load_script_from_path path, &block
|
17
|
+
when path.directory?
|
18
|
+
enumerate_files_in_dir(path, &block)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
else
|
22
|
+
self.enum_for :each
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
@loaders = []
|
27
|
+
|
28
|
+
def self.register_loader(pattern, loader)
|
29
|
+
@loaders << [pattern, loader]
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.find_loader(path)
|
33
|
+
basename = path.basename.to_s
|
34
|
+
@loaders.find {|pair| pair.first === basename }&.last
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def load_script_from_path(path, &block)
|
40
|
+
preprocessor = preprocessors[path.extname]
|
41
|
+
|
42
|
+
begin
|
43
|
+
source = if preprocessor
|
44
|
+
preprocessor.run!(path.read)
|
45
|
+
else
|
46
|
+
path.read
|
47
|
+
end
|
48
|
+
|
49
|
+
script = Script.new(path: path, node: Parser::CurrentRuby.parse(source, path.to_s))
|
50
|
+
rescue StandardError, LoadError, Preprocessor::Error => exn
|
51
|
+
script = exn
|
52
|
+
end
|
53
|
+
|
54
|
+
yield(path, script)
|
55
|
+
end
|
56
|
+
|
57
|
+
def enumerate_files_in_dir(path, &block)
|
58
|
+
if path.basename.to_s =~ /\A\.[^\.]+/
|
59
|
+
# skip hidden paths
|
60
|
+
return
|
61
|
+
end
|
62
|
+
|
63
|
+
case
|
64
|
+
when path.directory?
|
65
|
+
path.children.each do |child|
|
66
|
+
enumerate_files_in_dir child, &block
|
67
|
+
end
|
68
|
+
when path.file?
|
69
|
+
should_load_file = case
|
70
|
+
when path.extname == ".rb"
|
71
|
+
true
|
72
|
+
when path.extname == ".gemspec"
|
73
|
+
true
|
74
|
+
when path.basename.to_s == "Rakefile"
|
75
|
+
true
|
76
|
+
else
|
77
|
+
preprocessors.key?(path.extname)
|
78
|
+
end
|
79
|
+
|
80
|
+
load_script_from_path(path, &block) if should_load_file
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,32 @@
|
|
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
|
data/querly.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'querly/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "querly"
|
8
|
+
spec.version = Querly::VERSION
|
9
|
+
spec.authors = ["Soutaro Matsumoto"]
|
10
|
+
spec.email = ["matsumoto@soutaro.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Query Method Calls from Ruby Programs}
|
13
|
+
spec.description = %q{Querly is a query language and tool to find out method calls from Ruby programs. You write simple query, and Querly finds out wrong pieces in your program.}
|
14
|
+
spec.homepage = "https://github.com/soutaro/querly"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`
|
17
|
+
.split("\x0")
|
18
|
+
.reject { |f| f.match(%r{^(test|spec|features)/}) }
|
19
|
+
.push('lib/querly/pattern/parser.rb')
|
20
|
+
spec.bindir = "exe"
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.12"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
27
|
+
spec.add_development_dependency "racc", "= 1.4.14"
|
28
|
+
|
29
|
+
spec.add_dependency 'thor', "~> 0.19"
|
30
|
+
spec.add_dependency "parser", "~> 2.3.1"
|
31
|
+
spec.add_dependency "rainbow", "~> 2.1"
|
32
|
+
end
|