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 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
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /.idea
10
+ /parser.output
11
+ /lib/querly/pattern/parser.rb
12
+ parser.output
13
+ /Gemfile.lock
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.12.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in querly.gemspec
4
+ gemspec
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/querly ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.join(__dir__, "../lib")
4
+
5
+ require "querly"
6
+ require "querly/cli"
7
+
8
+ Querly::CLI.start(ARGV)
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