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