querly 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
@@ -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,15 @@
1
+ module Querly
2
+ class Script
3
+ attr_reader :path
4
+ attr_reader :node
5
+
6
+ def initialize(path:, node:)
7
+ @path = path
8
+ @node = node
9
+ end
10
+
11
+ def root_pair
12
+ NodePair.new(node: node)
13
+ end
14
+ end
15
+ 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
@@ -0,0 +1,3 @@
1
+ module Querly
2
+ VERSION = "0.1.0"
3
+ 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