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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b38a9eb384827611a39b2d1bef7e1b8cb574f2c8
|
4
|
+
data.tar.gz: c02c0c2ebb9844465855e2691e02715b5c73b359
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e9a5550cec1e87aad9dcfd0372af5a65ec82b46f685c8960c89b7a34316e64ffc2172136a4a238dfa3803349796df235d960ca4ca24df503f8895c4776a9631f
|
7
|
+
data.tar.gz: fa20ac97b8485b262e625e20a945719c254a7a13f4006e0e8513156567324e3b932c6d75e623b16062085026eb9ad8e4ad902adb169f786026588ebd64cfc729
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
# Querly - Query Method Calls from Ruby Programs
|
2
|
+
|
3
|
+
[](https://travis-ci.org/soutaro/querly)
|
4
|
+
|
5
|
+
Querly is a query language and tool to find out method calls from Ruby programs.
|
6
|
+
You write simple query for method calls, and Querly finds out *wrong* pieces from your program.
|
7
|
+
|
8
|
+
## Overview
|
9
|
+
|
10
|
+
Your project should have many local rules:
|
11
|
+
|
12
|
+
* Should not use `Customer#update_mail` but 30x faster `Customer.update_all_email` (Slower `#update_mail` is left just for existing code, but new code should not use it)
|
13
|
+
* Should not use `root_url` without `locale:` parameter
|
14
|
+
* Should not use `Net::HTTP` for Web API calls, but use `HTTPClient`
|
15
|
+
|
16
|
+
These local rule violations will be found during code review.
|
17
|
+
Reviewers will ask commiter to revise, commiter will fix, fine.
|
18
|
+
Really?
|
19
|
+
It is boring and time consuming.
|
20
|
+
We need some automation!
|
21
|
+
|
22
|
+
However, that rules cannot be the standard.
|
23
|
+
They make sense only in your project.
|
24
|
+
Okay, start writing a plug-in for RuboCop? (or other checking tools)
|
25
|
+
|
26
|
+
Instead of writing RuboCop plug-in, just define a Querly rule in a few lines of YAML.
|
27
|
+
|
28
|
+
```yml
|
29
|
+
rules:
|
30
|
+
- id: my_project.use_faster_email_update
|
31
|
+
pattern: update_mail
|
32
|
+
message: When updating Customer#email, newly written code should use 30x faster Customer.update_all_email
|
33
|
+
justification:
|
34
|
+
- When you are editing old code (it should be refactored...)
|
35
|
+
- You are sure updating only small number of customers, and performance does not matter
|
36
|
+
|
37
|
+
- id: my_project.root_url_without_locale
|
38
|
+
pattern: "root_url(!locale: _)"
|
39
|
+
message: Links to top page should be with locale parameter
|
40
|
+
|
41
|
+
- id: my_project.net_http
|
42
|
+
pattern: Net::HTTP
|
43
|
+
message: Use HTTPClient to make HTTP request
|
44
|
+
```
|
45
|
+
|
46
|
+
Write down your local rules, and let Querly do boring checks.
|
47
|
+
Focus on spec, design, UX, and other important things during code review!
|
48
|
+
|
49
|
+
## Installation
|
50
|
+
|
51
|
+
Install via RubyGems.
|
52
|
+
|
53
|
+
$ gem install querly
|
54
|
+
|
55
|
+
Or you can put it in your Gemfile.
|
56
|
+
|
57
|
+
```rb
|
58
|
+
gem 'querly'
|
59
|
+
```
|
60
|
+
|
61
|
+
## Quick Start
|
62
|
+
|
63
|
+
Copy the following YAML and paste as `querly.yml` in your project's repo.
|
64
|
+
|
65
|
+
```yaml
|
66
|
+
rules:
|
67
|
+
- id: sample.debug_print
|
68
|
+
pattern:
|
69
|
+
- p
|
70
|
+
- pp
|
71
|
+
message: Delete debug print
|
72
|
+
```
|
73
|
+
|
74
|
+
Run `querly` in the repo.
|
75
|
+
|
76
|
+
```
|
77
|
+
$ querly check .
|
78
|
+
```
|
79
|
+
|
80
|
+
If your code contains `p` or `pp` calls, querly will print warning messages.
|
81
|
+
|
82
|
+
```
|
83
|
+
./app/models/account.rb:44:10 p(account.id) Delete debug print
|
84
|
+
./app/controllers/accounts_controller.rb:17:2 pp params: params Delete debug print
|
85
|
+
```
|
86
|
+
|
87
|
+
## Configuration
|
88
|
+
|
89
|
+
Visit our wiki pages for configuration and query language reference.
|
90
|
+
|
91
|
+
* [Configuration](https://github.com/soutaro/querly/wiki/Configuration)
|
92
|
+
* [Patterns](https://github.com/soutaro/querly/wiki/Patterns)
|
93
|
+
|
94
|
+
Use `querly console` command to test patterns interactively.
|
95
|
+
|
96
|
+
## Notes
|
97
|
+
|
98
|
+
### Querly's analysis is syntactic
|
99
|
+
|
100
|
+
The analysis is currently purely syntactic:
|
101
|
+
|
102
|
+
```rb
|
103
|
+
record.save(validate: false)
|
104
|
+
```
|
105
|
+
|
106
|
+
and
|
107
|
+
|
108
|
+
```rb
|
109
|
+
x = false
|
110
|
+
record.save(validate: x)
|
111
|
+
```
|
112
|
+
|
113
|
+
will yield different results.
|
114
|
+
This can be improved by doing very primitive data flow analysis, and I'm planning to do that.
|
115
|
+
|
116
|
+
### Too many false positives!
|
117
|
+
|
118
|
+
The analysis itself does not have very good precision.
|
119
|
+
There will be many false positives, and *querly warning free code* does not make much sense.
|
120
|
+
|
121
|
+
* TODO: support to ignore warnings through magic comments in code
|
122
|
+
|
123
|
+
This is not to ensure *there is nothing wrong in the code*, but just tells you *code fragments you should review with special care*.
|
124
|
+
I believe it still improves your software development productivity.
|
125
|
+
|
126
|
+
### Incoming updates?
|
127
|
+
|
128
|
+
The following is the list of updates which would make sense.
|
129
|
+
|
130
|
+
* Support for importing rule sets, and provide some good default rules
|
131
|
+
* Support for ignoring warnings
|
132
|
+
* Improve analysis precision by intra procedural data flow analysis
|
133
|
+
|
134
|
+
## Development
|
135
|
+
|
136
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
137
|
+
|
138
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
139
|
+
|
140
|
+
## Contributing
|
141
|
+
|
142
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/soutaro/querly.
|
143
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
|
+
|
4
|
+
Rake::TestTask.new(:test) do |t|
|
5
|
+
t.libs << "test"
|
6
|
+
t.libs << "lib"
|
7
|
+
t.test_files = FileList['test/**/*_test.rb']
|
8
|
+
end
|
9
|
+
|
10
|
+
task :default => :test
|
11
|
+
task :build => :racc
|
12
|
+
task :test => :racc
|
13
|
+
|
14
|
+
rule /\.rb/ => ".y" do |t|
|
15
|
+
sh "racc", "-v", "-o", "#{t.name}", "#{t.source}"
|
16
|
+
end
|
17
|
+
|
18
|
+
task :racc => "lib/querly/pattern/parser.rb"
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "querly"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/exe/querly
ADDED
data/lib/querly.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require "yaml"
|
3
|
+
require "rainbow"
|
4
|
+
require "parser/current"
|
5
|
+
require "set"
|
6
|
+
require "open3"
|
7
|
+
|
8
|
+
Parser::Builders::Default.emit_lambda = true
|
9
|
+
|
10
|
+
require "querly/version"
|
11
|
+
require 'querly/analyzer'
|
12
|
+
require 'querly/rule'
|
13
|
+
require 'querly/pattern/expr'
|
14
|
+
require 'querly/pattern/argument'
|
15
|
+
require 'querly/script'
|
16
|
+
require 'querly/script_enumerator'
|
17
|
+
require 'querly/node_pair'
|
18
|
+
require "querly/pattern/parser"
|
19
|
+
require 'querly/pattern/kind'
|
20
|
+
require "querly/config"
|
21
|
+
require "querly/tagging"
|
22
|
+
require "querly/preprocessor"
|
23
|
+
|
24
|
+
module Querly
|
25
|
+
# Your code goes here...
|
26
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Querly
|
2
|
+
class Analyzer
|
3
|
+
attr_reader :rules
|
4
|
+
attr_reader :scripts
|
5
|
+
attr_reader :taggings
|
6
|
+
|
7
|
+
def initialize(taggings:)
|
8
|
+
@rules = []
|
9
|
+
@scripts = []
|
10
|
+
@taggings = taggings
|
11
|
+
end
|
12
|
+
|
13
|
+
#
|
14
|
+
# yields(script, rule, node_pair)
|
15
|
+
#
|
16
|
+
def run
|
17
|
+
scripts.each do |script|
|
18
|
+
tagging = taggings.find {|tagging| tagging.applicable?(script) }
|
19
|
+
|
20
|
+
each_subnode script.root_pair do |node_pair|
|
21
|
+
rules.each do |rule|
|
22
|
+
if applicable_rule?(tagging, rule)
|
23
|
+
if rule.patterns.any? {|pattern| test_pair(node_pair, pattern) }
|
24
|
+
yield script, rule, node_pair
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def find(pattern)
|
33
|
+
scripts.each do |script|
|
34
|
+
each_subnode script.root_pair do |node_pair|
|
35
|
+
if test_pair(node_pair, pattern)
|
36
|
+
yield script, node_pair
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_pair(node_pair, pattern)
|
43
|
+
pattern.expr =~ node_pair && pattern.test_kind(node_pair)
|
44
|
+
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
|
+
end
|
64
|
+
end
|
data/lib/querly/cli.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require "thor"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Querly
|
5
|
+
class CLI < Thor
|
6
|
+
desc "check [paths]", "Check paths based on configuration"
|
7
|
+
option :config, default: "querly.yml"
|
8
|
+
option :format, default: "text", type: :string, enum: %w(text json)
|
9
|
+
def check(*paths)
|
10
|
+
require 'querly/cli/formatter'
|
11
|
+
|
12
|
+
formatter = case options[:format]
|
13
|
+
when "text"
|
14
|
+
Formatter::Text.new
|
15
|
+
when "json"
|
16
|
+
Formatter::JSON.new
|
17
|
+
end
|
18
|
+
formatter.start
|
19
|
+
|
20
|
+
begin
|
21
|
+
unless config_path.file?
|
22
|
+
STDERR.puts <<-Message
|
23
|
+
Configuration file #{config_path} does not look a file.
|
24
|
+
Specify configuration file by --config option.
|
25
|
+
Message
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
|
29
|
+
config = Config.new
|
30
|
+
begin
|
31
|
+
config.add_file config_path
|
32
|
+
rescue => exn
|
33
|
+
formatter.config_error config_path, exn
|
34
|
+
exit
|
35
|
+
end
|
36
|
+
|
37
|
+
analyzer = Analyzer.new(taggings: config.taggings)
|
38
|
+
analyzer.rules.concat config.rules
|
39
|
+
|
40
|
+
ScriptEnumerator.new(paths: paths.map {|path| Pathname(path) }, preprocessors: config.preprocessors).each do |path, script|
|
41
|
+
case script
|
42
|
+
when Script
|
43
|
+
analyzer.scripts << script
|
44
|
+
formatter.script_load script
|
45
|
+
when StandardError, LoadError
|
46
|
+
formatter.script_error path, script
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
analyzer.run do |script, rule, pair|
|
51
|
+
formatter.issue_found script, rule, pair
|
52
|
+
end
|
53
|
+
rescue => exn
|
54
|
+
formatter.fatal_error exn
|
55
|
+
exit 1
|
56
|
+
ensure
|
57
|
+
formatter.finish
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
desc "console [paths]", "Start console for given paths"
|
62
|
+
def console(*paths)
|
63
|
+
require 'querly/cli/console'
|
64
|
+
Console.new(paths: paths.map {|path| Pathname(path) }).start
|
65
|
+
end
|
66
|
+
|
67
|
+
desc "test", "Check configuration"
|
68
|
+
option :config, default: "querly.yml"
|
69
|
+
def test()
|
70
|
+
require "querly/cli/test"
|
71
|
+
Test.new(config_path: config_path).run
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def config_path
|
77
|
+
[Pathname(options[:config]),
|
78
|
+
Pathname("querly.yaml")].compact.find(&:file?) || Pathname(options[:config])
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'readline'
|
2
|
+
|
3
|
+
module Querly
|
4
|
+
class CLI
|
5
|
+
class Console
|
6
|
+
attr_reader :paths
|
7
|
+
|
8
|
+
def initialize(paths:)
|
9
|
+
@paths = paths
|
10
|
+
end
|
11
|
+
|
12
|
+
def start
|
13
|
+
puts <<-Message
|
14
|
+
Querly #{VERSION}, interactive console
|
15
|
+
|
16
|
+
Message
|
17
|
+
|
18
|
+
puts_commands
|
19
|
+
|
20
|
+
STDOUT.print "Loading..."
|
21
|
+
STDOUT.flush
|
22
|
+
reload!
|
23
|
+
STDOUT.puts " ready!"
|
24
|
+
|
25
|
+
loop
|
26
|
+
end
|
27
|
+
|
28
|
+
def reload!
|
29
|
+
@analyzer = nil
|
30
|
+
analyzer
|
31
|
+
end
|
32
|
+
|
33
|
+
def analyzer
|
34
|
+
return @analyzer if @analyzer
|
35
|
+
|
36
|
+
@analyzer = Analyzer.new(taggings: [])
|
37
|
+
|
38
|
+
ScriptEnumerator.new(paths: paths).each do |path, script|
|
39
|
+
case script
|
40
|
+
when Script
|
41
|
+
@analyzer.scripts << script
|
42
|
+
when StandardError
|
43
|
+
p script
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
@analyzer
|
48
|
+
end
|
49
|
+
|
50
|
+
def loop
|
51
|
+
while line = Readline.readline("> ", true)
|
52
|
+
case line
|
53
|
+
when "quit"
|
54
|
+
exit
|
55
|
+
when "reload!"
|
56
|
+
STDOUT.print "reloading..."
|
57
|
+
STDOUT.flush
|
58
|
+
reload!
|
59
|
+
STDOUT.puts " done"
|
60
|
+
when /^find (.+)/
|
61
|
+
begin
|
62
|
+
pattern = Pattern::Parser.parse($1)
|
63
|
+
|
64
|
+
count = 0
|
65
|
+
|
66
|
+
analyzer.find(pattern) do |script, pair|
|
67
|
+
path = script.path.to_s
|
68
|
+
line = pair.node.loc.first_line
|
69
|
+
|
70
|
+
while true
|
71
|
+
parent = pair.parent
|
72
|
+
|
73
|
+
if parent && parent.node.loc.first_line == line
|
74
|
+
pair = pair.parent
|
75
|
+
else
|
76
|
+
break
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
src = Rainbow(pair.node.loc.expression.source.split(/\n/).first).blue
|
81
|
+
col = pair.node.loc.column
|
82
|
+
|
83
|
+
puts " #{path}:#{line}:#{col}\t#{src}"
|
84
|
+
|
85
|
+
count += 1
|
86
|
+
end
|
87
|
+
|
88
|
+
puts "#{count} results"
|
89
|
+
rescue => exn
|
90
|
+
STDOUT.puts exn.inspect
|
91
|
+
STDOUT.puts exn.backtrace
|
92
|
+
end
|
93
|
+
else
|
94
|
+
puts_commands
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def puts_commands
|
100
|
+
puts <<-Message
|
101
|
+
Commands:
|
102
|
+
- find PATTERN Find PATTERN from given paths
|
103
|
+
- reload! Reload program from paths
|
104
|
+
- quit
|
105
|
+
|
106
|
+
Message
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|