querly 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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