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
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
|
+
[![Build Status](https://travis-ci.org/soutaro/querly.svg?branch=master)](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
|