Repository: soutaro/querly
Branch: master
Commit: 9a44873ed766
Files: 66
Total size: 143.3 KB
Directory structure:
gitextract_rkriiky5/
├── .github/
│ └── workflows/
│ ├── rubocop.yml
│ └── ruby.yml
├── .gitignore
├── .rubocop.yml
├── CHANGELOG.md
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── bin/
│ ├── console
│ └── setup
├── exe/
│ ├── querly
│ └── querly-pp
├── lib/
│ ├── querly/
│ │ ├── analyzer.rb
│ │ ├── check.rb
│ │ ├── cli/
│ │ │ ├── console.rb
│ │ │ ├── find.rb
│ │ │ ├── formatter.rb
│ │ │ ├── rules.rb
│ │ │ └── test.rb
│ │ ├── cli.rb
│ │ ├── concerns/
│ │ │ └── backtrace_formatter.rb
│ │ ├── config.rb
│ │ ├── node_pair.rb
│ │ ├── pattern/
│ │ │ ├── argument.rb
│ │ │ ├── expr.rb
│ │ │ ├── kind.rb
│ │ │ └── parser.y
│ │ ├── pp/
│ │ │ └── cli.rb
│ │ ├── preprocessor.rb
│ │ ├── rule.rb
│ │ ├── rules/
│ │ │ └── sample.rb
│ │ ├── script.rb
│ │ ├── script_enumerator.rb
│ │ └── version.rb
│ └── querly.rb
├── manual/
│ ├── configuration.md
│ ├── examples.md
│ └── patterns.md
├── querly.gemspec
├── rules/
│ └── sample.yml
├── sample.yaml
├── template.yml
└── test/
├── analyzer_test.rb
├── check_test.rb
├── cli/
│ ├── console_test.rb
│ ├── rules_test.rb
│ └── test_test.rb
├── config_test.rb
├── data/
│ ├── test1/
│ │ ├── querly.yml
│ │ └── script.rb
│ ├── test2/
│ │ ├── querly.yml
│ │ └── script.rb
│ ├── test3/
│ │ ├── querly.yml
│ │ └── script.rb
│ └── test4/
│ ├── querly.yml
│ └── script.rb
├── node_pair_test.rb
├── pattern_parser_test.rb
├── pattern_test_test.rb
├── preprocessor_test.rb
├── querly_test.rb
├── rule_test.rb
├── script_enumerator_test.rb
├── smoke_test.rb
└── test_helper.rb
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/rubocop.yml
================================================
name: RuboCop
on: pull_request
jobs:
rubocop:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.0"
- run: gem install rubocop rubocop-rubycw
- name: Run RuboCop
run: rubocop --format github
================================================
FILE: .github/workflows/ruby.yml
================================================
name: Ruby
on:
push:
branches:
- master
pull_request: {}
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby: ["2.7", "3.0", head]
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- run: bin/setup
- run: bundle exec rake build test
================================================
FILE: .gitignore
================================================
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
/.idea
/parser.output
/lib/querly/pattern/parser.rb
parser.output
/Gemfile.lock
.querly_history
================================================
FILE: .rubocop.yml
================================================
require:
- rubocop-rubycw
AllCops:
DisabledByDefault: true
Exclude:
- test/data/**/*.rb
Rubycw/Rubycw:
Enabled: true
================================================
FILE: CHANGELOG.md
================================================
# Change Log
## master
## 1.3.0 (2021-07-05)
* Require Ruby 2.7 or 3.0 by @yubiquitous ([#88](https://github.com/soutaro/querly/pull/88))
* Use 3.0 compatible parser by @yubiquitous ([#88](https://github.com/soutaro/querly/pull/88))
## 1.2.0 (2020-12-15)
* Relax Thor version requirements by @y-yagi ([#85](https://github.com/soutaro/querly/pull/85))
* Fix ERB comment preprocessing by @mallowlabs ([#84](https://github.com/soutaro/querly/pull/84))
* Better error message for Ruby code syntax error by @ybiquitous ([#83](https://github.com/soutaro/querly/pull/83))
## 1.1.0 (2020-05-17)
* Fix invalid bytes sequence in UTF-8 error by @mallowlabs [#75](https://github.com/soutaro/querly/pull/75)
* Detect safe navigation operator as a method call by @pocke [#71](https://github.com/soutaro/querly/pull/71)
## 1.0.0 (2019-7-19)
* Add `--config` option for `find` and `console` [#67](https://github.com/soutaro/querly/pull/67)
* Improve preprocessor performance by processing concurrently [#68](https://github.com/soutaro/querly/pull/68)
## 0.16.0 (2019-04-23)
* Support string literal pattern (@pocke) [#64](https://github.com/soutaro/querly/pull/64)
* Allow underscore method name pattern (@pocke) [#63](https://github.com/soutaro/querly/pull/63)
* Add erb support (@hanachin) [#61](https://github.com/soutaro/querly/pull/61)
* Add `exit` command on console (@wata727) [#59](https://github.com/soutaro/querly/pull/59)
## 0.15.1 (2019-03-12)
* Relax parser version requirement
## 0.15.0 (2019-02-13)
* Fix broken `querly init` template (@ybiquitous) #56
* Relax `activesupport` requirement (@y-yagi) #57
## 0.14.0 (2019-01-22)
* Allow having `...` pattens anywhere positional argument patterns are valid #54
* Add `querly find` command (@gfx) #49
## 0.13.0 (2018-08-27)
* Make history file location configurable through `QUERLY_HOME` (defaults to `~/.querly`)
* Save `console` history (@gfx) #47
## 0.12.0 (2018-08-03)
* Declare MIT license #44
* Make reading backtrace easier in `console` command (@pocke) #43
* Highlight matched expression in querly console (@pocke) #42
* Set exit status = 1 when `querly.yaml` has syntax error (@pocke) #41
* Fix typos (@koic, @vzvu3k6k) #40, #39
## 0.11.0 (2018-04-22)
* Relax `rainbow` version requirement
## 0.10.0 (2018-04-13)
* Update parser (@yoshoku) #38
* Use Ruby25 parser
## 0.9.0 (2018-03-02)
* Fix literal testing (@pocke) #37
## 0.8.4 (2018-02-11)
* Loosen the restriction of `thor` version (@shinnn) #36
## 0.8.3 (2018-01-16)
* Fix preprocessor to avoid deadlocking #35
## 0.8.2 (2018-01-13)
* Move `Concerns::BacktraceFormatter` under `Querly` (@kohtaro24) #34
## 0.8.1 (2017-12-22)
* Update dependencies
## 0.8.0 (2017-12-19)
* Make `[conditional]` be aware of safe-navigation-operator (@pocke) #30
* Make preprocessors be aware of `bundle exec`.
When `querly` is invoked with `bundle exec`, so are preprocessors, and vice vesa.
* Add `--rule` option for `querly check` to filter rules to test
* Print rule id in text output
## 0.7.0 (2017-08-22)
* Add Wiki pages to repository in manual directory #25
* Add named literal pattern `:string: as 'name` with `where: { name: ["alice", /bob/] }` #24
* Add `init` command #28
## 0.6.0 (2017-06-27)
* Load current directory when no path is given (@wata727) #18
* Require Active Support ~> 5.0 (@gfx) #17
* Print error message if HAML 5.0 is loaded (@pocke) #16
## 0.5.0 (2017-06-16)
* Exit 1 on test failure #9
* Fix example index printing in test (@pocke) #8, #10
* Introduce pattern matching on method name by set of string and regexp
* Rule definitions in config can have more structured `examples` attribute
## 0.4.0 (2017-05-25)
* Update `parser` to 2.4 compatible version
* Check more pathnames which looks like Ruby by default (@pocke) #7
## 0.3.1 (2017-02-16)
* Allow `require` rules from config file
* Add `version` command
* Fix *with block* and *without block* pattern parsing
* Prettier backtrace printing
* Prettier pattern syntax error message
## 0.2.1 (2016-11-24)
* Fix `self` pattern matching
## 0.2.0 (2016-11-24)
* Remove `tagging` section from config
* Add `check` section to select rules to check
* Add `import` section to load rules from other file
* Add `querly rules` sub command to print loaded rules
* Add *with block* and *without block* pattern (`foo() {}` / `foo() !{}`)
* Add *some of receiver chain* pattern (`...`)
* Fix keyword args pattern matching bug
## 0.1.0
* First release.
================================================
FILE: Gemfile
================================================
source 'https://rubygems.org'
# Specify your gem's dependencies in querly.gemspec
gemspec
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2017 Soutaro Matsumoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================

# Querly - Pattern Based Checking Tool for Ruby

Querly is a query language and tool to find out method calls from Ruby programs.
Define rules to check your program with patterns to find out *bad* pieces.
Querly finds out matching pieces from your program.
## Overview
Your project may have many local rules:
* Should not use `Customer#update_mail` and use 30x faster `Customer.update_all_email` instead (Slower `#update_mail` is left just for existing code, but new code should not use it)
* Should not use `root_url` without `locale:` parameter
* Should not use `Net::HTTP` for Web API calls, but use `HTTPClient`
These local rule violations will be found during code review.
Reviewers will ask commiter to revise; commiter will fix; fine.
Really?
It is boring and time-consuming.
We need some automation!
However, that rules cannot be the standard.
They make sense only in your project.
Okay, start writing a plug-in for RuboCop? (or other checking tools)
Instead of writing RuboCop plug-in, just define a Querly rule in a few lines of YAML.
```yml
rules:
- id: my_project.use_faster_email_update
pattern: update_mail
message: When updating Customer#email, newly written code should use 30x faster Customer.update_all_email
justification:
- When you are editing old code (it should be refactored...)
- You are sure updating only small number of customers, and performance does not matter
- id: my_project.root_url_without_locale
pattern: "root_url(!locale: _)"
message: Links to top page should be with locale parameter
- id: my_project.net_http
pattern: Net::HTTP
message: Use HTTPClient to make HTTP request
```
Write down your local rules, and let Querly check conformance with them.
Focus on spec, design, UX, and other important things during code review!
## Installation
Install via RubyGems.
$ gem install querly
Or you can put it in your Gemfile.
```rb
gem 'querly'
```
## Quick Start
Copy the following YAML and paste as `querly.yml` in your project's repo.
```yaml
rules:
- id: sample.debug_print
pattern:
- self.p
- self.pp
message: Delete debug print
```
Run `querly` in the repo.
```
$ querly check .
```
If your code contains `p` or `pp` calls, querly will print warning messages.
```
./app/models/account.rb:44:10 p(account.id) Delete debug print
./app/controllers/accounts_controller.rb:17:2 pp params: params Delete debug print
```
## Configuration
See the following manual for configuration and query language reference.
* [Configuration](https://github.com/soutaro/querly/blob/master/manual/configuration.md)
* [Patterns](https://github.com/soutaro/querly/blob/master/manual/patterns.md)
Use `querly console` command to test patterns interactively.
## Requiring Rules
`import` section in config file now allows accepts `require` command.
```yaml
import:
- require: querly/rules/sample
- require: your_library/querly/rules
```
Querly ships with `querly/rules/sample` rule set. Check `lib/querly/rules/sample.rb` and `rules/sample.yml` for detail.
### Publishing Gems with Querly Rules
Querly provides `Querly.load_rule` API to allow publishing your rules as part of Ruby library.
Put rules YAML file in your gem, and add Ruby script in some directory like `lib/your_library/querly/rules.rb`.
```
Querly.load_rules File.join(__dir__, relative_path_to_yaml_file)
```
## Notes
### Querly's analysis is syntactic
The analysis is currently purely syntactic:
```rb
record.save(validate: false)
```
and
```rb
x = false
record.save(validate: x)
```
will yield different results.
This can be improved by doing very primitive data flow analysis, and I'm planning to do that.
### Too many false positives!
The analysis itself does not have very good precision.
There will be many false positives, and *querly warning free code* does not make much sense.
* TODO: support to ignore warnings through magic comments in code
Querly is not to ensure *there is nothing wrong in the code*, but just tells you *code fragments you should review with special care*.
I believe it still improves your software development productivity.
### Incoming updates?
The following is the list of updates which would make sense.
* Support for importing rule sets, and provide some good default rules
* Support for ignoring warnings
* Improve analysis precision by intra procedural data flow analysis
## Development
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.
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).
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/soutaro/querly.
================================================
FILE: Rakefile
================================================
require "bundler/gem_tasks"
require "rake/testtask"
Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList['test/**/*_test.rb']
end
task :default => :test
task :build => :racc
task :test => :racc
rule %r/\.rb/ => ".y" do |t|
sh "racc", "-v", "-o", "#{t.name}", "#{t.source}"
end
task :racc => "lib/querly/pattern/parser.rb"
================================================
FILE: bin/console
================================================
#!/usr/bin/env ruby
require "bundler/setup"
require "querly"
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start
require "irb"
IRB.start
================================================
FILE: bin/setup
================================================
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx
bundle install
bundle exec rake racc
================================================
FILE: exe/querly
================================================
#!/usr/bin/env ruby
$LOAD_PATH << File.join(__dir__, "../lib")
require "querly"
require "querly/cli"
Querly::CLI.start(ARGV)
================================================
FILE: exe/querly-pp
================================================
#!/usr/bin/env ruby
$LOAD_PATH << File.join(__dir__, "../lib")
require "querly/pp/cli"
Querly::PP::CLI.new(ARGV).run
================================================
FILE: lib/querly/analyzer.rb
================================================
module Querly
class Analyzer
attr_reader :config
attr_reader :scripts
attr_reader :rule
def initialize(config:, rule:)
@config = config
@scripts = []
@rule = rule
end
#
# yields(script, rule, node_pair)
#
def run
scripts.each do |script|
rules = config.rules_for_path(script.path)
script.root_pair.each_subpair do |node_pair|
rules.each do |rule|
if rule.match?(identifier: self.rule)
if rule.patterns.any? {|pattern| test_pair(node_pair, pattern) }
yield script, rule, node_pair
end
end
end
end
end
end
def find(pattern)
scripts.each do |script|
script.root_pair.each_subpair do |node_pair|
if test_pair(node_pair, pattern)
yield script, node_pair
end
end
end
end
def test_pair(node_pair, pattern)
pattern.expr =~ node_pair && pattern.test_kind(node_pair)
end
end
end
================================================
FILE: lib/querly/check.rb
================================================
module Querly
class Check
Query = Struct.new(:opr, :tags, :identifier) do
def apply(current:, all:)
case opr
when :append
current.union(all.select {|rule| match?(rule) })
when :except
current.reject {|rule| match?(rule) }.to_set
when :only
all.select {|rule| match?(rule) }.to_set
end
end
def match?(rule)
rule.match?(identifier: identifier, tags: tags)
end
end
attr_reader :patterns
attr_reader :rules
def initialize(pattern:, rules:)
@rules = rules
@has_trailing_slash = pattern.end_with?("/")
@has_middle_slash = /\/./ =~ pattern
@patterns = []
pattern.sub!(/\A\//, '')
case
when has_trailing_slash? && has_middle_slash?
patterns << File.join(pattern, "**")
when has_trailing_slash?
patterns << File.join(pattern, "**")
patterns << File.join("**", pattern, "**")
when has_middle_slash?
patterns << pattern
patterns << File.join(pattern, "**")
else
patterns << pattern
patterns << File.join("**", pattern)
patterns << File.join(pattern, "**")
patterns << File.join("**", pattern, "**")
end
end
def has_trailing_slash?
@has_trailing_slash
end
def has_middle_slash?
@has_middle_slash
end
def self.load(hash)
pattern = hash["path"]
rules = Array(hash["rules"]).map do |rule|
case rule
when String
parse_rule_query(:append, rule)
when Hash
case
when rule["append"]
parse_rule_query(:append, rule["append"])
when rule["except"]
parse_rule_query(:except, rule["except"])
when rule["only"]
parse_rule_query(:only, rule["only"])
else
parse_rule_query(:append, rule)
end
end
end
self.new(pattern: pattern, rules: rules)
end
def self.parse_rule_query(opr, query)
case query
when String
Query.new(opr, nil, query)
when Hash
if query['tags']
ts = query['tags']
if ts.is_a?(String)
ts = ts.split
end
tags = Set.new(ts)
end
identifier = query['id']
Query.new(opr, tags, identifier)
end
end
def match?(path:)
patterns.any? {|pat| File.fnmatch?(pat, path.to_s) }
end
end
end
================================================
FILE: lib/querly/cli/console.rb
================================================
require 'readline'
module Querly
class CLI
class Console
include Concerns::BacktraceFormatter
attr_reader :paths
attr_reader :history_path
attr_reader :history_size
attr_reader :config
attr_reader :history
attr_reader :threads
def initialize(paths:, history_path:, history_size:, config: nil, threads:)
@paths = paths
@history_path = history_path
@history_size = history_size
@config = config
@history = []
@threads = threads
end
def start
puts <<-Message
Querly #{VERSION}, interactive console
Message
puts_commands
STDOUT.print "Loading..."
STDOUT.flush
reload!
STDOUT.puts " ready!"
load_history
start_loop
end
def reload!
@analyzer = nil
analyzer
end
def analyzer
return @analyzer if @analyzer
@analyzer = Analyzer.new(config: config, rule: nil)
ScriptEnumerator.new(paths: paths, config: config, threads: threads).each do |path, script|
case script
when Script
@analyzer.scripts << script
when StandardError
p path: path, script: script.inspect
puts script.backtrace
end
end
@analyzer
end
def start_loop
while line = Readline.readline("> ", true)
case line
when "quit", "exit"
exit
when "reload!"
STDOUT.print "reloading..."
STDOUT.flush
reload!
STDOUT.puts " done"
when /^find (.+)/
begin
pattern = Pattern::Parser.parse($1, where: {})
count = 0
analyzer.find(pattern) do |script, pair|
path = script.path.to_s
line_no = pair.node.loc.first_line
range = pair.node.loc.expression
start_col = range.column
end_col = range.last_column
src = range.source_buffer.source_lines[line_no-1]
src = Rainbow(src[0...start_col]).blue +
Rainbow(src[start_col...end_col]).bright.blue.bold +
Rainbow(src[end_col..-1]).blue
puts " #{path}:#{line_no}:#{start_col}\t#{src}"
count += 1
end
puts "#{count} results"
save_history line
rescue => exn
STDOUT.puts Rainbow("Error: #{exn}").red
STDOUT.puts "Backtrace:"
STDOUT.puts format_backtrace(exn.backtrace)
end
else
puts_commands
end
end
end
def load_history
history_path.readlines.each do |line|
line.chomp!
Readline::HISTORY.push(line)
history.push line
end
rescue Errno::ENOENT
# in the first time
end
def save_history(line)
history.push line
if history.size > history_size
@history = history.drop(history.size - history_size)
end
history_path.write(history.join("\n") + "\n")
end
def puts_commands
puts <<-Message
Commands:
- find PATTERN Find PATTERN from given paths
- reload! Reload program from paths
- quit
Message
end
end
end
end
================================================
FILE: lib/querly/cli/find.rb
================================================
# frozen_string_literal: true
module Querly
class CLI
class Find
include Concerns::BacktraceFormatter
attr_reader :pattern_str
attr_reader :paths
attr_reader :config
attr_reader :threads
def initialize(pattern:, paths:, config: nil, threads:)
@pattern_str = pattern
@paths = paths
@config = config
@threads = threads
end
def start
count = 0
analyzer.find(pattern) do |script, pair|
path = script.path.to_s
line_no = pair.node.loc.first_line
range = pair.node.loc.expression
start_col = range.column
end_col = range.last_column
src = range.source_buffer.source_lines[line_no-1]
src = Rainbow(src[0...start_col]).blue +
Rainbow(src[start_col...end_col]).bright.blue.bold +
Rainbow(src[end_col..-1]).blue
puts " #{path}:#{line_no}:#{start_col}\t#{src}"
count += 1
end
puts "#{count} results"
rescue => exn
STDOUT.puts Rainbow("Error: #{exn}").red
STDOUT.puts "pattern: #{pattern_str}"
STDOUT.puts "Backtrace:"
STDOUT.puts format_backtrace(exn.backtrace)
end
def pattern
Pattern::Parser.parse(pattern_str, where: {})
end
def analyzer
return @analyzer if @analyzer
@analyzer = Analyzer.new(config: config, rule: nil)
ScriptEnumerator.new(paths: paths, config: config, threads: threads).each do |path, script|
case script
when Script
@analyzer.scripts << script
when StandardError
p path: path, script: script.inspect
puts script.backtrace
end
end
@analyzer
end
end
end
end
================================================
FILE: lib/querly/cli/formatter.rb
================================================
module Querly
class CLI
module Formatter
class Base
include Concerns::BacktraceFormatter
# Called when analyzer started
def start; end
# Called when config is successfully loaded
def config_load(config); end
# Called when failed to load config
# Exit(status == 0) after the call
def config_error(path, error); end
# Called when script is successfully loaded
def script_load(script); end
# Called when failed to load script
# Continue after the call
def script_error(path, error); end
# Called when issue is found
def issue_found(script, rule, pair); end
# Called on other error
# Abort(status != 0) after the call
def fatal_error(error)
STDERR.puts Rainbow("Fatal error: #{error}").red
STDERR.puts "Backtrace:"
STDERR.puts format_backtrace(error.backtrace)
end
# Called on exit/abort
def finish; end
end
class Text < Base
def config_error(path, error)
STDERR.puts Rainbow("Failed to load configuration: #{path}").red
STDERR.puts error
STDERR.puts "Backtrace:"
STDERR.puts format_backtrace(error.backtrace)
end
def script_error(path, error)
STDERR.puts Rainbow("Failed to load script: #{path}").red
if error.is_a? Parser::SyntaxError
STDERR.puts error.diagnostic.render
else
STDERR.puts error.inspect
end
end
def issue_found(script, rule, pair)
path = script.path.to_s
src = Rainbow(pair.node.loc.expression.source.split(/\n/).first).red
line = pair.node.loc.first_line
col = pair.node.loc.column
message = rule.messages.first.split(/\n/).first
STDOUT.puts "#{path}:#{line}:#{col}\t#{src}\t#{message} (#{rule.id})"
end
end
class JSON < Base
def initialize
@issues = []
@script_errors = []
@config_errors = []
@fatal = nil
end
def config_error(path, error)
@config_errors << [path, error]
end
def script_error(path, error)
@script_errors << [path, error]
end
def issue_found(script, rule, pair)
@issues << [script, rule, pair]
end
def finish
STDOUT.print as_json.to_json
end
def fatal_error(error)
super
@fatal = error
end
def as_json
case
when @fatal
# Fatal error found
{
fatal_error: {
message: @fatal.inspect,
backtrace: @fatal.backtrace
}
}
when !@config_errors.empty?
# Error found during config load
{
config_errors: @config_errors.map {|(path, error)|
{
path: path.to_s,
error: {
message: error.inspect,
backtrace: error.backtrace
}
}
}
}
else
# Successfully checked
{
issues: @issues.map {|(script, rule, pair)|
{
script: script.path.to_s,
rule: {
id: rule.id,
messages: rule.messages,
justifications: rule.justifications,
examples: rule.examples.map {|example|
{
before: example.before,
after: example.after
}
}
},
location: {
start: [pair.node.loc.first_line, pair.node.loc.column],
end: [pair.node.loc.last_line, pair.node.loc.last_column]
}
}
},
errors: @script_errors.map {|path, error|
{
path: path.to_s,
error: {
message: error.inspect,
backtrace: error.backtrace
}
}
}
}
end
end
end
end
end
end
================================================
FILE: lib/querly/cli/rules.rb
================================================
module Querly
class CLI
class Rules
attr_reader :config_path
attr_reader :stdout
attr_reader :ids
def initialize(config_path:, ids:, stdout: STDOUT)
@config_path = config_path
@stdout = stdout
@ids = ids
end
def config
yaml = YAML.load(config_path.read)
@config ||= Config.load(yaml, config_path: config_path, root_dir: config_path.parent.realpath)
end
def run
rules = config.rules.select {|rule| test_rule(rule) }
stdout.puts YAML.dump(rules.map {|rule| rule_to_yaml(rule) })
end
def test_rule(rule)
if ids.empty?
true
else
ids.any? {|id| rule.match?(identifier: id) }
end
end
def rule_to_yaml(rule)
{ "id" => rule.id }.tap do |hash|
singleton rule.sources do |a|
hash["pattern"] = a
end
singleton rule.messages do |a|
hash["message"] = a
end
empty rule.tags do |a|
hash["tags"] = a
end
singleton rule.justifications do |a|
hash["justification"] = a
end
singleton rule.before_examples do |a|
hash["before"] = a
end
singleton rule.after_examples do |a|
hash["after"] = a
end
end
end
def empty(array)
unless array.empty?
yield array.to_a
end
end
def singleton(array)
empty(array) do
if array.length == 1
yield array.first
else
yield array.to_a
end
end
end
end
end
end
================================================
FILE: lib/querly/cli/test.rb
================================================
module Querly
class CLI
class Test
attr_reader :config_path
attr_reader :stdout
attr_reader :stderr
def initialize(config_path:, stdout: STDOUT, stderr: STDERR)
@config_path = config_path
@stdout = stdout
@stderr = stderr
@success = true
end
def fail!
@success = false
end
def failed?
!@success
end
def run
config = load_config
unless config
stdout.puts "There is nothing to test at #{config_path} ..."
stdout.puts "Make a configuration and run test again!"
return 1
end
validate_rule_uniqueness(config.rules)
validate_rule_patterns(config.rules)
failed? ? 1 : 0
rescue => exn
stderr.puts Rainbow("Fatal error:").red
stderr.puts exn.inspect
stderr.puts exn.backtrace.map {|x| " " + x }.join("\n")
1
end
def validate_rule_uniqueness(rules)
ids = Set.new
stdout.puts "Checking rule id uniqueness..."
duplications = 0
rules.each do |rule|
unless ids.add?(rule.id)
stdout.puts Rainbow(" Rule id #{rule.id} duplicated!").red
duplications += 1
end
end
fail! unless duplications == 0
end
def validate_rule_patterns(rules)
stdout.puts "Checking rule patterns..."
tests = 0
false_positives = 0
false_negatives = 0
errors = 0
rules.each do |rule|
rule.before_examples.each.with_index(1) do |example, example_index|
tests += 1
begin
unless rule.patterns.any? {|pat| test_pattern(pat, example, expected: true) }
stdout.puts(Rainbow(" #{rule.id}").red + ":\t#{ordinalize example_index} *before* example didn't match with any pattern")
false_negatives += 1
end
rescue Parser::SyntaxError
errors += 1
stdout.puts(Rainbow(" #{rule.id}").red + ":\tParsing failed for #{ordinalize example_index} *before* example")
end
end
rule.after_examples.each.with_index(1) do |example, example_index|
tests += 1
begin
unless rule.patterns.all? {|pat| test_pattern(pat, example, expected: false) }
stdout.puts(Rainbow(" #{rule.id}").red + ":\t#{ordinalize example_index} *after* example matched with some of patterns")
false_positives += 1
end
rescue Parser::SyntaxError
errors += 1
stdout.puts(Rainbow(" #{rule.id}") + ":\tParsing failed for #{ordinalize example_index} *after* example")
end
end
rule.examples.each.with_index(1) do |example, index|
if example.before
tests += 1
begin
unless rule.patterns.any? {|pat| test_pattern(pat, example.before, expected: true) }
stdout.puts(Rainbow(" #{rule.id}").red + ":\tbefore of #{ordinalize index} example didn't match with any pattern")
false_negatives += 1
end
rescue Parser::SyntaxError
errors += 1
stdout.puts(Rainbow(" #{rule.id}").red + ":\tParsing failed on before of #{ordinalize index} example")
end
end
if example.after
tests += 1
begin
unless rule.patterns.all? {|pat| test_pattern(pat, example.after, expected: false) }
stdout.puts(Rainbow(" #{rule.id}").red + ":\tafter of #{ordinalize index} example matched with some of patterns")
false_positives += 1
end
rescue Parser::SyntaxError
errors += 1
stdout.puts(Rainbow(" #{rule.id}") + ":\tParsing failed on after of #{ordinalize index} example")
end
end
end
end
stdout.puts "Tested #{rules.size} rules with #{tests} tests."
if false_positives > 0 || false_negatives > 0 || errors > 0
stdout.puts " #{false_positives} examples found which should not match, but matched"
stdout.puts " #{false_negatives} examples found which should match, but didn't"
stdout.puts " #{errors} examples raised error"
fail!
else
stdout.puts Rainbow(" All tests green!").green
end
end
def test_pattern(pattern, example, expected:)
analyzer = Analyzer.new(config: nil, rule: nil)
found = false
node = Parser::Ruby30.parse(example)
NodePair.new(node: node).each_subpair do |pair|
if analyzer.test_pair(pair, pattern)
found = true
end
end
found == expected
end
def load_config
if config_path.file?
yaml = YAML.load(config_path.read)
Config.load(yaml, config_path: config_path, root_dir: config_path.parent.realpath, stderr: STDERR)
end
end
def ordinalize(number)
ActiveSupport::Inflector.ordinalize(number)
end
end
end
end
================================================
FILE: lib/querly/cli.rb
================================================
require "thor"
require "json"
if ENV["NO_COLOR"]
Rainbow.enabled = false
end
module Querly
class CLI < Thor
desc "check [paths]", "Check paths based on configuration"
option :config, default: "querly.yml"
option :root
option :format, default: "text", type: :string, enum: %w(text json)
option :rule, type: :string
option :threads, default: Parallel.processor_count, type: :numeric
def check(*paths)
require 'querly/cli/formatter'
formatter = case options[:format]
when "text"
Formatter::Text.new
when "json"
Formatter::JSON.new
end
formatter.start
threads = Integer(options[:threads])
begin
unless config_path.file?
STDERR.puts <<-Message
Configuration file #{config_path} does not look a file.
Specify configuration file by --config option.
Message
exit 1
end
begin
config = config(root_option: options[:root])
rescue => exn
formatter.config_error config_path, exn
end
analyzer = Analyzer.new(config: config, rule: options[:rule])
ScriptEnumerator.new(paths: paths.empty? ? [Pathname.pwd] : paths.map {|path| Pathname(path) }, config: config, threads: threads).each do |path, script|
case script
when Script
analyzer.scripts << script
formatter.script_load script
when StandardError, LoadError
formatter.script_error path, script
end
end
analyzer.run do |script, rule, pair|
formatter.issue_found script, rule, pair
end
rescue => exn
formatter.fatal_error exn
exit 1
ensure
formatter.finish
end
end
desc "console [paths]", "Start console for given paths"
option :config, default: "querly.yml"
option :threads, default: Parallel.processor_count, type: :numeric
def console(*paths)
require 'querly/cli/console'
home_path = if (path = ENV["QUERLY_HOME"])
Pathname(path)
else
Pathname(Dir.home) + ".querly"
end
home_path.mkdir unless home_path.exist?
config = config_path.file? ? config(root_option: nil) : nil
threads = Integer(options[:threads])
Console.new(
paths: paths.empty? ? [Pathname.pwd] : paths.map {|path| Pathname(path) },
history_path: home_path + "history",
history_size: ENV["QUERLY_HISTORY_SIZE"]&.to_i || 1_000_000,
config: config,
threads: threads
).start
end
desc "find pattern [paths]", "Find for the pattern in given paths"
option :config, default: "querly.yml"
option :threads, default: Parallel.processor_count, type: :numeric
def find(pattern, *paths)
require 'querly/cli/find'
config = config_path.file? ? config(root_option: nil) : nil
threads = Integer(options[:threads])
Find.new(
pattern: pattern,
paths: paths.empty? ? [Pathname.pwd] : paths.map {|path| Pathname(path) },
config: config,
threads: threads
).start
end
desc "test", "Check configuration"
option :config, default: "querly.yml"
def test()
require "querly/cli/test"
exit Test.new(config_path: config_path).run
end
desc "rules", "Print loaded rules"
option :config, default: "querly.yml"
def rules(*ids)
require "querly/cli/rules"
Rules.new(config_path: config_path, ids: ids).run
end
desc "version", "Print version"
def version
puts "Querly #{VERSION}"
end
def self.source_root
File.join(__dir__, "../..")
end
include Thor::Actions
desc "init", "Generate Querly config file (querly.yml)"
def init()
copy_file("template.yml", "querly.yml")
end
private
def config(root_option:)
root_path = root_option ? Pathname(root_option).realpath : config_path.parent.realpath
yaml = YAML.load(config_path.read)
Config.load(yaml, config_path: config_path, root_dir: root_path, stderr: STDERR)
end
def config_path
[Pathname(options[:config]),
Pathname("querly.yaml")].compact.find(&:file?) || Pathname(options[:config])
end
end
end
================================================
FILE: lib/querly/concerns/backtrace_formatter.rb
================================================
module Querly
module Concerns
module BacktraceFormatter
def format_backtrace(backtrace, indent: 2)
backtrace.map {|x| " "*indent + x }.join("\n")
end
end
end
end
================================================
FILE: lib/querly/config.rb
================================================
module Querly
class Config
attr_reader :rules
attr_reader :preprocessors
attr_reader :root_dir
attr_reader :checks
attr_reader :rules_cache
def initialize(rules:, preprocessors:, root_dir:, checks:)
@rules = rules
@root_dir = root_dir
@preprocessors = preprocessors
@checks = checks
@rules_cache = {}
end
def self.load(hash, config_path:, root_dir:, stderr: STDERR)
Factory.new(hash, config_path: config_path, root_dir: root_dir, stderr: stderr).config
end
def all_rules
@all_rules ||= Set.new(rules)
end
def relative_path_from_root(path)
path.absolute? ? path.relative_path_from(root_dir) : path.cleanpath
end
def rules_for_path(path)
relative_path = relative_path_from_root(path)
matching_checks = checks.select {|check| check.match?(path: relative_path) }
if rules_cache.key?(matching_checks)
rules_cache[matching_checks]
else
matching_checks.flat_map(&:rules).inject(all_rules) do |rules, query|
query.apply(current: rules, all: all_rules)
end.tap do |rules|
rules_cache[matching_checks] = rules
end
end
end
class Factory
attr_reader :yaml
attr_reader :root_dir
attr_reader :stderr
attr_reader :config_path
def initialize(yaml, config_path:, root_dir:, stderr: STDERR)
@yaml = yaml
@config_path = config_path
@root_dir = root_dir
@stderr = stderr
end
def config
if yaml["tagging"]
stderr.puts "tagging is deprecated and ignored"
end
rules = Array(yaml["rules"]).map {|hash| Rule.load(hash) }
preprocessors = (yaml["preprocessor"] || {}).each.with_object({}) do |(key, value), hash|
hash[key] = Preprocessor.new(ext: key, command: value)
end
imports = Array(yaml["import"])
imports.each do |import|
if import["load"]
load_pattern = Pathname(import["load"])
load_pattern = config_path.parent + load_pattern if load_pattern.relative?
Pathname.glob(load_pattern.to_s) do |path|
stderr.puts "Loading rules from #{path}..."
YAML.load(path.read).each do |hash|
rules << Rule.load(hash)
end
end
end
if import["require"]
stderr.puts "Require rules from #{import["require"]}..."
require import["require"]
end
end
rules.concat Querly.required_rules
checks = Array(yaml["check"]).map {|hash| Check.load(hash) }
Config.new(rules: rules, preprocessors: preprocessors, checks: checks, root_dir: root_dir)
end
end
end
end
================================================
FILE: lib/querly/node_pair.rb
================================================
module Querly
class NodePair
attr_reader :node
attr_reader :parent
def initialize(node:, parent: nil)
@node = node
@parent = parent
end
def children
node.children.flat_map do |child|
if child.is_a?(Parser::AST::Node)
self.class.new(node: child, parent: self)
else
[]
end
end
end
def each_subpair(&block)
if block_given?
return unless node
yield self
children.each do |child|
child.each_subpair(&block)
end
else
enum_for :each_subpair
end
end
end
end
================================================
FILE: lib/querly/pattern/argument.rb
================================================
module Querly
module Pattern
module Argument
class Base
attr_reader :tail
def initialize(tail:)
@tail = tail
end
def ==(other)
other.class == self.class && other.attributes == attributes
end
def attributes
instance_variables.each.with_object({}) do |name, hash|
hash[name] = instance_variable_get(name)
end
end
end
class AnySeq < Base
def initialize(tail: nil)
super(tail: tail)
end
end
class Expr < Base
attr_reader :expr
def initialize(expr:, tail:)
@expr = expr
super(tail: tail)
end
end
class KeyValue < Base
attr_reader :key
attr_reader :value
attr_reader :negated
def initialize(key:, value:, tail:, negated: false)
@key = key
@value = value
@negated = negated
super(tail: tail)
end
end
class BlockPass < Base
attr_reader :expr
def initialize(expr:)
@expr = expr
super(tail: nil)
end
end
end
end
end
================================================
FILE: lib/querly/pattern/expr.rb
================================================
module Querly
module Pattern
module Expr
class Base
def =~(pair)
test_node(pair.node)
end
def test_node(node)
false
end
def ==(other)
other.class == self.class && other.attributes == attributes
end
def attributes
instance_variables.each.with_object({}) do |name, hash|
hash[name] = instance_variable_get(name)
end
end
end
class Any < Base
def test_node(node)
!!node
end
end
class Not < Base
attr_reader :pattern
def initialize(pattern:)
@pattern = pattern
end
def test_node(node)
!pattern.test_node(node)
end
end
class Constant < Base
attr_reader :path
def initialize(path:)
@path = path
end
def test_node(node)
if path
test_constant node, path
else
node&.type == :const
end
end
def test_constant(node, path)
if node
case node.type
when :const
parent = node.children[0]
name = node.children[1]
if name == path.last
path.count == 1 || test_constant(parent, path.take(path.count - 1))
end
when :cbase
path.empty?
end
else
path.empty?
end
end
end
class Nil < Base
def test_node(node)
node&.type == :nil
end
end
class Literal < Base
attr_reader :type
attr_reader :values
def initialize(type:, values: nil)
@type = type
@values = values ? Array(values) : nil
end
def with_values(values)
self.class.new(type: type, values: values)
end
def test_value(object)
if values
values.any? {|value| value === object }
else
true
end
end
def test_node(node)
case node&.type
when :int
return false unless type == :int || type == :number
test_value(node.children.first)
when :float
return false unless type == :float || type == :number
test_value(node.children.first)
when :true
type == :bool && (values == nil || values == [true])
when :false
type == :bool && (values == nil || values == [false])
when :str
return false unless type == :string
test_value(node.children.first.scrub)
when :sym
return false unless type == :symbol
test_value(node.children.first)
when :regexp
return false unless type == :regexp
test_value(node.children.first)
end
end
end
class Send < Base
attr_reader :name
attr_reader :receiver
attr_reader :args
attr_reader :block
def initialize(receiver:, name:, block:, args: Argument::AnySeq.new)
@name = Array(name)
@receiver = receiver
@args = args
@block = block
end
def =~(pair)
# Skip send node with block
type = pair.node.type
if (type == :send || type == :csend) && pair.parent
if pair.parent.node.type == :block
if pair.parent.node.children.first.equal? pair.node
return false
end
end
end
test_node pair.node
end
def test_name(node)
name.map do |n|
case n
when String
n.to_sym
else
n
end
end.any? {|n| n === node.children[1] }
end
def test_node(node)
return false if block == true && node.type != :block
return false if block == false && node.type == :block
node = node.children.first if node&.type == :block
case node&.type
when :send, :csend
return false unless test_name(node)
return false unless test_receiver(node.children[0])
return false unless test_args(node.children.drop(2), args)
true
end
end
def test_receiver(node)
case receiver
when Self
!node || receiver.test_node(node)
when nil
true
else
receiver.test_node(node)
end
end
def test_args(nodes, args)
first_node = nodes.first
case args
when Argument::AnySeq
case args.tail
when Argument::KeyValue
if first_node
case
when nodes.last.type == :kwsplat
true
when nodes.last.type == :hash && args.tail.is_a?(Argument::KeyValue)
hash = hash_node_to_hash(nodes.last)
test_hash_args(hash, args.tail)
else
test_hash_args({}, args.tail)
end
else
test_hash_args({}, args.tail)
end
when Argument::Expr
nodes.size.times.any? do |i|
test_args(nodes.drop(i), args.tail)
end
else
true
end
when Argument::Expr
if first_node
args.expr.test_node(nodes.first) && test_args(nodes.drop(1), args.tail)
end
when Argument::KeyValue
if first_node
types = nodes.map(&:type)
if types == [:hash]
hash = hash_node_to_hash(nodes.first)
test_hash_args(hash, args)
elsif types == [:hash, :kwsplat]
true
else
args.negated
end
else
test_hash_args({}, args)
end
when Argument::BlockPass
first_node&.type == :block_pass && args.expr.test_node(first_node.children.first)
when nil
nodes.empty?
end
end
def hash_node_to_hash(node)
node.children.each.with_object({}) do |pair, h|
key = pair.children[0]
value = pair.children[1]
if key.type == :sym
h[key.children[0]] = value
end
end
end
def test_hash_args(hash, args)
while args
if args.is_a?(Argument::KeyValue)
node = hash[args.key]
if !args.negated == !!(node && args.value.test_node(node))
hash.delete args.key
else
return false
end
else
break
end
args = args.tail
end
args.is_a?(Argument::AnySeq) || hash.empty?
end
end
class ReceiverContext < Base
attr_reader :receiver
def initialize(receiver:)
@receiver = receiver
end
def test_node(node)
if receiver.test_node(node)
true
else
type = node&.type
(type == :send || type == :csend) && test_node(node.children[0])
end
end
end
class Self < Base
def test_node(node)
node&.type == :self
end
end
class Vcall < Base
attr_reader :name
def initialize(name:)
@name = name
end
def =~(pair)
node = pair.node
if node.type == :lvar
# We don't want lvar without method call
# Skips when the node is not receiver of :send
parent_node = pair.parent&.node
if parent_node && (parent_node.type == :send || parent_node.type == :csend) && parent_node.children.first.equal?(node)
test_node(node)
end
else
test_node(node)
end
end
def test_node(node)
case node&.type
when :send, :csend
node.children[1] == name
when :lvar
node.children.first == name
end
end
end
class Dstr < Base
def test_node(node)
node&.type == :dstr
end
end
class Ivar < Base
attr_reader :name
def initialize(name:)
@name = name
end
def test_node(node)
if node&.type == :ivar
name.nil? || node.children.first == name
end
end
end
end
end
end
================================================
FILE: lib/querly/pattern/kind.rb
================================================
module Querly
module Pattern
module Kind
class Base
attr_reader :expr
def initialize(expr:)
@expr = expr
end
end
module Negatable
attr_reader :negated
def initialize(expr:, negated:)
@negated = negated
super(expr: expr)
end
end
class Any < Base
def test_kind(pair)
true
end
end
class Conditional < Base
include Negatable
def test_kind(pair)
!negated == !!conditional?(pair)
end
def conditional?(pair)
node = pair.node
parent = pair.parent&.node
case parent&.type
when :if
node.equal? parent.children.first
when :while
node.equal? parent.children.first
when :and
node.equal? parent.children.first
when :or
node.equal? parent.children.first
when :csend
node.equal? parent.children.first
else
false
end
end
end
class Discarded < Base
include Negatable
def test_kind(pair)
!negated == !!discarded?(pair)
end
def discarded?(pair)
node = pair.node
parent = pair.parent&.node
case parent&.type
when :begin
if node.equal? parent.children.last
discarded? pair.parent
else
true
end
else
false
end
end
end
end
end
end
================================================
FILE: lib/querly/pattern/parser.y
================================================
class Querly::Pattern::Parser
prechigh
nonassoc EXCLAMATION
nonassoc LPAREN
left DOT
preclow
rule
target: kinded_expr
kinded_expr: expr { result = Kind::Any.new(expr: val[0]) }
| expr CONDITIONAL_KIND { result = Kind::Conditional.new(expr: val[0], negated: val[1]) }
| expr DISCARDED_KIND { result = Kind::Discarded.new(expr: val[0], negated: val[1]) }
expr: constant { result = Expr::Constant.new(path: val[0]) }
| send
| SELF { result = Expr::Self.new }
| EXCLAMATION expr { result = Expr::Not.new(pattern: val[1]) }
| BOOL { result = Expr::Literal.new(type: :bool, values: val[0]) }
| literal { result = val[0] }
| literal AS META { result = val[0].with_values(resolve_meta(val[2])) }
| DSTR { result = Expr::Dstr.new() }
| UNDERBAR { result = Expr::Any.new }
| NIL { result = Expr::Nil.new }
| LPAREN expr RPAREN { result = val[1] }
| IVAR { result = Expr::Ivar.new(name: val[0]) }
literal:
STRING { result = Expr::Literal.new(type: :string, values: val[0]) }
| INT { result = Expr::Literal.new(type: :int, values: val[0]) }
| FLOAT { result = Expr::Literal.new(type: :float, values: val[0]) }
| SYMBOL { result = Expr::Literal.new(type: :symbol, values: val[0]) }
| NUMBER { result = Expr::Literal.new(type: :number, values: val[0]) }
| REGEXP { result = Expr::Literal.new(type: :regexp, values: nil) }
args: { result = nil }
| expr { result = Argument::Expr.new(expr: val[0], tail: nil)}
| expr COMMA args { result = Argument::Expr.new(expr: val[0], tail: val[2]) }
| AMP expr { result = Argument::BlockPass.new(expr: val[1]) }
| kw_args
| DOTDOTDOT { result = Argument::AnySeq.new }
| DOTDOTDOT COMMA args { result = Argument::AnySeq.new(tail: val[2]) }
| DOTDOTDOT COMMA kw_args { result = Argument::AnySeq.new(tail: val[2]) }
kw_args: { result = nil }
| AMP expr { result = Argument::BlockPass.new(expr: val[1]) }
| DOTDOTDOT { result = Argument::AnySeq.new }
| key_value { result = Argument::KeyValue.new(key: val[0][:key],
value: val[0][:value],
tail: nil,
negated: val[0][:negated]) }
| key_value COMMA kw_args { result = Argument::KeyValue.new(key: val[0][:key],
value: val[0][:value],
tail: val[2],
negated: val[0][:negated]) }
key_value: keyword COLON expr { result = { key: val[0], value: val[2], negated: false } }
| EXCLAMATION keyword COLON expr { result = { key: val[1], value: val[3], negated: true } }
method_name: METHOD
| EXCLAMATION
| AS
| META { result = resolve_meta(val[0]) }
method_name_or_ident: method_name
| LIDENT
| UIDENT
keyword: LIDENT | UIDENT
constant: UIDENT { result = [val[0]] }
| UIDENT COLONCOLON constant { result = [val[0]] + val[2] }
send: LIDENT block { result = val[1] != nil ? Expr::Send.new(receiver: nil, name: val[0], block: val[1]) : Expr::Vcall.new(name: val[0]) }
| UIDENT block { result = Expr::Send.new(receiver: nil, name: val[0], block: val[1]) }
| method_name { result = Expr::Send.new(receiver: nil, name: val[0], block: nil) }
| method_name_or_ident LPAREN args RPAREN block { result = Expr::Send.new(receiver: nil,
name: val[0],
args: val[2],
block: val[4]) }
| receiver method_name_or_ident block { result = Expr::Send.new(receiver: val[0],
name: val[1],
args: Argument::AnySeq.new,
block: val[2]) }
| receiver method_name_or_ident LPAREN args RPAREN block { result = Expr::Send.new(receiver: val[0],
name: val[1],
args: val[3],
block: val[5]) }
| receiver UNDERBAR block { result = Expr::Send.new(receiver: val[0], name: /.+/, block: val[2]) }
| receiver UNDERBAR LPAREN args RPAREN block { result = Expr::Send.new(receiver: val[0],
name: /.+/,
args: val[3],
block: val[5]) }
receiver: expr DOT { result = val[0] }
| expr DOTDOTDOT { result = Expr::ReceiverContext.new(receiver: val[0]) }
block: { result = nil }
| WITH_BLOCK { result = true }
| WITHOUT_BLOCK { result = false }
end
---- inner
require "strscan"
attr_reader :input
attr_reader :where
def initialize(input, where:)
super()
@input = StringScanner.new(input)
@where = where
end
def self.parse(str, where:)
self.new(str, where: where).do_parse
end
def next_token
input.scan(/\s+/)
case
when input.eos?
[false, false]
when input.scan(/true\b/)
[:BOOL, true]
when input.scan(/false\b/)
[:BOOL, false]
when input.scan(/nil/)
[:NIL, false]
when input.scan(/:string:/)
[:STRING, nil]
when input.scan(/"([^"]+)"/)
[:STRING, input[1]]
when input.scan(/:dstr:/)
[:DSTR, nil]
when input.scan(/:int:/)
[:INT, nil]
when input.scan(/:float:/)
[:FLOAT, nil]
when input.scan(/:bool:/)
[:BOOL, nil]
when input.scan(/:symbol:/)
[:SYMBOL, nil]
when input.scan(/:number:/)
[:NUMBER, nil]
when input.scan(/:regexp:/)
[:REGEXP, nil]
when input.scan(/:\w+/)
s = input.matched
[:SYMBOL, s[1, s.size - 1].to_sym]
when input.scan(/as\b/)
[:AS, :as]
when input.scan(/{}/)
[:WITH_BLOCK, nil]
when input.scan(/!{}/)
[:WITHOUT_BLOCK, nil]
when input.scan(/[+-]?[0-9]+\.[0-9]/)
[:FLOAT, input.matched.to_f]
when input.scan(/[+-]?[0-9]+/)
[:INT, input.matched.to_i]
when input.scan(/\_/)
[:UNDERBAR, input.matched]
when input.scan(/[A-Z]\w*/)
[:UIDENT, input.matched.to_sym]
when input.scan(/self/)
[:SELF, nil]
when input.scan(/'[a-z]\w*/)
s = input.matched
[:META, s[1, s.size - 1].to_sym]
when input.scan(/[a-z_](\w)*(\?|\!|=)?/)
[:LIDENT, input.matched.to_sym]
when input.scan(/\(/)
[:LPAREN, input.matched]
when input.scan(/\)/)
[:RPAREN, input.matched]
when input.scan(/\.\.\./)
[:DOTDOTDOT, input.matched]
when input.scan(/\,/)
[:COMMA, input.matched]
when input.scan(/\./)
[:DOT, input.matched]
when input.scan(/\!/)
[:EXCLAMATION, input.matched.to_sym]
when input.scan(/\[conditional\]/)
[:CONDITIONAL_KIND, false]
when input.scan(/\[!conditional\]/)
[:CONDITIONAL_KIND, true]
when input.scan(/\[discarded\]/)
[:DISCARDED_KIND, false]
when input.scan(/\[!discarded\]/)
[:DISCARDED_KIND, true]
when input.scan(/\[\]=/)
[:METHOD, :"[]="]
when input.scan(/\[\]/)
[:METHOD, :"[]"]
when input.scan(/::/)
[:COLONCOLON, input.matched]
when input.scan(/:/)
[:COLON, input.matched]
when input.scan(/\*/)
[:STAR, "*"]
when input.scan(/@\w+/)
[:IVAR, input.matched.to_sym]
when input.scan(/@/)
[:IVAR, nil]
when input.scan(/&/)
[:AMP, nil]
end
end
def resolve_meta(name)
where[name] or raise Racc::ParseError, "Undefined meta variable: '#{name}"
end
================================================
FILE: lib/querly/pp/cli.rb
================================================
require "optparse"
module Querly
module PP
class CLI
attr_reader :argv
attr_reader :command
attr_reader :load_paths
attr_reader :requires
attr_reader :stdin
attr_reader :stderr
attr_reader :stdout
def initialize(argv, stdin: STDIN, stdout: STDOUT, stderr: STDERR)
@argv = argv
@stdin = stdin
@stdout = stdout
@stderr = stderr
@load_paths = []
@requires = []
OptionParser.new do |opts|
opts.banner = "Usage: #{opts.program_name} pp-name [options]"
opts.on("-I dir") {|path| load_paths << path }
opts.on("-r lib") {|rq| requires << rq }
end.permute!(argv)
@command = argv.shift&.to_sym
end
def load_libs
load_paths.each do |path|
$LOAD_PATH << path
end
requires.each do |lib|
require lib
end
end
def run
available_commands = [:haml, :erb]
if available_commands.include?(command)
send :"run_#{command}"
else
stderr.puts "Unknown command: #{command}"
stderr.puts " available commands: #{available_commands.join(", ")}"
exit 1
end
end
def run_haml
require "haml"
load_libs
source = stdin.read
if Haml::VERSION >= '5.0.0'
stdout.print Haml::Engine.new(source).precompiled
else
options = Haml::Options.new
parser = Haml::Parser.new(source, options)
parser.parse
compiler = Haml::Compiler.new(options)
compiler.compile(parser.root)
stdout.print compiler.precompiled
end
end
def run_erb
require 'better_html'
require 'better_html/parser'
load_libs
source = stdin.read
source_buffer = Parser::Source::Buffer.new('(erb)')
source_buffer.source = source
parser = BetterHtml::Parser.new(source_buffer, template_language: :html)
new_source = source.gsub(/./, ' ')
parser.ast.descendants(:erb).each do |erb_node|
indicator_node, _, code_node, = *erb_node
next if indicator_node&.loc&.source == '#'
new_source[code_node.loc.range] = code_node.loc.source
new_source[code_node.loc.range.end] = ';'
end
stdout.puts new_source
end
end
end
end
================================================
FILE: lib/querly/preprocessor.rb
================================================
module Querly
class Preprocessor
class Error < StandardError
attr_reader :command
attr_reader :status
def initialize(command:, status:)
@command = command
@status = status
end
end
attr_reader :ext
attr_reader :command
def initialize(ext:, command:)
@ext = ext
@command = command
end
def run!(path)
stdout_read, stdout_write = IO.pipe
output = ""
reader = Thread.new do
while (line = stdout_read.gets)
output << line
end
end
succeeded = system(command, in: path.to_s, out: stdout_write)
stdout_write.close
reader.join
raise Error.new(status: $?, command: command) unless succeeded
output
end
end
end
================================================
FILE: lib/querly/rule.rb
================================================
module Querly
class Rule
class Example
attr_reader :before
attr_reader :after
def initialize(before:, after:)
@before = before
@after = after
end
def ==(other)
other.is_a?(Example) && other.before == before && other.after == after
end
end
attr_reader :id
attr_reader :patterns
attr_reader :messages
attr_reader :sources
attr_reader :justifications
attr_reader :before_examples
attr_reader :after_examples
attr_reader :examples
attr_reader :tags
def initialize(id:, messages:, patterns:, sources:, tags:, before_examples:, after_examples:, justifications:, examples:)
@id = id
@patterns = patterns
@sources = sources
@messages = messages
@justifications = justifications
@before_examples = before_examples
@after_examples = after_examples
@tags = tags
@examples = examples
end
def match?(identifier: nil, tags: nil)
if identifier
unless id == identifier || id.start_with?(identifier + ".")
return false
end
end
if tags
unless tags.subset?(self.tags)
return false
end
end
true
end
class InvalidRuleHashError < StandardError; end
class PatternSyntaxError < StandardError; end
def self.load(hash)
id = hash["id"]
raise InvalidRuleHashError, "id is missing" unless id
srcs = case hash["pattern"]
when Array
hash["pattern"]
when nil
[]
else
[hash["pattern"]]
end
raise InvalidRuleHashError, "pattern is missing" if srcs.empty?
patterns = srcs.map.with_index do |src, index|
case src
when String
subject = src
where = {}
when Hash
subject = src['subject']
where = Hash[src['where'].map {|k,v| [k.to_sym, translate_where(v)] }]
end
begin
Pattern::Parser.parse(subject, where: where)
rescue Racc::ParseError => exn
raise PatternSyntaxError, "Pattern syntax error: rule=#{hash["id"]}, index=#{index}, pattern=#{Rainbow(subject.split("\n").first).blue}, where=#{where.inspect}: #{exn}"
end
end
messages = Array(hash["message"])
raise InvalidRuleHashError, "message is missing" if messages.empty?
tags = Set.new(Array(hash["tags"]))
examples = [hash["examples"]].compact.flatten.map do |example|
raise(InvalidRuleHashError, "Example should have at least before or after, #{example.inspect}") unless example.key?("before") || example.key?("after")
Example.new(before: example["before"], after: example["after"])
end
before_examples = Array(hash["before"])
after_examples = Array(hash["after"])
justifications = Array(hash["justification"])
Rule.new(id: id,
messages: messages,
patterns: patterns,
sources: srcs,
tags: tags,
before_examples: before_examples,
after_examples: after_examples,
justifications: justifications,
examples: examples)
end
def self.translate_where(value)
Array(value).map do |v|
case v
when /\A\/(.*)\/\Z/
Regexp.new($1)
else
v
end
end
end
end
end
================================================
FILE: lib/querly/rules/sample.rb
================================================
Querly.load_rule File.join(__dir__, "../../../rules/sample.yml")
================================================
FILE: lib/querly/script.rb
================================================
module Querly
class Script
attr_reader :path
attr_reader :node
def self.load(path:, source:)
parser = Parser::Ruby30.new(Builder.new).tap do |parser|
parser.diagnostics.all_errors_are_fatal = true
parser.diagnostics.ignore_warnings = true
end
buffer = Parser::Source::Buffer.new(path.to_s, 1)
buffer.source = source
self.new(path: path, node: parser.parse(buffer))
end
def initialize(path:, node:)
@path = path
@node = node
end
def root_pair
NodePair.new(node: node)
end
class Builder < Parser::Builders::Default
def string_value(token)
value(token)
end
def emit_lambda
true
end
end
end
end
================================================
FILE: lib/querly/script_enumerator.rb
================================================
module Querly
class ScriptEnumerator
attr_reader :paths
attr_reader :config
attr_reader :threads
def initialize(paths:, config:, threads:)
@paths = paths
@config = config
@threads = threads
end
# Yields `Script` object concurrently, in different threads.
def each(&block)
if block_given?
Parallel.each(each_path, in_threads: threads) do |path|
load_script_from_path path, &block
end
else
self.enum_for :each
end
end
def each_path(&block)
if block_given?
paths.each do |path|
if path.directory?
enumerate_files_in_dir(path, &block)
else
yield path
end
end
else
enum_for :each_path
end
end
@loaders = []
def self.register_loader(pattern, loader)
@loaders << [pattern, loader]
end
def self.find_loader(path)
basename = path.basename.to_s
@loaders.find {|pair| pair.first === basename }&.last
end
private
def load_script_from_path(path, &block)
preprocessor = preprocessors[path.extname]
begin
source = if preprocessor
preprocessor.run!(path)
else
path.read
end
script = Script.load(path: path, source: source)
rescue StandardError, LoadError, Preprocessor::Error => exn
script = exn
end
yield(path, script)
end
def preprocessors
config&.preprocessors || {}
end
def enumerate_files_in_dir(path, &block)
if path.basename.to_s =~ /\A\.[^\.]+/
# skip hidden paths
return
end
case
when path.directory?
path.children.each do |child|
enumerate_files_in_dir child, &block
end
when path.file?
extensions = %w[
.rb .builder .fcgi .gemspec .god .jbuilder .jb .mspec .opal .pluginspec
.podspec .rabl .rake .rbuild .rbw .rbx .ru .ruby .spec .thor .watchr
]
basenames = %w[
.irbrc .pryrc buildfile Appraisals Berksfile Brewfile Buildfile Capfile
Cheffile Dangerfile Deliverfile Fastfile Gemfile Guardfile Jarfile Mavenfile
Podfile Puppetfile Rakefile Snapfile Thorfile Vagabondfile Vagrantfile
]
should_load_file = case
when extensions.include?(path.extname)
true
when basenames.include?(path.basename.to_s)
true
else
preprocessors.key?(path.extname)
end
yield path if should_load_file
end
end
end
end
================================================
FILE: lib/querly/version.rb
================================================
module Querly
VERSION = "1.3.0"
end
================================================
FILE: lib/querly.rb
================================================
require 'pathname'
require "yaml"
require "rainbow"
require "parser/ruby30"
require "set"
require "open3"
require "active_support/inflector"
require "parallel"
require "querly/version"
require 'querly/analyzer'
require 'querly/rule'
require 'querly/pattern/expr'
require 'querly/pattern/argument'
require 'querly/script'
require 'querly/script_enumerator'
require 'querly/node_pair'
require "querly/pattern/parser"
require 'querly/pattern/kind'
require "querly/config"
require "querly/preprocessor"
require "querly/check"
require "querly/concerns/backtrace_formatter"
module Querly
@@required_rules = []
def self.required_rules
@@required_rules
end
def self.load_rule(*files)
files.each do |file|
path = Pathname(file)
yaml = YAML.load(path.read)
rules = yaml.map {|hash| Rule.load(hash) }
required_rules.concat rules
end
end
end
================================================
FILE: manual/configuration.md
================================================
# Overview
The configuration file, default name is `querly.yml`, will look like the following.
```yml
rules:
...
preprocessor:
...
check:
...
```
# rules
`rules` is array of rule hash.
```yml
- id: com.sideci.json
pattern: Net::HTTP
message: "Should use HTTPClient instead of Net::HTTP"
justification:
- No exception!
before:
- "Net::HTTP.get(url)"
after:
- HTTPClient.new.get_content(url)
```
The rule hash contains following keys:
* `id` Identifier of the rule, must be unique (string)
* `pattern` Patterns to find out (string, or array of string)
* `message` Error message to explain why the code fragment needs special care (string)
* `justification` When the *bad use* is allowed (string, or array of string)
* `before` Sample ruby code to find out (string, or array of string)
* `after` Sample ruby code to be fixed (string, or array of string)
# preprocessor
When your project contains `.slim`, `.haml`, or any templates which contains Ruby code, preprocessor is to translate the templates to Ruby code.
`preprocessor` is a hash; key of extension of the templates, value of command line.
```yml
.slim: slimrb --compile
.haml: bundle exec querly-pp haml -I lib -r your_custom_plugin
```
The command will be executed with stdin of template code, and should emit ruby code to stdout.
## querly-pp
Querly 0.2.0 ships with `querly-pp` command line tool which compiles given HAML source to Ruby script.
`-I` and `-r` options can be used to use plugins.
# check
Define set of rules to check for each file.
```yml
check:
- path: /test
rules:
- com.acme.corp
- append: com.acme.corp
- except: com.acme.corp
- only: com.acme.corp
- path: /test/unit
rules:
- append:
tags: foo bar
- except:
tags: foo bar
- only:
tags: foo bar
```
* `path` Files to apply the rules in `.gitignore` syntax
* `rules` Rules to check
All matching `check` element against given file name will be applied, sequentially.
* `/lib/bar.rb` => no checks will be applied (all rules)
* `/test/test_helper.rb` => `/test` check will be applied
* `/test/unit/account_test.rb` => `/test` and `/test/unit` checks will be applied
## Rules
You can use `append:`, `except:` and `only:` operation.
* `append:` appends rules to current rule set
* `except:` removes rules from current rule set
* `only:` update current rule set
================================================
FILE: manual/examples.md
================================================
In this page, I will show some rules I have written. They are all real rules from my repos.
# `js: true` option with feature spec
I see some of Feature Spec scenarios have `js: true` option, and others do not. The reason they look strange to me is some scenarios without `js: true` depend on JavaScript. I changed one of the `js: true` to `js: false`, and run the specs again. They run! What is happening?? The `js` does not stand for *JavaScript*?? What else?
The magic happens in `rails_helper.rb`.
```rb
Capybara.configure do |config|
config.default_driver = :poltergeist
config.javascript_driver = :poltergeist
end
```
Okay, `js: true` does not make any sense, because both `default_driver` and `javascript_driver` are same.
Should we fix all of the scenarios now? If we leave them, new teammates will misunderstand `js: true` does something important and required. However, we don't want to fix them now. It does not do anything bad right now. So, my conclusion is *I will do that, but not now* 😸
It's the time to add a new Querly rule. When someone tries to add new scenario with `js: true`, tell the person that it does not make any sense.
```yaml
- id: sample.scenario_with_js_option
pattern: "scenario(..., js: _, ...)"
message: |
You do not need js:true option
We are using Poltergeist as both default_driver and javascript_driver!
before:
- "scenario 'hello world', js: true, type: :feature do end"
after:
- "scenario 'foo bar' do end"
```
No new `js: true` scenario will be written. Our new teammate may try to write that. But Querly will tell they don't have to do that, instead of me.
================================================
FILE: manual/patterns.md
================================================
# Syntax
## Toplevel
* *expr*
* *expr* `[` *kind* `]` (kinded expr)
* *expr* `[!` *kind* `]` (negated kinded expr)
## expr
* `_` (any expr)
* *method* (method call, with any receiver and any args)
* *method* `(` *args* `)` *block_spec* (method call with any receiver)
* *receiver* *method* (method call with any args)
* *receiver* *method* `(` *args* `)` *block_spec* (method call)
* *literal*
* `self` (self)
* `!` *expr*
### block_spec
* (no spec)
* `{}` (method call should be with block)
* `!{}` (method call should not be with block)
### receiver
* *expr* `.` (receiver matching with the pattern)
* *expr* `...` (some receiver in the chain matching with the pattern)
### Examples
* `p(_)` `p` call with one argument, any receiver
* `self.p(1)` `p` call with `1`, receiver is `self` or omitted.
* `foo.bar.baz` `baz` call with receiver of `bar` call of receiver of `foo` call
* `update_attribute(:symbol:, :string:)` `update_attribute` call with symbol and string literals
* `File.open(...) !{}` `File.open` call but without block
```rb
p 1 # p(_) matches
p 2 # p(_) matches
p 1, 2, 3 # p(_) does not match
p(1) # self.p(1) matches
foo(1).bar {|x| x+1 }.baz(3) # foo.bar.baz matches
(1+2).foo.bar(*args).baz.bla # foo.bar.baz matches, partially
foo.xyz.bar.baz # foo.bar.baz does not match
update_attribute(:name, "hoge") # f(:symbol:, :string:) matches
update_attribute(:name, name) # f(:symbol:, :string:) does not match
foo.bar.baz # foo.bar.baz matches
foo.bar.baz # foo...baz matches
bar.foo.baz # foo...bar...baz does not match
```
## args & kwargs
### args
* *expr* `,` *args*
* *expr* `,` *kwargs*
* *expr*
* `...` `,` *kwargs* (any argument sequence, followed by keyword arguments)
* `...` (any argument sequence, including any keyword arguments)
### Literals
* `123` (integer)
* `1.23` (float)
* `:foobar` (symbol)
* `:symbol:` (any symbol literal)
* `"foobar"` (string)
* NOTE: It only supports double quotation.
* `:string:` (any string literal)
* `:dstr:` (any dstr `"hi #{name}"`)
* `true`, `false` (true and false)
* `nil` (nil)
* `:number:`, `:int:`, `:float:` (any number, any integer, any float)
* `:bool:` (true or false)
### kwargs
* *symbol* `:` *expr* `,` ...
* `!` *symbol* `:` *expr* `,` ...
* `...`
* `&` *expr*
### Examples
```rb
f(1,2,3) # f(...), f(1,2,...), and f(1, ...) matches
# f(_,_), f(0, ...) does not match
JSON.load(string, symbolize_names: true) # JSON.load(..., symbolize_names: true) matches
# JSON.load(symbolize_names: true) does not match
record.update(email: email, name: name) # update(name: _, email: _) matches
# update(name: _) does not match
# update(name: _, ...) matches
# update(!id: _, ...) matches
article.try(&:author) # try(&:symbol:) matches
article.try(:author) # try(&:symbol:) does not match
article.try {|x| x.author } # try(&:symbol:) does not match
```
## kind
* `conditional` (When expr appears in *conditional* context)
* `discarded` (When expr appears in *discarded* context)
Kind allows you to find out something like:
* `save` call but does not check its result for error recovery
*conditional* context is
* Condition of `if` construct
* Condition of loop constructs
* LHS of `&&` and `||`
```rb
# record.save is in conditional context
unless record.save
# error recovery
end
# record.save is not in conditional context
x = record.save
# record.save is in conditional context
record.save or abort()
```
*discarded* context is where the value of the expression is completely discarded, a bit looser than *conditional*.
```rb
def f()
# record.save is in discarded context
foo()
record.save()
bar
end
```
# Interpolation Syntax
If you want to describe a pattern that can not be described with above syntax, you can use interpolation as follows:
```yaml
id: find_by_abc_and_def
pattern:
subject: "'finder(...)"
where:
finder:
- /find_by_\w+\_.*/
- find_by_id
```
It matches with `find_by_email_and_name(...)`.
- Meta variables `'finder` can occur only as method name
- Unused meta var definition is okay, but undefined meta var reference raises an error
- If value of meta var is a string `foo`, it matches send nodes with exactly same method name
- If value of meta var is a regexp `/foo/`, it matches send nodes with method name which `=~` the regexp
You can also use `as` syntax with `:symbol:` and so on.
```yaml
id: migration_references
pattern:
subject: "t.integer(:symbol: as 'column, ...)"
where:
column: '/.+_id/'
```
# Difference from Ruby
* Method call parenthesis cannot be omitted (if omitted, it means *any arguments*)
* `+`, `-`, `[]` or other *operator* should be written as method calls like `_.+(_)`, `[]=(:string:, _)`
# Testing
You can test patterns by `querly console .` command interactively.
```
Querly 0.1.0, interactive console
Commands:
- find PATTERN Find PATTERN from given paths
- reload! Reload program from paths
- quit
Loading... ready!
>
```
Also `querly test` will help you.
It test configuration file by checking patterns in rules against `before` and `after` examples.
================================================
FILE: querly.gemspec
================================================
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'querly/version'
Gem::Specification.new do |spec|
spec.name = "querly"
spec.version = Querly::VERSION
spec.authors = ["Soutaro Matsumoto"]
spec.email = ["matsumoto@soutaro.com"]
spec.license = "MIT"
spec.summary = %q{Pattern Based Checking Tool for Ruby}
spec.description = %q{Querly is a query language and tool to find out method calls from Ruby programs. Define rules to check your program with patterns to find out *bad* pieces. Querly finds out matching pieces from your program.}
spec.homepage = "https://github.com/soutaro/querly"
spec.files = `git ls-files -z`
.split("\x0")
.reject { |f| f.match(%r{^(test|spec|features)/}) }
.push('lib/querly/pattern/parser.rb')
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
spec.required_ruby_version = ">= 2.7"
spec.add_development_dependency "bundler", ">= 1.12"
spec.add_development_dependency "rake", "~> 13.0"
spec.add_development_dependency "minitest", "~> 5.0"
spec.add_development_dependency "racc", ">= 1.4.14"
spec.add_development_dependency "unification_assertion", "0.0.1"
spec.add_development_dependency "better_html", "~> 1.0.13"
spec.add_development_dependency "slim", "~> 4.0.1"
spec.add_development_dependency "haml", "~> 5.0.4"
spec.add_dependency 'thor', ">= 0.19.0"
spec.add_dependency "parser", ">= 3.0"
spec.add_dependency "rainbow", ">= 2.1"
spec.add_dependency "activesupport", ">= 5.0"
spec.add_dependency "parallel", "~>1.17"
end
================================================
FILE: rules/sample.yml
================================================
- id: sample.ruby.pathname
pattern: Pathname.new
message: |
Did you mean `Pathname` method?
- id: sample.ruby.file.dir
pattern: "File.dirname(:string:)"
message: |
Did you mean `__dir__`?
- id: sample.ruby.file.open
pattern: File.open(...) !{}
message: |
Using block is better in Ruby
- id: sample.ruby.debug_print
pattern:
- self.p
- self.pp
message: |
Delete debug print
- id: sample.ruby.clone
pattern:
- clone()
- "clone(!freeze: _)"
message: |
Did you mean `dup`?
`clone` returns frozen object when receiver is frozen.
* If object is frozen, you usually does not need to clone it
* When you need a copy of a object, you probably want to modify that; did you mean dup?
* Ruby 2.4 accepts freeze keyword and returns not frozen object
================================================
FILE: sample.yaml
================================================
rules:
- id: sample.delete_all
pattern:
- delete_all
- update_all
message: |
Validations and callbacks will be skipped
It's faster than calling destroy or update each record, but may be dangerous.
justification:
- You have too much record to update/destroy one by one
- When you are sure skipping callbacks/validations is okay
before:
- records.delete_all
- "records.update_all(people_id: nil)"
after:
- records.each(&:destroy)
- |
records.each do |record|
record.update(people_id: nil)
end
- id: sample.save_not_conditional
pattern:
- "save(!validate: false, ...) [!conditional]"
- "update(...) [!conditional]"
- "update_attributes(...) [!conditional]"
message: |
save or update returns false when it fails.
Check the return value and try error recovery.
justification:
- When there is no way to recover from error.
before:
- record.save()
- "record.update(foo: bar)"
- "record.update(attrs)"
after:
- if record.save() then do_something end
- "record.save(validate: false)"
- "record.update(foo: 1) or fail"
- id: sample.skip_validation
pattern:
- "save(validate: false)"
- "save!(validate: false)"
message: These calls will skip validation
justification:
- When you want to skip validation
- When you have done validation code in other than ActiveRecord's validation mechanism
- id: sample.net_http
pattern: Net::HTTP
message: Use HTTPClient to make Web api calls
before:
- Net::HTTP.get(url)
after:
- HTTPClient.new.get_content(url)
- id: sample.root_url_without_locale
pattern: "root_url(!locale: _)"
message: Put locale parameter in the link to top page
before: root_url()
after: "root_url(locale: I18n.locale)"
- id: sample.transaction
pattern: transaction
message: Use with_account_lock helper, generally
justification:
- It does not need lock over account
- It does not access accounts table
before:
- transaction do something end
after:
- with_account_lock(account) do something end
- id: sample.oj
pattern:
- JSON.load
- JSON.dump
message: Use Oj for JSON load and dump
- id: sample.metaprogramming_abuse
pattern:
- classify
- constantize
- eval
- instance_values
- safe_constantize
message: Consider three times before using meta-programming
- id: sample.activesupport.try
pattern: "try(:symbol:, ...)"
message: try returns nil if the method is not defined, try! instead?
- id: sample.try_with_block_pass
pattern: "try(&:symbol:)"
message: It's same as just passing symbol, and slower
- id: sample.transaction_renew
pattern: "transaction(requires_new: true)"
message: Our RDBMS does not support nested transaction
- id: sample.capybara.assertion
pattern:
- assert_equal
- assert
message: |
Using minitest assertions with Capybara may make test unstable
Use Capybara assertions like assert_selector
justification:
- There is no access to Capybara in that test
- You have using retrying in test
tags:
- test
- capybara
- id: sample.capybara.negations
pattern:
- refute
- assert_nil
message: |
Negating capybara assertions would make test unstable
Maybe you can use Capybara helpers like has_no_css?
justification:
- There is no access to Capybara in the test
- You have implemented retrying in test
tags:
- test
- capybara
- id: sample.order-group
pattern: order...group
before:
- records.where.order.group
- records.order.where.group
message: |
Using both group and order may generate broken SQL
- id: sample.pp_meta
message: |
Method names can be a meta variable reference
Meta variable starts with single quote ', and followed with lower letter.
pattern:
- subject: "'p(...)"
where:
p: /p+/
- id: sample.count
pattern: count() !{}
message: |
Use size or length for count, if receiver is an array
examples:
- before: "[].count"
after: "[].size"
- after: "[].count(:x)"
- after: "[].count {|x| x > 3 }"
preprocessor:
.slim: slimrb --compile
.haml: querly-pp haml
.erb: querly-pp erb
import:
- load: querly/rules/*.yml
- require: querly/rules/sample
check:
- path: /
rules:
- except: minitest
- path: /test
rules:
- minitest
- except:
tags: capybara minitest
- path: /test/integration
rules:
- append:
tags: capybara minitest
- path: /features/step_definitions
rules:
- append:
tags: capybara minitest
================================================
FILE: template.yml
================================================
rules:
- id: sample.debug_print
pattern:
- self.p
- self.pp
message: Delete debug print
examples:
- before: |
pp some: error
- id: sample.file.open
pattern: File.open(...) !{}
message: |
Use block to read/write file
If you use block, the open method closes file implicitly.
You don't have to close files explicitly.
examples:
- before: |
io = File.open("foo.txt")
io.write("hello world")
io.close
after: |
File.open("foo.txt") do |io|
io.write("hello world")
end
- id: sample.exception
pattern: Exception
message: |
You probably should use StandardError
If you are trying to define error class, inherit that from StandardError.
justification:
- You are sure you want to define an exception which is not rescued by default
examples:
- before: class MyError < Exception; end
after: class MyError < StandardError; end
- id: sample.test.assert_equal_size
pattern:
subject: "assert_equal(:int: as 'zero, _.'size, ...)"
where:
zero: 0
size:
- size
- count
message: |
Comparing size of something with 0 can be written using assert_empty
examples:
- before: |
assert_equal 0, some.size
after: |
assert_empty some.size
- before: |
assert_equal 0, some.count
after: |
assert_empty some.count
preprocessor:
# .slim: slimrb --compile # Install `slim` gem for slim support
# .erb: querly-pp erb # Install `better_erb` gem for erb support
# .haml: querly-pp haml # Install `haml` gem for haml support
check:
- path: /
rules:
- except: sample.test
- path: /test
rules:
- append: sample.test
================================================
FILE: test/analyzer_test.rb
================================================
require_relative "test_helper"
class AnalyzerTest < Minitest::Test
Analyzer = Querly::Analyzer
Config = Querly::Config
def stderr
@stderr ||= StringIO.new
end
end
================================================
FILE: test/check_test.rb
================================================
require_relative "test_helper"
class CheckTest < Minitest::Test
Check = Querly::Check
Rule = Querly::Rule
def root
@root ||= Pathname("/root/path")
end
def test_match1
check = Check.new(pattern: "foo", rules: [])
assert check.match?(path: Pathname("foo/bar"))
assert check.match?(path: Pathname("foo"))
assert check.match?(path: Pathname("bar/foo"))
assert check.match?(path: Pathname("bar/foo/baz"))
refute check.match?(path: Pathname("foobar"))
refute check.match?(path: Pathname("bar"))
refute check.match?(path: Pathname("bazbar"))
end
def test_match2
check = Check.new(pattern: "foo/bar", rules: [])
assert check.match?(path: Pathname("foo/bar"))
assert check.match?(path: Pathname("foo/bar/baz"))
refute check.match?(path: Pathname("xyzzy/foo/bar"))
refute check.match?(path: Pathname("foo/baz/bar"))
end
def test_match3
check = Check.new(pattern: "foo/bar/", rules: [])
assert check.match?(path: Pathname("foo/bar/baz"))
refute check.match?(path: Pathname("foo/bar"))
refute check.match?(path: Pathname("xyz/foo/bar/baz"))
end
def test_match4
check = Check.new(pattern: "foo/", rules: [])
assert check.match?(path: Pathname("foo/bar"))
assert check.match?(path: Pathname("baz/foo/bar"))
refute check.match?(path: Pathname("foo"))
refute check.match?(path: Pathname("baz/foo"))
end
def test_match5
check = Check.new(pattern: "/foo", rules: [])
assert check.match?(path: Pathname("foo/bar"))
assert check.match?(path: Pathname("foo"))
refute check.match?(path: Pathname("baz/foo/bar"))
refute check.match?(path: Pathname("baz/foo"))
end
def test_load
check = Check.load('path' => "foo",
'rules' => [
"rails.models",
{ "id" => "ruby", "tags" => ["foo", "bar"] },
{ "append" => { "tags" => ["baz"] } },
{ "only" => "minitest" },
{ "except" => { "id" => "rspec", "tags" => "t1 t2" } },
])
assert_equal 5, check.rules.size
# appending rule by id
assert_equal Check::Query.new(:append, nil, "rails.models"), check.rules[0]
# default operand is append
assert_equal Check::Query.new(:append, Set.new(["foo", "bar"]), "ruby"), check.rules[1]
# append by explicit tags
assert_equal Check::Query.new(:append, Set.new(["baz"]), nil), check.rules[2]
# only by implicit id
assert_equal Check::Query.new(:only, nil, "minitest"), check.rules[3]
# except by explicit id
assert_equal Check::Query.new(:except, Set.new(["t1", "t2"]), "rspec"), check.rules[4]
end
def test_query_match
rule = Rule.new(id: "ruby.pathname", messages: nil, patterns: nil, sources: nil, tags: Set.new(["tag1", "tag2"]), before_examples: [], after_examples: [], justifications: [], examples: [])
assert Check::Query.new(:append, nil, "ruby.pathname").match?(rule)
assert Check::Query.new(:append, nil, "ruby").match?(rule)
refute Check::Query.new(:append, nil, "ruby23").match?(rule)
assert Check::Query.new(:append, Set.new(["tag1"]), nil).match?(rule)
assert Check::Query.new(:append, Set.new(["tag1", "tag2"]), nil).match?(rule)
refute Check::Query.new(:append, Set.new(["tag1", "foo"]), nil).match?(rule)
assert Check::Query.new(:append, Set.new(["tag1"]), "ruby").match?(rule)
refute Check::Query.new(:append, Set.new(["tag1"]), "ruby23").match?(rule)
refute Check::Query.new(:append, Set.new(["tag1", "foo"]), "ruby").match?(rule)
end
def test_query_apply
r1 = Rule.new(id: "ruby.pathname", messages: nil, patterns: nil, sources: nil, tags: Set.new(), before_examples: [], after_examples: [], justifications: [], examples: [])
r2 = Rule.new(id: "minitest.assert", messages: nil, patterns: nil, sources: nil, tags: Set.new(), before_examples: [], after_examples: [], justifications: [], examples: [])
all_rules = Set.new([r1, r2])
assert_equal Set.new([r1, r2]), Check::Query.new(:append, nil, "ruby").apply(current: Set.new([r2]), all: all_rules)
assert_equal Set.new([r2]), Check::Query.new(:except, nil, "ruby").apply(current: all_rules, all: all_rules)
assert_equal Set.new([r1]), Check::Query.new(:only, nil, "ruby").apply(current: all_rules, all: all_rules)
end
end
================================================
FILE: test/cli/console_test.rb
================================================
require_relative "../test_helper"
require "querly/cli/console"
require "pty"
class ConsoleTest < Minitest::Test
include TestHelper
def exe_path
Pathname(__dir__) + "../../exe/querly"
end
def read_for(read, pattern:)
timeout_at = Time.now + 3
result = ""
while true
if Time.now > timeout_at
raise "Timedout waiting for #{pattern}"
end
buf = ""
read.read_nonblock 1024, buf rescue IO::EAGAINWaitReadable
if buf == ""
sleep 0.1
else
result << buf.force_encoding(Encoding::UTF_8)
end
if pattern =~ result
break
end
end
result
end
def test_console
mktmpdir do |path|
(path + "foo.rb").write(<<-EOF)
class UsersController
def create
user = User.create!(params[:user])
redirect_to user_path(user)
end
end
EOF
homedir = path + "home"
homedir.mkdir
history = path + "home/.querly/history"
PTY.spawn({ "NO_COLOR" => "true", "QUERLY_HISTORY_SIZE" => "2", "HOME" => homedir.to_s }, exe_path.to_s, "console", chdir: path.to_s) do |read, write, pid|
read_for(read, pattern: /^> $/)
write.puts "reload!"
read.gets
read_for(read, pattern: /^> $/)
write.puts "find create!"
read.gets
output = read_for(read, pattern: /^> $/)
assert_match %r/#{Regexp.escape "User.create!(params[:user])"}/, output
write.puts "find redirect_to"
read.gets
output = read_for(read, pattern: /^> $/)
assert_match %r/#{Regexp.escape "redirect_to user_path(user)"}/, output
write.puts "find User.find_each"
read.gets
output = read_for(read, pattern: /^> $/)
assert_match %r/#{Regexp.escape "0 results"}/, output
write.puts "find crea te !!"
read.gets
output = read_for(read, pattern: /^> $/)
assert_match %r/#{Regexp.escape "parse error on value"}/, output
write.puts "no such command"
read.gets
output = read_for(read, pattern: /^> $/)
assert_match %r/#{Regexp.escape "Commands:"}/, output
write.puts "quit"
read.gets
Process.wait pid
end
assert_equal ["find redirect_to", "find User.find_each"], history.readlines.map(&:chomp)
PTY.spawn({ "NO_COLOR" => "true", "QUERLY_HISTORY_SIZE" => "2", "HOME" => homedir.to_s }, exe_path.to_s, "console", chdir: path.to_s) do |read, write, pid|
read_for(read, pattern: /^> $/)
write.puts "reload!"
read.gets
read_for(read, pattern: /^> $/)
write.puts "find create!"
read.gets
output = read_for(read, pattern: /^> $/)
assert_match %r/#{Regexp.escape "User.create!(params[:user])"}/, output
write.puts "exit"
read.gets
Process.wait pid
end
assert_equal ["find User.find_each", "find create!"], history.readlines.map(&:chomp)
end
end
def test_history_location_override
mktmpdir do |path|
(path + "foo.rb").write(<<-EOF)
class UsersController
def create
user = User.create!(params[:user])
redirect_to user_path(user)
end
end
EOF
homedir = path + "querly"
homedir.mkdir
PTY.spawn({ "NO_COLOR" => "true", "QUERLY_HISTORY_SIZE" => "2", "QUERLY_HOME" => homedir.to_s }, exe_path.to_s, "console", chdir: path.to_s) do |read, write, pid|
read_for(read, pattern: /^> $/)
write.puts "find create!"
read.gets
output = read_for(read, pattern: /^> $/)
assert_match %r/#{Regexp.escape "User.create!(params[:user])"}/, output
write.puts "quit"
read.gets
Process.wait pid
end
history = path + "querly/history"
assert_equal ["find create!"], history.readlines.map(&:chomp)
end
end
end
================================================
FILE: test/cli/rules_test.rb
================================================
require_relative "../test_helper"
require "querly/cli/rules"
class RulesTest < Minitest::Test
include TestHelper
def test_rules_command
config = {
"rules" => [
{
"id" => "foo.rule1",
"message" => "Sample Message",
"pattern" => "@_"
},
{
"id" => "bar.rule2",
"message" => ["foo", "bar"],
"pattern" => ["@_", "foo"]
}
]
}
with_config config do |path|
rules = Querly::CLI::Rules.new(config_path: path, ids: ["foo"], stdout: stdout)
rules.run
assert_match(/foo\.rule1/, stdout.string)
refute_match(/bar\.rule2/, stdout.string)
end
end
end
================================================
FILE: test/cli/test_test.rb
================================================
require_relative "../test_helper"
class TestTest < Minitest::Test
Test = Querly::CLI::Test
Config = Querly::Config
attr_accessor :stdout, :stderr
def setup
self.stdout = StringIO.new
self.stderr = StringIO.new
end
def test_load_config_failure
test = Test.new(config_path: Pathname("querly.yaml"), stdout: stdout, stderr: stderr)
def test.load_config
nil
end
result = test.run
assert_equal 1, result
assert_match %r/There is nothing to test at querly\.yaml/, stdout.string
end
def test_rule_uniqueness
test = Test.new(config_path: Pathname("querly.yaml"), stdout: stdout, stderr: stderr)
def test.load_config
Config.load(
{
"rules" =>
[
{ "id" => "id1", "pattern" => "_", "message" => "hello" },
{ "id" => "id1", "pattern" => "_", "message" => "hello" },
{ "id" => "id2", "pattern" => "_", "message" => "hello" }
]
},
config_path: Pathname.pwd,
root_dir: Pathname.pwd,
stderr: stderr
)
end
result = test.run
assert_equal 1, result
assert_match %r/Rule id id1 duplicated!/, stdout.string
refute_match %r/Rule id id2 duplicated!/, stdout.string
end
def test_rule_patterns_pass
test = Test.new(config_path: Pathname("querly.yaml"), stdout: stdout, stderr: stderr)
def test.load_config
Config.load(
{
"rules" => [
{
"id" => "id1",
"pattern" => [
"foo()",
"foo(1)"
],
"message" => "hello",
"before" => ["self.foo()", "foo(1)"],
"after" => ["self.foo(x)", "bar()"]
},
]
},
config_path: Pathname.pwd,
root_dir: Pathname.pwd,
stderr: stderr
)
end
result = test.run
assert_equal 0, result
assert_match %r/All tests green!/, stdout.string
end
def test_rule_patterns_before_after_fail
test = Test.new(config_path: Pathname("querly.yaml"), stdout: stdout, stderr: stderr)
def test.load_config
Config.load(
{
"rules" => [
{
"id" => "id1",
"pattern" => [
"foo()",
"foo(1)"
],
"message" => "hello",
"before" => ["self.foo(x)", "foo(1)"],
"after" => ["self.foo()", "bar(1)"]
},
]
},
config_path: Pathname.pwd,
root_dir: Pathname.pwd,
stderr: stderr
)
end
result = test.run
assert_equal 1, result
assert_match %r/id1:\t1st \*before\* example didn't match with any pattern/, stdout.string
assert_match %r/id1:\t1st \*after\* example matched with some of patterns/, stdout.string
assert_match %r/1 examples found which should not match, but matched/, stdout.string
assert_match %r/1 examples found which should match, but didn't/, stdout.string
end
def test_rule_patterns_example_fail
test = Test.new(config_path: Pathname("querly.yaml"), stdout: stdout, stderr: stderr)
def test.load_config
Config.load(
{
"rules" => [
{
"id" => "id1",
"pattern" => [
"foo()",
"foo(1)"
],
"message" => "hello",
"examples" => [{ "before" => "self.foo(1)", "after" => "self.foo(1)" },
{ "before" => "foo(0)", "after" => "bar(1)" }
]
},
]
},
config_path: Pathname.pwd,
root_dir: Pathname.pwd,
stderr: stderr
)
end
result = test.run
assert_equal 1, result
assert_match %r/id1:\tafter of 1st example matched with some of patterns/, stdout.string
assert_match %r/id1:\tbefore of 2nd example didn't match with any pattern/, stdout.string
assert_match %r/1 examples found which should not match, but matched/, stdout.string
assert_match %r/1 examples found which should match, but didn't/, stdout.string
end
def test_rule_patterns_error
test = Test.new(config_path: Pathname("querly.yaml"), stdout: stdout, stderr: stderr)
def test.load_config
Config.load(
{
"rules" =>[
{
"id" => "id1",
"pattern" => "_",
"message" => "hello",
"examples" => [{ "before" => "self.foo(", "after" => "1)" }]
},
]
},
config_path: Pathname.pwd,
root_dir: Pathname.pwd,
stderr: stderr
)
end
result = test.run
assert_equal 1, result
assert_match %r/2 examples raised error/, stdout.string
end
end
================================================
FILE: test/config_test.rb
================================================
require_relative "test_helper"
class ConfigTest < Minitest::Test
include TestHelper
Config = Querly::Config
Preprocessor = Querly::Preprocessor
def stderr
@stderr ||= StringIO.new
end
def test_factory_config_returns_empty_config
config = Config::Factory.new({}, config_path: Pathname("/foo/bar"), root_dir: Pathname("/foo/bar"), stderr: stderr).config
assert_instance_of Config, config
assert_empty config.rules
assert_empty config.preprocessors
assert_equal Pathname("/foo/bar"), config.root_dir
end
def test_factory_config_resturns_config_with_rules
config = Config::Factory.new(
{
"rules" => [
{
"id" => "rule.id",
"pattern" => "_",
"message" => "Hello world"
}
],
"preprocessor" => {
".slim" => "slimrb --compile"
},
"check" => [
{
"path" => "/test",
"rules" => ["rails", "minitest"]
},
{
"path" => "/test/integration",
"rules" => ["capybara", { "except" => "minitest" }]
}
]
},
config_path: Pathname("/foo/bar"),
root_dir: Pathname("/foo/bar"),
stderr: stderr
).config
assert_instance_of Config, config
assert_equal ["rule.id"], config.rules.map(&:id)
assert_equal [".slim"], config.preprocessors.keys
assert_equal Pathname("/foo/bar"), config.root_dir
end
def test_factory_config_prints_warning_on_tagging
Config::Factory.new({ "tagging" => [] }, config_path: Pathname("/foo/bar"), root_dir: Pathname("/foo/bar"), stderr: stderr).config
assert_match %r/tagging is deprecated and ignored/, stderr.string
end
def test_relative_path_from_root
config = Config::Factory.new({}, config_path: Pathname("/foo/bar"), root_dir: Pathname("/foo/bar"), stderr: stderr).config
# Relative path from root_dir
assert_equal Pathname("a/b/c.rb"), config.relative_path_from_root(Pathname("a/b/c.rb"))
assert_equal Pathname("a/b/c.rb"), config.relative_path_from_root(Pathname("a/b/../b/c.rb"))
assert_equal Pathname("baz/Rakefile"), config.relative_path_from_root(Pathname("/foo/bar/baz/Rakefile"))
# Nonsense...
assert_equal Pathname("../x.rb"), config.relative_path_from_root(Pathname("../x.rb"))
assert_equal Pathname("../../a/b/c.rb"), config.relative_path_from_root(Pathname("/a/b/c.rb"))
end
def test_loading_rules_from_file
hash = { "import" => [
{ "load" => "foo.yml" },
{ "load" => "rules/*" }
]}
with_config hash do |path|
dir = path.parent
(dir + "foo.yml").write(YAML.dump([{ "id" => "rule1", "pattern" => "_", "message" => "rule1" }]))
(dir + "rules").mkpath
(dir + "rules" + "1.yml").write(YAML.dump([{ "id" => "rule2", "pattern" => "_", "message" => "rule1" }]))
(dir + "rules" + "2.yml").write(YAML.dump([
{ "id" => "rule3", "pattern" => "_", "message" => "rule1" },
{ "id" => "rule4", "pattern" => "_", "message" => "rule1" }
]))
config = Config.load(YAML.load(path.read), config_path: path, root_dir: path, stderr: stderr)
assert_equal ["rule1", "rule2", "rule3", "rule4"], config.rules.map(&:id).sort
end
end
def test_analyzer_rules_for_path
root_dir = Pathname("/foo/bar")
config = Config.load(
{
"rules" => [{ "id" => "rule1", "pattern" => "_", "message" => "" },
{ "id" => "rule2", "pattern" => "_", "message" => "" },
{ "id" => "rule3", "pattern" => "_", "message" => "" },
{ "id" => "rule4", "pattern" => "_", "message" => "" }],
"check" => [{ "path" => "/test", "rules" => [{ "only" => "rule2" }] },
{ "path" => "/test/unit", "rules" => ["rule3"] }]
},
config_path: root_dir,
root_dir: root_dir,
stderr: stderr
)
assert_equal ["rule1", "rule2", "rule3", "rule4"], config.rules_for_path(root_dir + "foo.rb").map(&:id)
assert_equal ["rule2"], config.rules_for_path(root_dir + "test/foo.rb").map(&:id)
assert_equal ["rule2", "rule3"], config.rules_for_path(root_dir + "test/unit/foo.rb").map(&:id)
end
end
================================================
FILE: test/data/test1/querly.yml
================================================
rules:
- id: test1.rule1
pattern:
- foobar
message: |
Use foo.bar instead of foobar
foo.bar is not good.
justification:
- Some reason
- Another reason
examples:
- before: foobar
after: foobarbaz
================================================
FILE: test/data/test1/script.rb
================================================
foobar()
================================================
FILE: test/data/test2/querly.yml
================================================
rules:
- id: test2.rule1
pattern:
- foobar
message: Use foo.bar instead of foobar
================================================
FILE: test/data/test2/script.rb
================================================
1+
================================================
FILE: test/data/test3/querly.yml
================================================
rules:
- id: test.pppp
message: pp...
pattern:
subject: "'p(...)"
where:
p: /p+/
================================================
FILE: test/data/test3/script.rb
================================================
p(1)
pp(2)
ppp(3)
================================================
FILE: test/data/test4/querly.yml
================================================
rules:
- id: test_rule
message: This is a test message
pattern: _.test
================================================
FILE: test/data/test4/script.rb
================================================
array = [1, 2, 3]
# Endless range since Ruby 2.6
array[1..]
# Beginless range since Ruby 2.7
array[..1]
# Numbered block parameters since Ruby 2.7
array.map { _1**2 }
# Pattern matching since Ruby 2.7
case array
in [a, *]
puts a
end
# Arguments forwarding since Ruby 2.7
def foo(...)
bar(...)
end
# Extended arguments forwarding since Ruby 3.0
def foo2(a, ...)
bar2(a, ...)
end
# One-line pattern matching since Ruby 3.0
{ a: 1, b: 2, c: 3 } => hash
# Endless method definition since Ruby 3.0
def square(x) = x * x
================================================
FILE: test/node_pair_test.rb
================================================
require_relative "test_helper"
class NodePairTest < Minitest::Test
include TestHelper
def test_each_subpair
node = ruby("foo(bar(baz))")
pair = Querly::NodePair.new(node: node)
nodes = pair.each_subpair.map(&:node)
assert_equal 3, nodes.count
assert nodes.include?(ruby("baz"))
assert nodes.include?(ruby("bar(baz)"))
assert nodes.include?(ruby("foo(bar(baz))"))
end
end
================================================
FILE: test/pattern_parser_test.rb
================================================
require_relative "test_helper"
class PatternParserTest < Minitest::Test
include TestHelper
def test_parser1
pattern = parse_expr("foo().bar")
assert_instance_of E::Send, pattern
assert_equal [:bar], pattern.name
assert_instance_of A::AnySeq, pattern.args
assert_instance_of E::Send, pattern.receiver
assert_equal [:foo], pattern.receiver.name
assert_nil pattern.receiver.args
end
def test_aaa
# p Querly::Pattern::Parser.parse("foo(!foo: bar)")
end
def test_ivar
pat = parse_expr("@")
assert_equal E::Ivar.new(name: nil), pat
end
def test_ivar_with_name
pat = parse_expr("@x_123A")
assert_equal E::Ivar.new(name: :@x_123A), pat
end
def test_pattern
pat = parse_expr(":racc")
assert_equal E::Literal.new(type: :symbol, values: :racc), pat
end
def test_constant
pat = parse_expr("E")
assert_equal E::Constant.new(path: [:E]), pat
end
def test_dot3_args
pat = parse_expr("foo(..., 1, ...)")
assert_equal E::Send.new(receiver: nil,
name: :foo,
args: A::AnySeq.new(tail: A::Expr.new(expr: E::Literal.new(type: :int, values: 1),
tail: A::AnySeq.new)),
block: nil), pat
end
def test_keyword_arg
pat = parse_expr("foo(!x: 1, ...)")
assert_equal E::Send.new(receiver: nil,
name: :foo,
args: A::KeyValue.new(key: :x,
value: E::Literal.new(type: :int, values: 1),
negated: true,
tail: A::AnySeq.new),
block: nil), pat
end
def test_keyword_arg2
pat = parse_expr("foo(!X: 1, ...)")
assert_equal E::Send.new(receiver: nil,
name: :foo,
args: A::KeyValue.new(key: :X,
value: E::Literal.new(type: :int, values: 1),
negated: true,
tail: A::AnySeq.new),
block: nil), pat
end
def test_send_with_block
pat = parse_expr("foo() {}")
assert_equal E::Send.new(receiver: nil,
name: :foo,
args: nil,
block: true), pat
end
def test_send_without_block
pat = parse_expr("foo() !{}")
assert_equal E::Send.new(receiver: nil,
name: :foo,
args: nil,
block: false), pat
end
def test_send_without_block2
pat = parse_expr("foo !{}")
assert_equal E::Send.new(receiver: nil,
name: :foo,
args: A::AnySeq.new,
block: false), pat
end
def test_send_with_block_uident
pat = parse_expr("Foo {}")
assert_equal E::Send.new(receiver: nil,
name: :Foo,
args: A::AnySeq.new,
block: true), pat
end
def test_method_names
assert_equal [:[]], parse_expr("[]()").name
assert_equal [:[]=], parse_expr("[]=()").name
assert_equal [:!], parse_expr("!()").name
end
def test_send
assert_equal :f, parse_expr("f").name
assert_equal [:f], parse_expr("f()").name
assert_equal [:f], parse_expr("_.f").name
assert_equal [:f], parse_expr("_.f()").name
assert_equal [:F], parse_expr("F()").name
assert_equal [:F], parse_expr("_.F()").name
assert_equal [:F], parse_expr("_.F").name
end
def test_any_method
recv = E::Vcall.new(name: :a)
assert_equal E::Send.new(receiver: recv,
name: /.+/,
args: A::AnySeq.new,
block: nil), parse_expr("a._")
assert_equal E::Send.new(receiver: recv,
name: /.+/,
args: A::AnySeq.new,
block: true), parse_expr("a._{}")
assert_equal E::Send.new(receiver: recv,
name: /.+/,
args: A::AnySeq.new,
block: false), parse_expr("a._!{}")
assert_equal E::Send.new(receiver: recv,
name: /.+/,
args: nil,
block: nil), parse_expr("a._()")
assert_equal E::Send.new(receiver: recv,
name: /.+/,
args: A::Expr.new(expr: E::Vcall.new(name: :b), tail: nil),
block: nil), parse_expr("a._(b)")
end
def test_method_name
assert_equal [:f!], parse_expr("f!()").name
assert_equal [:f=], parse_expr("f=(3)").name
assert_equal [:f?], parse_expr("f?()").name
end
def test_block_pass
pat = parse_expr("map(&:id)")
args = pat.args
assert_instance_of A::BlockPass, args
assert_equal E::Literal.new(type: :symbol, values: :id), args.expr
end
def test_vcall
pat = parse_expr("foo")
assert_instance_of E::Vcall, pat
assert_equal :foo, pat.name
end
def test_dstr
pat = parse_expr(":dstr:")
assert_instance_of E::Dstr, pat
end
def test_any_kinded
pat = parse_kinded("foo")
assert_instance_of K::Any, pat
end
def test_conditonal_kinded
pat = parse_kinded("foo [conditional]")
assert_instance_of K::Conditional, pat
refute pat.negated
end
def test_conditional_kinded2
pat = parse_kinded("foo [!conditional]")
assert_instance_of K::Conditional, pat
assert pat.negated
end
def test_discarded_kinded
pat = parse_kinded("foo [discarded]")
assert_instance_of K::Discarded, pat
refute pat.negated
end
def test_discarded_kinded2
pat = parse_kinded("foo [!discarded]")
assert_instance_of K::Discarded, pat
assert pat.negated
end
def test_regexp
pat = parse_expr(":regexp:")
assert_instance_of E::Literal, pat
assert_equal :regexp, pat.type
end
def test_any_receiver
pat = parse_expr("foo...bar")
assert_instance_of E::Send, pat
assert_equal [:bar], pat.name
assert_instance_of E::ReceiverContext, pat.receiver
assert_instance_of E::Vcall, pat.receiver.receiver
assert_equal :foo, pat.receiver.receiver.name
end
def test_parse_self
pat = parse_expr("self")
assert_instance_of E::Self, pat
end
def test_send_with_meta
pat = parse_expr("'g()", where: { g: [:foo, :bar] })
assert_instance_of E::Send, pat
assert_equal [:foo, :bar], pat.name
end
def test_send_with_missing_meta
assert_raises Racc::ParseError do
parse_expr("'g('h())", where: { g: [:foo, :bar] })
end
end
def test_as_method
pat = parse_expr("self.as")
assert_instance_of E::Send, pat
assert_equal [:as], pat.name
end
def test_string
pat = parse_expr(":string: as 's", where: { s: ["foo"] })
assert_instance_of E::Literal, pat
assert_equal :string, pat.type
assert_equal ["foo"], pat.values
end
def test_string_literal
pat = parse_expr('"foo"')
assert_instance_of E::Literal, pat
assert_equal :string, pat.type
assert_equal ["foo"], pat.values
end
def test_string_literal_with_backslash_escape
skip("Implement escape sequence in the future")
pat = parse_expr('"foo\n"')
assert_instance_of E::Literal, pat
assert_equal :string, pat.type
assert_equal ["foo\n"], pat.values
end
def test_as_something
pat = parse_expr("assert()")
assert_instance_of E::Send, pat
assert_equal [:assert], pat.name
end
def test_as_things2
%w(assert true_or_false falsey).each do |word|
pat = parse_expr("#{word}()")
assert_instance_of E::Send, pat
assert_equal [word.to_sym], pat.name
end
end
end
================================================
FILE: test/pattern_test_test.rb
================================================
require_relative "test_helper"
class PatternTestTest < Minitest::Test
include TestHelper
def assert_node(node, type:)
refute_nil node
assert_equal type, node.type
yield node.children if block_given?
end
def test_ivar_without_name
nodes = query_pattern("@", "@x.foo")
assert_equal 1, nodes.size
assert_node nodes.first, type: :ivar do |name, *_|
assert_equal :@x, name
end
end
def test_ivar_with_name
nodes = query_pattern("@x", "@x + @y")
assert_equal 1, nodes.size
assert_node nodes.first, type: :ivar do |name, *_|
assert_equal :@x, name
end
end
def test_constant
nodes = query_pattern("C", "C.f")
assert_equal 1, nodes.size
assert_node nodes.first, type: :const do |parent, name|
assert_nil parent
assert_equal :C, name
end
end
def test_constant_with_parent
nodes = query_pattern("A::B", "A::B::C")
assert_node nodes.first, type: :const do |parent, name|
assert_equal :B, name
end
end
def test_constant_with_parent2
nodes = query_pattern("B::C", "A::B::C")
assert_node nodes.first, type: :const do |parent, name|
assert_equal :C, name
end
end
def test_int
nodes = query_pattern(":int:", "[/1/, 1, 3.0, 1i, 1r]")
assert_equal 1, nodes.size
assert_equal [ruby("1")], nodes
end
def test_float
nodes = query_pattern(":float:", "['42', /1/, 1, 3.0, 1i, 1r]")
assert_equal 1, nodes.size
assert_equal [ruby("3.0")], nodes
end
def test_bool
nodes = query_pattern(":bool:", "[true, false, nil]")
assert_equal 2, nodes.size
assert_equal [ruby("true"), ruby('false')], nodes
end
def test_symbol
nodes = query_pattern(":symbol:", ":foo")
assert_node nodes.first, type: :sym do |name, *_|
assert_equal :foo, name
end
end
def test_symbol2
nodes = query_pattern(":foo", ":foo.bar(:baz)")
assert_equal 1, nodes.size
assert_node nodes.first, type: :sym do |name, *_|
assert_equal :foo, name
end
end
def test_string
nodes = E::Literal.new(type: :string, values: ["foo"])
assert nodes.test_node(ruby('"foo"'))
refute nodes.test_node(ruby('"bar"'))
end
def test_string2
nodes = E::Literal.new(type: :string, values: ["foo", "bar"])
assert nodes.test_node(ruby('"foo"'))
assert nodes.test_node(ruby('"bar"'))
refute nodes.test_node(ruby('"baz"'))
end
def test_string3
nodes = E::Literal.new(type: :string, values: [/foo/])
assert nodes.test_node(ruby('"foo bar"'))
refute nodes.test_node(ruby('"baz"'))
end
def test_byte_sequence_string
nodes = E::Literal.new(type: :string, values: [/foo/])
assert nodes.test_node(ruby('"\xfffoo"'))
refute nodes.test_node(ruby('"\xffbaz"'))
end
def test_regexp
nodes = query_pattern(":regexp:", '[/1/, /#{2}/, 3]')
assert_equal 2, nodes.size
assert_equal [ruby("/1/"), ruby('/#{2}/')], nodes
end
def test_call_without_args
nodes = query_pattern("foo", "foo(); foo(1)")
assert_equal 2, nodes.size
assert_equal ruby("foo()"), nodes[0]
assert_equal ruby("foo(1)"), nodes[1]
end
def test_call_with_no_arg
nodes = query_pattern("foo()", "foo(); foo(1)")
assert_equal 1, nodes.size
assert_equal ruby("foo()"), nodes.first
end
def test_call_with_any_args
nodes = query_pattern("foo(1, ...)", "foo(0); foo(1, 2); foo(x, y)")
assert_equal 1, nodes.size
assert_equal ruby("foo(1, 2)"), nodes.first
end
def test_call_with_any_expr_arg
nodes = query_pattern("foo(_)", "foo(1, 2); foo(x)")
assert_equal 1, nodes.size
assert_equal ruby("foo(x)"), nodes.first
end
def test_call_with_not_expr_arg
nodes = query_pattern("foo(!1)", "foo(1); foo(2)")
assert_equal 1, nodes.size
assert_equal ruby("foo(2)"), nodes.first
end
def test_call_with_kw_args1
nodes = query_pattern("foo(bar: _)", "foo(bar: true)")
assert_equal 1, nodes.size
assert_equal ruby("foo(bar: true)"), nodes.first
end
def test_call_with_kw_args2
nodes = query_pattern("foo(bar: _)", "foo(bar: true, baz: 1)")
assert_empty nodes
end
def test_call_with_kw_args3
nodes = query_pattern("foo(bar: _)", "foo({ bar: true }, baz: 1)")
assert_empty nodes
end
def test_call_with_kw_args4
nodes = query_pattern("foo(_, baz: 1)", "foo({ bar: true }, baz: 1)")
assert nodes.one?
assert_equal ruby("foo({ bar: true }, baz: 1)"), nodes.first
end
def test_call_with_kw_args5
nodes = query_pattern("foo(..., baz: 1)", "foo(0, baz: 1)")
assert nodes.one?
assert_equal ruby("foo(0, baz: 1)"), nodes.first
end
def test_call_with_kw_args6
nodes = query_pattern("foo(..., baz: 1)", "foo(1, baz: 2)")
assert_empty nodes
end
def test_call_with_kw_args_rest1
nodes = query_pattern("foo(bar: _, ...)", "foo(bar: true, baz: false)")
assert_equal 1, nodes.size
assert_equal ruby("foo(bar: true, baz: false)"), nodes.first
end
def test_call_with_kw_args_rest2
nodes = query_pattern("foo(bar: _, ...)", "foo(baz: false)")
assert_empty nodes
end
def test_call_with_negated_kw1
nodes = query_pattern("foo(!bar: 1)", "foo(bar: 3)")
assert_equal 1, nodes.size
assert_equal ruby("foo(bar: 3)"), nodes.first
end
def test_call_with_negated_kw2
nodes = query_pattern("foo(!bar: 1, ...)", "foo(baz: true)")
assert_equal 1, nodes.size
assert_equal ruby("foo(baz: true)"), nodes.first
end
def test_call_with_negated_kw3
nodes = query_pattern("foo(!bar: 1, baz: true)", "foo(baz: true)")
assert_equal 1, nodes.size
assert_equal ruby("foo(baz: true)"), nodes.first
end
def test_call_with_negated_kw4
nodes = query_pattern("foo(!bar: 1)", "foo()")
assert_equal 1, nodes.size
assert_equal ruby("foo()"), nodes.first
end
def test_call_with_negated_kw5
nodes = query_pattern("foo(!bar: _)", "foo(params)")
assert_equal 1, nodes.size
assert_equal ruby("foo(params)"), nodes.first
end
def test_call_with_rest_and_kw
nodes = query_pattern("foo(_, ..., key: _, ...)", "foo(1); foo(2, key: 3); foo(key: 4); foo(1,2)")
assert_equal 1, nodes.size
assert_equal ruby("foo(2, key: 3)"), nodes.first
end
def test_call_with_two_dot3
nodes = query_pattern("foo(..., 1, ...)",
"foo(1); foo(1, 2, 3); foo(true, false); foo(2)")
assert_equal [ruby("foo(1)"), ruby("foo(1,2,3)")], nodes
end
def test_call_with_block_pass
nodes = query_pattern("map(&:id)", "foo.map(&:id)")
assert_equal 1, nodes.size
assert_equal ruby("foo.map(&:id)"), nodes.first
end
def test_call_with_names
node = E::Send.new(name: [:foo, :bar, /baz/], args: nil, receiver: E::Any.new, block: nil)
assert node.test_node(ruby("a.foo()"))
assert node.test_node(ruby("a.bar()"))
assert node.test_node(ruby("a.foo_bar_baz()"))
refute node.test_node(ruby("a.test()"))
end
def test_call_without_receiver
nodes = query_pattern("foo", "foo; bar.foo; bar&.foo")
assert_equal 3, nodes.size
end
def test_call_with_any_receiver
nodes = query_pattern("_.foo", "foo; bar.foo; bar&.foo")
assert_equal 2, nodes.size
assert_equal [ruby("bar.foo"), ruby("bar&.foo")], nodes
end
def test_call_any_method
nodes = query_pattern("foo._", "foo; foo.bar; foo.baz; foo&.baz")
assert_equal 3, nodes.size
assert_equal [ruby("foo.bar"), ruby("foo.baz"), ruby("foo&.baz")], nodes
end
def test_call_any_method_with_args
nodes = query_pattern("foo._(baz)", "foo.bar(baz); foo.bar(bar); foo&.bar(baz)")
assert_equal 2, nodes.size
assert_equal [ruby("foo.bar(baz)"), ruby("foo&.bar(baz)")], nodes
end
def test_call_any_method_with_block
nodes = query_pattern("foo._{}", "foo.bar; foo.baz{}; foo&.foobar{}")
assert_equal 2, nodes.size
assert_equal [ruby("foo.baz{}"), ruby("foo&.foobar{}")], nodes
end
def test_vcall
# Vcall pattern matches with local variable
nodes = query_pattern("foo", "foo = 1; foo.bar; foo&.bar")
assert_equal 2, nodes.size
assert_equal :lvar, nodes.first.type
assert_equal :foo, nodes.first.children.first
assert_equal :lvar, nodes[1].type
assert_equal :foo, nodes[1].children.first
end
def test_vcall2
# Vcall pattern matches with method call
nodes = query_pattern("foo", "foo(1,2,3)")
assert_equal 1, nodes.size
assert_equal ruby("foo(1,2,3)"), nodes.first
end
def test_vcall3
# If lvar is receiver, it matches
nodes = query_pattern("foo", "foo = 1; foo.bar()")
assert_equal 1, nodes.size
assert_equal ruby("foo = 1; foo").children.last, nodes.first
end
def test_vcall4
# If lvar is not a receiver, it doesn't match
nodes = query_pattern("foo", "foo = 1; f.bar(foo)")
assert_empty nodes
end
def test_dstr
nodes = query_pattern(":dstr:", 'foo("#{1+2}")')
assert_equal 1, nodes.size
assert_equal ruby('"#{1+2}"'), nodes.first
end
def test_without_block_option
nodes = query_pattern("foo()", "foo() { foo() }")
assert_equal 2, nodes.size
end
def test_with_block
nodes = query_pattern("foo() {}", "foo do foo() end")
assert_equal 1, nodes.size
assert_equal ruby("foo() do foo() end"), nodes.first
end
def test_without_block
nodes = query_pattern("foo() !{}", "foo do foo() end")
assert_equal 1, nodes.size
assert_equal ruby("foo()"), nodes.first
end
def test_any_receiver1
nodes = query_pattern("f...g", "f.g")
assert_equal [ruby("f.g")], nodes
end
def test_any_receiver2
nodes = query_pattern("f...h", "f.g.h")
assert_equal [ruby("f.g.h")], nodes
end
def test_any_receiver3
nodes = query_pattern("g...h", "f(g).h")
assert_equal [], nodes
end
def test_any_receiver4
nodes = query_pattern("a...b...c", "[a.c.b.d.c]")
assert_equal [ruby("a.c.b.d.c")], nodes
end
def test_any_receiver5
nodes = query_pattern("a...b", "[a.b.b]")
assert_equal Set.new([ruby("a.b.b"), ruby("a.b")]), Set.new(nodes)
end
def test_any_receiver6
nodes = query_pattern("f...h", "f&.g&.h")
assert_equal [ruby("f&.g&.h")], nodes
end
def test_self
nodes = query_pattern("self.f", "f(); self.f(); foo.f()")
assert_equal Set.new([ruby("self.f"), ruby("f()")]), Set.new(nodes)
end
def test_string_value
nodes = query_pattern("has_many(:symbol: as 'children)", "has_many(:children); has_many(:repositories)", where: { children: [:children] })
assert_equal Set.new([ruby("has_many :children")]), Set.new(nodes)
end
def test_conditional_if
nodes = query_pattern('foo [conditional]', 'if foo; bar; end')
assert_equal 1, nodes.size
assert_equal ruby('foo'), nodes.first
end
def test_conditional_while
nodes = query_pattern('foo [conditional]', 'while foo; bar; end')
assert_equal 1, nodes.size
assert_equal ruby('foo'), nodes.first
end
def test_conditional_and
nodes = query_pattern('foo [conditional]', 'foo && bar')
assert_equal 1, nodes.size
assert_equal ruby('foo'), nodes.first
end
def test_conditional_or
nodes = query_pattern('foo [conditional]', 'foo || bar')
assert_equal 1, nodes.size
assert_equal ruby('foo'), nodes.first
end
def test_conditional_csend
nodes = query_pattern('foo [conditional]', 'foo&.bar')
assert_equal 1, nodes.size
assert_equal ruby('foo'), nodes.first
end
end
================================================
FILE: test/preprocessor_test.rb
================================================
require_relative "test_helper"
class PreprocessorTest < Minitest::Test
Preprocessor = Querly::Preprocessor
def with_temp_file(content)
Tempfile.create("querly-preprocessor") do |io|
io.write content
io.close
yield Pathname(io.path)
end
end
def test_preprocessing_succeeded
preprocessor = Preprocessor.new(ext: ".foo", command: "cat -n")
target = with_temp_file(<<-EOS) do |path|
foo
bar
EOS
preprocessor.run!(path)
end
assert_equal(<<-EXPECTED, target)
1\tfoo
2\tbar
EXPECTED
end
def test_preprocessing_failed
preprocessor = Preprocessor.new(ext: ".foo", command: "grep XYZ")
assert_raises Preprocessor::Error do
with_temp_file(<<-EOS) do |path|
foo
bar
EOS
preprocessor.run!(path)
end
end
end
end
================================================
FILE: test/querly_test.rb
================================================
require 'test_helper'
class QuerlyTest < Minitest::Test
def test_that_it_has_a_version_number
refute_nil ::Querly::VERSION
end
end
================================================
FILE: test/rule_test.rb
================================================
require_relative "test_helper"
class RuleTest < Minitest::Test
Rule = Querly::Rule
E = Querly::Pattern::Expr
K = Querly::Pattern::Kind
def test_load_rule
rule = Rule.load(
"id" => "foo.bar.baz",
"pattern" => "@",
"message" => "message1"
)
assert_equal "foo.bar.baz", rule.id
assert_equal ["message1"], rule.messages
assert_equal [E::Ivar.new(name: nil)], rule.patterns.map(&:expr)
assert_equal Set.new, rule.tags
assert_equal [], rule.examples
assert_equal [], rule.justifications
end
def test_load_rule3
rule = Rule.load(
"id" => "foo.bar.baz",
"pattern" => ["@", "_"],
"message" => "message1",
"tags" => ["tag1", "tag2"],
"examples" => { "before" => "foo", "after" => "bar"},
"justification" => ["some", "message"]
)
assert_equal "foo.bar.baz", rule.id
assert_equal ["message1"], rule.messages
assert_equal [E::Ivar.new(name: nil), E::Any.new], rule.patterns.map(&:expr)
assert_equal Set.new(["tag1", "tag2"]), rule.tags
assert_equal [Rule::Example.new(before: "foo", after: "bar")], rule.examples
assert_equal ["some", "message"], rule.justifications
end
def test_load_rule2
rule = Rule.load(
"id" => "foo.bar.baz",
"pattern" => ["@", "_"],
"message" => "message1",
"tags" => ["tag1", "tag2"],
"examples" => [{ "before" => "foo", "after" => "bar"},
{ "before" => "foo" },
{ "after" => "bar" }],
"justification" => ["some", "message"]
)
assert_equal "foo.bar.baz", rule.id
assert_equal ["message1"], rule.messages
assert_equal [E::Ivar.new(name: nil), E::Any.new], rule.patterns.map(&:expr)
assert_equal Set.new(["tag1", "tag2"]), rule.tags
assert_equal [Rule::Example.new(before: "foo", after: "bar"),
Rule::Example.new(before: "foo", after: nil),
Rule::Example.new(before: nil, after: "bar")], rule.examples
assert_equal ["some", "message"], rule.justifications
end
def test_load_rule_before_and_after_examples
rule = Rule.load(
"id" => "foo.bar.baz",
"pattern" => ["@", "_"],
"message" => "message1",
"tags" => ["tag1", "tag2"],
"before" => ["foo", "bar"],
"after" => ["baz", "a"],
"justification" => ["some", "message"]
)
assert_equal "foo.bar.baz", rule.id
assert_equal ["message1"], rule.messages
assert_equal [E::Ivar.new(name: nil), E::Any.new], rule.patterns.map(&:expr)
assert_equal Set.new(["tag1", "tag2"]), rule.tags
assert_equal [], rule.examples
assert_equal ["foo", "bar"], rule.before_examples
assert_equal ["baz", "a"], rule.after_examples
assert_equal ["some", "message"], rule.justifications
end
def test_load_rule_raises_on_pattern_syntax_error
exn = assert_raises Rule::PatternSyntaxError do
Rule.load("id" => "id1", "pattern" => "syntax error")
end
assert_match(/Pattern syntax error: rule=id1, index=0, pattern=syntax error, where={}:/, exn.message)
end
def test_load_rule_raises_without_id
exn = assert_raises Rule::InvalidRuleHashError do
Rule.load("pattern" => "_", "message" => "message1")
end
assert_equal "id is missing", exn.message
end
def test_load_rule_raises_without_pattern
exn = assert_raises Rule::InvalidRuleHashError do
Rule.load("id" => "id1", "message" => "hello world")
end
assert_equal "pattern is missing", exn.message
end
def test_load_rule_raises_without_message
exn = assert_raises Rule::InvalidRuleHashError do
Rule.load("id" => "id1", "pattern" => "foobar")
end
assert_equal "message is missing", exn.message
end
def test_load_including_pattern_with_where_clause
rule = Rule.load("id" => "id1", "message" => "message", "pattern" => { 'subject' => "'g()'", 'where' => { 'g' => ["foo", "/bar/"] } })
assert_equal 1, rule.patterns.size
pattern = rule.patterns.first
assert_equal ["foo", /bar/], pattern.expr.name
end
def test_load_rule_raises_exception_on_invalid_example
assert_raises Rule::InvalidRuleHashError do
Rule.load("id" => "id1", "message" => "message", "pattern" => { 'subject' => "'g()'", 'where' => { 'g' => ["foo", "/bar/"] } }, "examples" => [{}])
end
end
def test_translate_where
w = YAML.load(<<-YAML)
- foo
- /bar/
- :baz
- 1
- 2.0
YAML
assert_equal ["foo", /bar/, :baz, 1, 2.0], Rule.translate_where(w)
end
end
================================================
FILE: test/script_enumerator_test.rb
================================================
require_relative "test_helper"
class ScriptEnumeratorTest < Minitest::Test
include TestHelper
ScriptEnumerator = Querly::ScriptEnumerator
Config = Querly::Config
def test_parsing_ruby
mktmpdir do |dir|
config = Config.new(rules: [], preprocessors: {}, root_dir: dir, checks: [])
e = ScriptEnumerator.new(paths: nil, config: config, threads: 1)
ruby_path = dir + "foo.rb"
ruby_path.write <<-EOR
def foo()
end
EOR
e.__send__(:load_script_from_path, ruby_path) do |path, script|
assert_equal ruby_path, path
assert_instance_of Querly::Script, script
end
end
end
def test_parse_error_ruby
mktmpdir do |dir|
config = Config.new(rules: [], preprocessors: {}, root_dir: dir, checks: [])
e = ScriptEnumerator.new(paths: nil, config: config, threads: 1)
ruby_path = dir + "foo.rb"
ruby_path.write <<-EOR
def foo()
EOR
e.__send__(:load_script_from_path, ruby_path) do |path, script|
assert_equal ruby_path, path
assert_instance_of Parser::SyntaxError, script
end
end
end
def test_no_parse_error_on_invalid_utf8_sequence
mktmpdir do |dir|
config = Config.new(rules: [], preprocessors: {}, root_dir: dir, checks: [])
e = ScriptEnumerator.new(paths: nil, config: config, threads: 1)
ruby_path = dir + "foo.rb"
ruby_path.write '"\xFF"'
e.__send__(:load_script_from_path, ruby_path) do |path, script|
assert_equal ruby_path, path
assert_instance_of Querly::Script, script
end
end
end
end
================================================
FILE: test/smoke_test.rb
================================================
require_relative "test_helper"
require "open3"
require "tmpdir"
class SmokeTest < Minitest::Test
include UnificationAssertion
def dirs
@dirs ||= [root]
end
def push_dir(dir)
dirs.push dir
yield
ensure
dirs.pop
end
def sh!(*args, **options)
output, _, status = Open3.capture3(*args, { chdir: dirs.last.to_s }.merge(options))
unless status.success?
raise "Failed: #{args.inspect}"
puts output
end
output
end
def querly_path
Pathname(__dir__) + "../exe/querly"
end
def run_querly(*args, **options)
sh!(*args.unshift(querly_path.to_s), **options)
end
def sh(*args, **options)
Open3.capture3(*args, { chdir: dirs.last.to_s }.merge(options))
end
def root
(Pathname(__dir__) + "../").realpath
end
def test_help
run_querly("help")
end
def test_rules
run_querly("--config=sample.yml", "rules")
end
def test_check
run_querly("--config=sample.yml", "check", ".")
end
def test_test
run_querly("--config=sample.yml", "test")
end
def test_console
run_querly("console", ".", stdin_data: ["help", "reload", "find self.p", "quit"].join("\n"))
end
def test_version
run_querly("version")
end
def test_check_json_format
push_dir root + "test/data/test1" do
output = JSON.parse(run_querly("check", "--format=json", "."), symbolize_names: true)
assert_unifiable({
issues: [
{
script: "script.rb",
location: { start: [1,0], end: [1,8] },
rule: {
id: "test1.rule1",
messages: ["Use foo.bar instead of foobar\n\nfoo.bar is not good.\n"],
justifications: ["Some reason", "Another reason"],
examples: [{ before: "foobar", after: "foobarbaz" }],
}
}
],
errors: []
}, output)
end
end
def test_check_with_rule
push_dir root + "test/data/test1" do
output = JSON.parse(run_querly("check", "--format=json", "--rule=test1.rule1", "."), symbolize_names: true)
assert_unifiable({
issues: [
{
script: "script.rb",
location: { start: [1,0], end: [1,8] },
rule: {
id: "test1.rule1",
messages: ["Use foo.bar instead of foobar\n\nfoo.bar is not good.\n"],
justifications: ["Some reason", "Another reason"],
examples: [{ before: "foobar", after: "foobarbaz" }],
}
}
],
errors: []
}, output)
output = JSON.parse(run_querly("check", "--format=json", "--rule=test1", "."), symbolize_names: true)
assert_unifiable({
issues: [
{
script: "script.rb",
location: { start: [1,0], end: [1,8] },
rule: {
id: "test1.rule1",
messages: ["Use foo.bar instead of foobar\n\nfoo.bar is not good.\n"],
justifications: ["Some reason", "Another reason"],
examples: [{ before: "foobar", after: "foobarbaz" }],
}
}
],
errors: []
}, output)
output = JSON.parse(run_querly("check", "--format=json", "--rule=no_such_rule", "."), symbolize_names: true)
assert_unifiable({
issues: [],
errors: []
}, output)
end
end
def test_check_when_omit_paths
test_dir = root + "test/data/test1"
push_dir test_dir do
output = JSON.parse(run_querly("check", "--format=json"), symbolize_names: true)
assert_unifiable({
issues: [
{
script: (test_dir + "script.rb").cleanpath.to_s,
location: { start: [1,0], end: [1,8] },
rule: {
id: "test1.rule1",
messages: ["Use foo.bar instead of foobar\n\nfoo.bar is not good.\n"],
justifications: ["Some reason", "Another reason"],
examples: [{ before: "foobar", after: "foobarbaz" }],
}
}
],
errors: []
}, output)
end
end
def test_check_json_format_with_not_a_config_file
push_dir root + "test/data/test1" do
out, err, status = sh("bundle", "exec", "querly", "check", "--format=json", "--config=no.such.config", ".")
refute status.success?
assert_match(/Configuration file no.such.config does not look a file./, err)
assert_unifiable({ issues: [], errors: [] }, JSON.parse(out, symbolize_names: true))
end
end
def test_run3
push_dir root + "test/data/test2" do
out, _, status = sh("bundle", "exec", "querly", "check", "--format=json", ".")
assert status.success?
# Syntax error recorded in errors
assert_unifiable({
issues: [],
errors: [{ path: "script.rb", error: :_ }]
}, JSON.parse(out, symbolize_names: true))
end
end
def test_run4
push_dir root + "test/data/test3" do
out, _, status = sh("bundle", "exec", "querly", "check", "--format=json", ".")
assert status.success?
assert_unifiable({
issues: [
{
script: "script.rb",
location: { start: [1, 0], end: [1, 4] },
rule: { id: "test.pppp", messages: :_, justifications: :_, examples: :_ }
},
{
script: "script.rb",
location: { start: [2, 0], end: [2, 5] },
rule: { id: "test.pppp", messages: :_, justifications: :_, examples: :_ }
},
{
script: "script.rb",
location: { start: [3, 0], end: [3, 6] },
rule: { id: "test.pppp", messages: :_, justifications: :_, examples: :_ }
},
],
errors: []
}, JSON.parse(out, symbolize_names: true))
end
end
def test_load_file_not_found
push_dir root + "test/data/test3" do
out, _, status = sh(querly_path.to_s, "check", "--format=json", "not_found.rb")
assert status.success?
assert_unifiable({
issues: [],
errors: [{ path: "not_found.rb", error: :_ }]
}, JSON.parse(out, symbolize_names: true))
end
end
def mktmpdir
tmp = root + "tmp"
tmp.mkdir unless tmp.directory?
Dir.mktmpdir("a", root + "tmp") do |dir|
yield Pathname(dir)
end
end
def test_init
mktmpdir do |path|
push_dir path do
_, _ = run_querly("init")
assert_operator (path + "querly.yml"), :file?
_, _ = run_querly("test", "--config=querly.yml")
end
end
end
def test_check_text_format_when_syntax_error
push_dir root + "test/data/test2" do
out, err, status = sh(querly_path.to_s, "check", "--format=text", ".")
assert status.success?
assert_empty out
assert_equal [
"Failed to load script: script.rb\n",
"script.rb:2:1: error: unexpected token $end\n",
"script.rb:2: \n",
"script.rb:2: \n",
].join, err
end
end
def test_check_new_syntax
push_dir root + "test/data/test4" do
out, err, status = sh(querly_path.to_s, "check", ".")
assert status.success?
assert_empty out
assert_empty err
end
end
end
================================================
FILE: test/test_helper.rb
================================================
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'querly'
require "querly/cli"
require "querly/cli/test"
require 'minitest/autorun'
require "tmpdir"
require "unification_assertion"
require "tempfile"
Rainbow.enabled = false
module TestHelper
E = Querly::Pattern::Expr
A = Querly::Pattern::Argument
K = Querly::Pattern::Kind
def parse_expr(src, where: {})
Querly::Pattern::Parser.parse(src, where: where).expr
end
def parse_kinded(src, where: {})
Querly::Pattern::Parser.parse(src, where: where)
end
def query_pattern(pattern, src, where: {})
pat = parse_kinded(pattern, where: where)
analyzer = Querly::Analyzer.new(config: nil, rule: nil)
analyzer.scripts << Querly::Script.new(path: Pathname("(input)"),
node: Parser::Ruby30.parse(src, "(input)"))
[].tap do |result|
analyzer.find(pat) do |script, pair|
result << pair.node
end
end
end
def ruby(src)
Querly::Script.load(path: "(input)", source: src).node
end
def with_config(hash)
Dir.mktmpdir do |dir|
path = Pathname(dir) + "querly.yml"
path.write(YAML.dump(hash))
yield path
end
end
def stdout
@stdout ||= StringIO.new
end
def mktmpdir
Dir.mktmpdir do |dir|
yield Pathname(dir)
end
end
end
gitextract_rkriiky5/
├── .github/
│ └── workflows/
│ ├── rubocop.yml
│ └── ruby.yml
├── .gitignore
├── .rubocop.yml
├── CHANGELOG.md
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── bin/
│ ├── console
│ └── setup
├── exe/
│ ├── querly
│ └── querly-pp
├── lib/
│ ├── querly/
│ │ ├── analyzer.rb
│ │ ├── check.rb
│ │ ├── cli/
│ │ │ ├── console.rb
│ │ │ ├── find.rb
│ │ │ ├── formatter.rb
│ │ │ ├── rules.rb
│ │ │ └── test.rb
│ │ ├── cli.rb
│ │ ├── concerns/
│ │ │ └── backtrace_formatter.rb
│ │ ├── config.rb
│ │ ├── node_pair.rb
│ │ ├── pattern/
│ │ │ ├── argument.rb
│ │ │ ├── expr.rb
│ │ │ ├── kind.rb
│ │ │ └── parser.y
│ │ ├── pp/
│ │ │ └── cli.rb
│ │ ├── preprocessor.rb
│ │ ├── rule.rb
│ │ ├── rules/
│ │ │ └── sample.rb
│ │ ├── script.rb
│ │ ├── script_enumerator.rb
│ │ └── version.rb
│ └── querly.rb
├── manual/
│ ├── configuration.md
│ ├── examples.md
│ └── patterns.md
├── querly.gemspec
├── rules/
│ └── sample.yml
├── sample.yaml
├── template.yml
└── test/
├── analyzer_test.rb
├── check_test.rb
├── cli/
│ ├── console_test.rb
│ ├── rules_test.rb
│ └── test_test.rb
├── config_test.rb
├── data/
│ ├── test1/
│ │ ├── querly.yml
│ │ └── script.rb
│ ├── test2/
│ │ ├── querly.yml
│ │ └── script.rb
│ ├── test3/
│ │ ├── querly.yml
│ │ └── script.rb
│ └── test4/
│ ├── querly.yml
│ └── script.rb
├── node_pair_test.rb
├── pattern_parser_test.rb
├── pattern_test_test.rb
├── preprocessor_test.rb
├── querly_test.rb
├── rule_test.rb
├── script_enumerator_test.rb
├── smoke_test.rb
└── test_helper.rb
SYMBOL INDEX (432 symbols across 37 files)
FILE: lib/querly.rb
type Querly (line 25) | module Querly
function required_rules (line 28) | def self.required_rules
function load_rule (line 32) | def self.load_rule(*files)
FILE: lib/querly/analyzer.rb
type Querly (line 1) | module Querly
class Analyzer (line 2) | class Analyzer
method initialize (line 7) | def initialize(config:, rule:)
method run (line 16) | def run
method find (line 31) | def find(pattern)
method test_pair (line 41) | def test_pair(node_pair, pattern)
FILE: lib/querly/check.rb
type Querly (line 1) | module Querly
class Check (line 2) | class Check
method apply (line 4) | def apply(current:, all:)
method match? (line 15) | def match?(rule)
method initialize (line 23) | def initialize(pattern:, rules:)
method has_trailing_slash? (line 50) | def has_trailing_slash?
method has_middle_slash? (line 54) | def has_middle_slash?
method load (line 58) | def self.load(hash)
method parse_rule_query (line 82) | def self.parse_rule_query(opr, query)
method match? (line 100) | def match?(path:)
FILE: lib/querly/cli.rb
type Querly (line 8) | module Querly
class CLI (line 9) | class CLI < Thor
method check (line 16) | def check(*paths)
method console (line 70) | def console(*paths)
method find (line 93) | def find(pattern, *paths)
method test (line 109) | def test()
method rules (line 116) | def rules(*ids)
method version (line 122) | def version
method source_root (line 126) | def self.source_root
method init (line 133) | def init()
method config (line 139) | def config(root_option:)
method config_path (line 146) | def config_path
FILE: lib/querly/cli/console.rb
type Querly (line 3) | module Querly
class CLI (line 4) | class CLI
class Console (line 5) | class Console
method initialize (line 15) | def initialize(paths:, history_path:, history_size:, config: nil, ...
method start (line 24) | def start
method reload! (line 41) | def reload!
method analyzer (line 46) | def analyzer
method start_loop (line 64) | def start_loop
method load_history (line 111) | def load_history
method save_history (line 121) | def save_history(line)
method puts_commands (line 129) | def puts_commands
FILE: lib/querly/cli/find.rb
type Querly (line 3) | module Querly
class CLI (line 4) | class CLI
class Find (line 5) | class Find
method initialize (line 13) | def initialize(pattern:, paths:, config: nil, threads:)
method start (line 20) | def start
method pattern (line 48) | def pattern
method analyzer (line 52) | def analyzer
FILE: lib/querly/cli/formatter.rb
type Querly (line 1) | module Querly
class CLI (line 2) | class CLI
type Formatter (line 3) | module Formatter
class Base (line 4) | class Base
method start (line 8) | def start; end
method config_load (line 11) | def config_load(config); end
method config_error (line 15) | def config_error(path, error); end
method script_load (line 18) | def script_load(script); end
method script_error (line 22) | def script_error(path, error); end
method issue_found (line 25) | def issue_found(script, rule, pair); end
method fatal_error (line 29) | def fatal_error(error)
method finish (line 36) | def finish; end
class Text (line 39) | class Text < Base
method config_error (line 40) | def config_error(path, error)
method script_error (line 47) | def script_error(path, error)
method issue_found (line 57) | def issue_found(script, rule, pair)
class JSON (line 68) | class JSON < Base
method initialize (line 69) | def initialize
method config_error (line 76) | def config_error(path, error)
method script_error (line 80) | def script_error(path, error)
method issue_found (line 84) | def issue_found(script, rule, pair)
method finish (line 88) | def finish
method fatal_error (line 92) | def fatal_error(error)
method as_json (line 97) | def as_json
FILE: lib/querly/cli/rules.rb
type Querly (line 1) | module Querly
class CLI (line 2) | class CLI
class Rules (line 3) | class Rules
method initialize (line 8) | def initialize(config_path:, ids:, stdout: STDOUT)
method config (line 14) | def config
method run (line 19) | def run
method test_rule (line 24) | def test_rule(rule)
method rule_to_yaml (line 32) | def rule_to_yaml(rule)
method empty (line 60) | def empty(array)
method singleton (line 66) | def singleton(array)
FILE: lib/querly/cli/test.rb
type Querly (line 1) | module Querly
class CLI (line 2) | class CLI
class Test (line 3) | class Test
method initialize (line 8) | def initialize(config_path:, stdout: STDOUT, stderr: STDERR)
method fail! (line 15) | def fail!
method failed? (line 19) | def failed?
method run (line 23) | def run
method validate_rule_uniqueness (line 44) | def validate_rule_uniqueness(rules)
method validate_rule_patterns (line 61) | def validate_rule_patterns(rules)
method test_pattern (line 138) | def test_pattern(pattern, example, expected:)
method load_config (line 153) | def load_config
method ordinalize (line 160) | def ordinalize(number)
FILE: lib/querly/concerns/backtrace_formatter.rb
type Querly (line 1) | module Querly
type Concerns (line 2) | module Concerns
type BacktraceFormatter (line 3) | module BacktraceFormatter
function format_backtrace (line 4) | def format_backtrace(backtrace, indent: 2)
FILE: lib/querly/config.rb
type Querly (line 1) | module Querly
class Config (line 2) | class Config
method initialize (line 9) | def initialize(rules:, preprocessors:, root_dir:, checks:)
method load (line 17) | def self.load(hash, config_path:, root_dir:, stderr: STDERR)
method all_rules (line 21) | def all_rules
method relative_path_from_root (line 25) | def relative_path_from_root(path)
method rules_for_path (line 29) | def rules_for_path(path)
class Factory (line 44) | class Factory
method initialize (line 50) | def initialize(yaml, config_path:, root_dir:, stderr: STDERR)
method config (line 57) | def config
FILE: lib/querly/node_pair.rb
type Querly (line 1) | module Querly
class NodePair (line 2) | class NodePair
method initialize (line 6) | def initialize(node:, parent: nil)
method children (line 11) | def children
method each_subpair (line 21) | def each_subpair(&block)
FILE: lib/querly/pattern/argument.rb
type Querly (line 1) | module Querly
type Pattern (line 2) | module Pattern
type Argument (line 3) | module Argument
class Base (line 4) | class Base
method initialize (line 7) | def initialize(tail:)
method == (line 11) | def ==(other)
method attributes (line 15) | def attributes
class AnySeq (line 22) | class AnySeq < Base
method initialize (line 23) | def initialize(tail: nil)
class Expr (line 28) | class Expr < Base
method initialize (line 31) | def initialize(expr:, tail:)
class KeyValue (line 37) | class KeyValue < Base
method initialize (line 42) | def initialize(key:, value:, tail:, negated: false)
class BlockPass (line 51) | class BlockPass < Base
method initialize (line 54) | def initialize(expr:)
FILE: lib/querly/pattern/expr.rb
type Querly (line 1) | module Querly
type Pattern (line 2) | module Pattern
type Expr (line 3) | module Expr
class Base (line 4) | class Base
method =~ (line 5) | def =~(pair)
method test_node (line 9) | def test_node(node)
method == (line 13) | def ==(other)
method attributes (line 17) | def attributes
class Any (line 24) | class Any < Base
method test_node (line 25) | def test_node(node)
class Not (line 30) | class Not < Base
method initialize (line 33) | def initialize(pattern:)
method test_node (line 37) | def test_node(node)
class Constant (line 42) | class Constant < Base
method initialize (line 45) | def initialize(path:)
method test_node (line 49) | def test_node(node)
method test_constant (line 57) | def test_constant(node, path)
class Nil (line 76) | class Nil < Base
method test_node (line 77) | def test_node(node)
class Literal (line 82) | class Literal < Base
method initialize (line 86) | def initialize(type:, values: nil)
method with_values (line 91) | def with_values(values)
method test_value (line 95) | def test_value(object)
method test_node (line 103) | def test_node(node)
class Send (line 135) | class Send < Base
method initialize (line 141) | def initialize(receiver:, name:, block:, args: Argument::AnySeq....
method =~ (line 148) | def =~(pair)
method test_name (line 162) | def test_name(node)
method test_node (line 173) | def test_node(node)
method test_receiver (line 188) | def test_receiver(node)
method test_args (line 199) | def test_args(nodes, args)
method hash_node_to_hash (line 251) | def hash_node_to_hash(node)
method test_hash_args (line 262) | def test_hash_args(hash, args)
class ReceiverContext (line 283) | class ReceiverContext < Base
method initialize (line 286) | def initialize(receiver:)
method test_node (line 290) | def test_node(node)
class Self (line 300) | class Self < Base
method test_node (line 301) | def test_node(node)
class Vcall (line 306) | class Vcall < Base
method initialize (line 309) | def initialize(name:)
method =~ (line 313) | def =~(pair)
method test_node (line 328) | def test_node(node)
class Dstr (line 338) | class Dstr < Base
method test_node (line 339) | def test_node(node)
class Ivar (line 344) | class Ivar < Base
method initialize (line 347) | def initialize(name:)
method test_node (line 351) | def test_node(node)
FILE: lib/querly/pattern/kind.rb
type Querly (line 1) | module Querly
type Pattern (line 2) | module Pattern
type Kind (line 3) | module Kind
class Base (line 4) | class Base
method initialize (line 7) | def initialize(expr:)
type Negatable (line 12) | module Negatable
function initialize (line 15) | def initialize(expr:, negated:)
class Any (line 21) | class Any < Base
method test_kind (line 22) | def test_kind(pair)
class Conditional (line 27) | class Conditional < Base
method test_kind (line 30) | def test_kind(pair)
method conditional? (line 34) | def conditional?(pair)
class Discarded (line 55) | class Discarded < Base
method test_kind (line 58) | def test_kind(pair)
method discarded? (line 62) | def discarded?(pair)
FILE: lib/querly/pp/cli.rb
type Querly (line 3) | module Querly
type PP (line 4) | module PP
class CLI (line 5) | class CLI
method initialize (line 15) | def initialize(argv, stdin: STDIN, stdout: STDOUT, stderr: STDERR)
method load_libs (line 33) | def load_libs
method run (line 44) | def run
method run_haml (line 56) | def run_haml
method run_erb (line 74) | def run_erb
FILE: lib/querly/preprocessor.rb
type Querly (line 1) | module Querly
class Preprocessor (line 2) | class Preprocessor
class Error (line 3) | class Error < StandardError
method initialize (line 7) | def initialize(command:, status:)
method initialize (line 16) | def initialize(ext:, command:)
method run! (line 21) | def run!(path)
FILE: lib/querly/rule.rb
type Querly (line 1) | module Querly
class Rule (line 2) | class Rule
class Example (line 3) | class Example
method initialize (line 7) | def initialize(before:, after:)
method == (line 12) | def ==(other)
method initialize (line 28) | def initialize(id:, messages:, patterns:, sources:, tags:, before_ex...
method match? (line 40) | def match?(identifier: nil, tags: nil)
class InvalidRuleHashError (line 56) | class InvalidRuleHashError < StandardError; end
class PatternSyntaxError (line 57) | class PatternSyntaxError < StandardError; end
method load (line 59) | def self.load(hash)
method translate_where (line 113) | def self.translate_where(value)
FILE: lib/querly/script.rb
type Querly (line 1) | module Querly
class Script (line 2) | class Script
method load (line 6) | def self.load(path:, source:)
method initialize (line 16) | def initialize(path:, node:)
method root_pair (line 21) | def root_pair
class Builder (line 25) | class Builder < Parser::Builders::Default
method string_value (line 26) | def string_value(token)
method emit_lambda (line 30) | def emit_lambda
FILE: lib/querly/script_enumerator.rb
type Querly (line 1) | module Querly
class ScriptEnumerator (line 2) | class ScriptEnumerator
method initialize (line 7) | def initialize(paths:, config:, threads:)
method each (line 14) | def each(&block)
method each_path (line 24) | def each_path(&block)
method register_loader (line 40) | def self.register_loader(pattern, loader)
method find_loader (line 44) | def self.find_loader(path)
method load_script_from_path (line 51) | def load_script_from_path(path, &block)
method preprocessors (line 69) | def preprocessors
method enumerate_files_in_dir (line 73) | def enumerate_files_in_dir(path, &block)
FILE: lib/querly/version.rb
type Querly (line 1) | module Querly
FILE: test/analyzer_test.rb
class AnalyzerTest (line 3) | class AnalyzerTest < Minitest::Test
method stderr (line 7) | def stderr
FILE: test/check_test.rb
class CheckTest (line 3) | class CheckTest < Minitest::Test
method root (line 7) | def root
method test_match1 (line 11) | def test_match1
method test_match2 (line 24) | def test_match2
method test_match3 (line 34) | def test_match3
method test_match4 (line 43) | def test_match4
method test_match5 (line 53) | def test_match5
method test_load (line 63) | def test_load
method test_query_match (line 87) | def test_query_match
method test_query_apply (line 103) | def test_query_apply
FILE: test/cli/console_test.rb
class ConsoleTest (line 5) | class ConsoleTest < Minitest::Test
method exe_path (line 8) | def exe_path
method read_for (line 12) | def read_for(read, pattern:)
method test_console (line 38) | def test_console
method test_history_location_override (line 116) | def test_history_location_override
FILE: test/cli/rules_test.rb
class RulesTest (line 4) | class RulesTest < Minitest::Test
method test_rules_command (line 7) | def test_rules_command
FILE: test/cli/test_test.rb
class TestTest (line 3) | class TestTest < Minitest::Test
method setup (line 9) | def setup
method test_load_config_failure (line 14) | def test_load_config_failure
method test_rule_uniqueness (line 27) | def test_rule_uniqueness
method test_rule_patterns_pass (line 53) | def test_rule_patterns_pass
method test_rule_patterns_before_after_fail (line 84) | def test_rule_patterns_before_after_fail
method test_rule_patterns_example_fail (line 118) | def test_rule_patterns_example_fail
method test_rule_patterns_error (line 153) | def test_rule_patterns_error
FILE: test/config_test.rb
class ConfigTest (line 3) | class ConfigTest < Minitest::Test
method stderr (line 9) | def stderr
method test_factory_config_returns_empty_config (line 13) | def test_factory_config_returns_empty_config
method test_factory_config_resturns_config_with_rules (line 22) | def test_factory_config_resturns_config_with_rules
method test_factory_config_prints_warning_on_tagging (line 57) | def test_factory_config_prints_warning_on_tagging
method test_relative_path_from_root (line 63) | def test_relative_path_from_root
method test_loading_rules_from_file (line 76) | def test_loading_rules_from_file
method test_analyzer_rules_for_path (line 99) | def test_analyzer_rules_for_path
FILE: test/data/test4/script.rb
function foo (line 19) | def foo(...)
function foo2 (line 24) | def foo2(a, ...)
function square (line 32) | def square(x) = x * x
FILE: test/node_pair_test.rb
class NodePairTest (line 3) | class NodePairTest < Minitest::Test
method test_each_subpair (line 6) | def test_each_subpair
FILE: test/pattern_parser_test.rb
class PatternParserTest (line 3) | class PatternParserTest < Minitest::Test
method test_parser1 (line 6) | def test_parser1
method test_aaa (line 18) | def test_aaa
method test_ivar (line 22) | def test_ivar
method test_ivar_with_name (line 27) | def test_ivar_with_name
method test_pattern (line 32) | def test_pattern
method test_constant (line 37) | def test_constant
method test_dot3_args (line 42) | def test_dot3_args
method test_keyword_arg (line 51) | def test_keyword_arg
method test_keyword_arg2 (line 62) | def test_keyword_arg2
method test_send_with_block (line 73) | def test_send_with_block
method test_send_without_block (line 81) | def test_send_without_block
method test_send_without_block2 (line 89) | def test_send_without_block2
method test_send_with_block_uident (line 97) | def test_send_with_block_uident
method test_method_names (line 105) | def test_method_names
method test_send (line 111) | def test_send
method test_any_method (line 121) | def test_any_method
method test_method_name (line 145) | def test_method_name
method test_block_pass (line 151) | def test_block_pass
method test_vcall (line 159) | def test_vcall
method test_dstr (line 166) | def test_dstr
method test_any_kinded (line 171) | def test_any_kinded
method test_conditonal_kinded (line 176) | def test_conditonal_kinded
method test_conditional_kinded2 (line 182) | def test_conditional_kinded2
method test_discarded_kinded (line 188) | def test_discarded_kinded
method test_discarded_kinded2 (line 194) | def test_discarded_kinded2
method test_regexp (line 200) | def test_regexp
method test_any_receiver (line 206) | def test_any_receiver
method test_parse_self (line 218) | def test_parse_self
method test_send_with_meta (line 223) | def test_send_with_meta
method test_send_with_missing_meta (line 229) | def test_send_with_missing_meta
method test_as_method (line 235) | def test_as_method
method test_string (line 242) | def test_string
method test_string_literal (line 249) | def test_string_literal
method test_string_literal_with_backslash_escape (line 256) | def test_string_literal_with_backslash_escape
method test_as_something (line 265) | def test_as_something
method test_as_things2 (line 271) | def test_as_things2
FILE: test/pattern_test_test.rb
class PatternTestTest (line 3) | class PatternTestTest < Minitest::Test
method assert_node (line 6) | def assert_node(node, type:)
method test_ivar_without_name (line 12) | def test_ivar_without_name
method test_ivar_with_name (line 20) | def test_ivar_with_name
method test_constant (line 28) | def test_constant
method test_constant_with_parent (line 38) | def test_constant_with_parent
method test_constant_with_parent2 (line 45) | def test_constant_with_parent2
method test_int (line 52) | def test_int
method test_float (line 58) | def test_float
method test_bool (line 64) | def test_bool
method test_symbol (line 70) | def test_symbol
method test_symbol2 (line 77) | def test_symbol2
method test_string (line 85) | def test_string
method test_string2 (line 91) | def test_string2
method test_string3 (line 98) | def test_string3
method test_byte_sequence_string (line 104) | def test_byte_sequence_string
method test_regexp (line 110) | def test_regexp
method test_call_without_args (line 116) | def test_call_without_args
method test_call_with_no_arg (line 123) | def test_call_with_no_arg
method test_call_with_any_args (line 129) | def test_call_with_any_args
method test_call_with_any_expr_arg (line 135) | def test_call_with_any_expr_arg
method test_call_with_not_expr_arg (line 141) | def test_call_with_not_expr_arg
method test_call_with_kw_args1 (line 147) | def test_call_with_kw_args1
method test_call_with_kw_args2 (line 153) | def test_call_with_kw_args2
method test_call_with_kw_args3 (line 158) | def test_call_with_kw_args3
method test_call_with_kw_args4 (line 163) | def test_call_with_kw_args4
method test_call_with_kw_args5 (line 169) | def test_call_with_kw_args5
method test_call_with_kw_args6 (line 175) | def test_call_with_kw_args6
method test_call_with_kw_args_rest1 (line 180) | def test_call_with_kw_args_rest1
method test_call_with_kw_args_rest2 (line 186) | def test_call_with_kw_args_rest2
method test_call_with_negated_kw1 (line 191) | def test_call_with_negated_kw1
method test_call_with_negated_kw2 (line 197) | def test_call_with_negated_kw2
method test_call_with_negated_kw3 (line 203) | def test_call_with_negated_kw3
method test_call_with_negated_kw4 (line 209) | def test_call_with_negated_kw4
method test_call_with_negated_kw5 (line 215) | def test_call_with_negated_kw5
method test_call_with_rest_and_kw (line 221) | def test_call_with_rest_and_kw
method test_call_with_two_dot3 (line 228) | def test_call_with_two_dot3
method test_call_with_block_pass (line 235) | def test_call_with_block_pass
method test_call_with_names (line 241) | def test_call_with_names
method test_call_without_receiver (line 249) | def test_call_without_receiver
method test_call_with_any_receiver (line 254) | def test_call_with_any_receiver
method test_call_any_method (line 260) | def test_call_any_method
method test_call_any_method_with_args (line 266) | def test_call_any_method_with_args
method test_call_any_method_with_block (line 272) | def test_call_any_method_with_block
method test_vcall (line 278) | def test_vcall
method test_vcall2 (line 288) | def test_vcall2
method test_vcall3 (line 295) | def test_vcall3
method test_vcall4 (line 302) | def test_vcall4
method test_dstr (line 308) | def test_dstr
method test_without_block_option (line 314) | def test_without_block_option
method test_with_block (line 319) | def test_with_block
method test_without_block (line 325) | def test_without_block
method test_any_receiver1 (line 331) | def test_any_receiver1
method test_any_receiver2 (line 336) | def test_any_receiver2
method test_any_receiver3 (line 341) | def test_any_receiver3
method test_any_receiver4 (line 346) | def test_any_receiver4
method test_any_receiver5 (line 351) | def test_any_receiver5
method test_any_receiver6 (line 356) | def test_any_receiver6
method test_self (line 361) | def test_self
method test_string_value (line 366) | def test_string_value
method test_conditional_if (line 371) | def test_conditional_if
method test_conditional_while (line 377) | def test_conditional_while
method test_conditional_and (line 383) | def test_conditional_and
method test_conditional_or (line 389) | def test_conditional_or
method test_conditional_csend (line 395) | def test_conditional_csend
FILE: test/preprocessor_test.rb
class PreprocessorTest (line 3) | class PreprocessorTest < Minitest::Test
method with_temp_file (line 6) | def with_temp_file(content)
method test_preprocessing_succeeded (line 14) | def test_preprocessing_succeeded
method test_preprocessing_failed (line 30) | def test_preprocessing_failed
FILE: test/querly_test.rb
class QuerlyTest (line 3) | class QuerlyTest < Minitest::Test
method test_that_it_has_a_version_number (line 4) | def test_that_it_has_a_version_number
FILE: test/rule_test.rb
class RuleTest (line 3) | class RuleTest < Minitest::Test
method test_load_rule (line 8) | def test_load_rule
method test_load_rule3 (line 23) | def test_load_rule3
method test_load_rule2 (line 41) | def test_load_rule2
method test_load_rule_before_and_after_examples (line 63) | def test_load_rule_before_and_after_examples
method test_load_rule_raises_on_pattern_syntax_error (line 84) | def test_load_rule_raises_on_pattern_syntax_error
method test_load_rule_raises_without_id (line 92) | def test_load_rule_raises_without_id
method test_load_rule_raises_without_pattern (line 100) | def test_load_rule_raises_without_pattern
method test_load_rule_raises_without_message (line 108) | def test_load_rule_raises_without_message
method test_load_including_pattern_with_where_clause (line 116) | def test_load_including_pattern_with_where_clause
method test_load_rule_raises_exception_on_invalid_example (line 124) | def test_load_rule_raises_exception_on_invalid_example
method test_translate_where (line 130) | def test_translate_where
FILE: test/script_enumerator_test.rb
class ScriptEnumeratorTest (line 3) | class ScriptEnumeratorTest < Minitest::Test
method test_parsing_ruby (line 9) | def test_parsing_ruby
method test_parse_error_ruby (line 27) | def test_parse_error_ruby
method test_no_parse_error_on_invalid_utf8_sequence (line 44) | def test_no_parse_error_on_invalid_utf8_sequence
FILE: test/smoke_test.rb
class SmokeTest (line 6) | class SmokeTest < Minitest::Test
method dirs (line 9) | def dirs
method push_dir (line 13) | def push_dir(dir)
method sh! (line 20) | def sh!(*args, **options)
method querly_path (line 31) | def querly_path
method run_querly (line 35) | def run_querly(*args, **options)
method sh (line 39) | def sh(*args, **options)
method root (line 43) | def root
method test_help (line 47) | def test_help
method test_rules (line 51) | def test_rules
method test_check (line 55) | def test_check
method test_test (line 59) | def test_test
method test_console (line 63) | def test_console
method test_version (line 67) | def test_version
method test_check_json_format (line 71) | def test_check_json_format
method test_check_with_rule (line 92) | def test_check_with_rule
method test_check_when_omit_paths (line 136) | def test_check_when_omit_paths
method test_check_json_format_with_not_a_config_file (line 158) | def test_check_json_format_with_not_a_config_file
method test_run3 (line 168) | def test_run3
method test_run4 (line 182) | def test_run4
method test_load_file_not_found (line 210) | def test_load_file_not_found
method mktmpdir (line 222) | def mktmpdir
method test_init (line 230) | def test_init
method test_check_text_format_when_syntax_error (line 240) | def test_check_text_format_when_syntax_error
method test_check_new_syntax (line 255) | def test_check_new_syntax
FILE: test/test_helper.rb
type TestHelper (line 13) | module TestHelper
function parse_expr (line 18) | def parse_expr(src, where: {})
function parse_kinded (line 22) | def parse_kinded(src, where: {})
function query_pattern (line 26) | def query_pattern(pattern, src, where: {})
function ruby (line 40) | def ruby(src)
function with_config (line 44) | def with_config(hash)
function stdout (line 52) | def stdout
function mktmpdir (line 56) | def mktmpdir
Condensed preview — 66 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (158K chars).
[
{
"path": ".github/workflows/rubocop.yml",
"chars": 296,
"preview": "name: RuboCop\n\non: pull_request\n\njobs:\n rubocop:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v2\n"
},
{
"path": ".github/workflows/ruby.yml",
"chars": 392,
"preview": "name: Ruby\n\non:\n push:\n branches:\n - master\n pull_request: {}\n\njobs:\n test:\n runs-on: ubuntu-latest\n st"
},
{
"path": ".gitignore",
"chars": 169,
"preview": "/.bundle/\n/.yardoc\n/_yardoc/\n/coverage/\n/doc/\n/pkg/\n/spec/reports/\n/tmp/\n/.idea\n/parser.output\n/lib/querly/pattern/parse"
},
{
"path": ".rubocop.yml",
"chars": 131,
"preview": "require:\n - rubocop-rubycw\n\nAllCops:\n DisabledByDefault: true\n Exclude:\n - test/data/**/*.rb\n\nRubycw/Rubycw:\n Ena"
},
{
"path": "CHANGELOG.md",
"chars": 4463,
"preview": "# Change Log\n\n## master\n\n## 1.3.0 (2021-07-05)\n\n* Require Ruby 2.7 or 3.0 by @yubiquitous ([#88](https://github.com/sout"
},
{
"path": "Gemfile",
"chars": 91,
"preview": "source 'https://rubygems.org'\n\n# Specify your gem's dependencies in querly.gemspec\ngemspec\n"
},
{
"path": "LICENSE",
"chars": 1084,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2017 Soutaro Matsumoto\n\nPermission is hereby granted, free of charge, to any person"
},
{
"path": "README.md",
"chars": 5249,
"preview": "\n\n# Querly - Pattern Based Che"
},
{
"path": "Rakefile",
"chars": 371,
"preview": "require \"bundler/gem_tasks\"\nrequire \"rake/testtask\"\n\nRake::TestTask.new(:test) do |t|\n t.libs << \"test\"\n t.libs << \"li"
},
{
"path": "bin/console",
"chars": 331,
"preview": "#!/usr/bin/env ruby\n\nrequire \"bundler/setup\"\nrequire \"querly\"\n\n# You can add fixtures and/or initialization code here to"
},
{
"path": "bin/setup",
"chars": 96,
"preview": "#!/usr/bin/env bash\nset -euo pipefail\nIFS=$'\\n\\t'\nset -vx\n\nbundle install\nbundle exec rake racc\n"
},
{
"path": "exe/querly",
"chars": 128,
"preview": "#!/usr/bin/env ruby\n\n$LOAD_PATH << File.join(__dir__, \"../lib\")\n\nrequire \"querly\"\nrequire \"querly/cli\"\n\nQuerly::CLI.star"
},
{
"path": "exe/querly-pp",
"chars": 120,
"preview": "#!/usr/bin/env ruby\n\n$LOAD_PATH << File.join(__dir__, \"../lib\")\n\nrequire \"querly/pp/cli\"\n\nQuerly::PP::CLI.new(ARGV).run\n"
},
{
"path": "lib/querly/analyzer.rb",
"chars": 1041,
"preview": "module Querly\n class Analyzer\n attr_reader :config\n attr_reader :scripts\n attr_reader :rule\n\n def initializ"
},
{
"path": "lib/querly/check.rb",
"chars": 2492,
"preview": "module Querly\n class Check\n Query = Struct.new(:opr, :tags, :identifier) do\n def apply(current:, all:)\n "
},
{
"path": "lib/querly/cli/console.rb",
"chars": 3426,
"preview": "require 'readline'\n\nmodule Querly\n class CLI\n class Console\n include Concerns::BacktraceFormatter\n\n attr_r"
},
{
"path": "lib/querly/cli/find.rb",
"chars": 1813,
"preview": "# frozen_string_literal: true\n\nmodule Querly\n class CLI\n class Find\n include Concerns::BacktraceFormatter\n\n "
},
{
"path": "lib/querly/cli/formatter.rb",
"chars": 4408,
"preview": "module Querly\n class CLI\n module Formatter\n class Base\n include Concerns::BacktraceFormatter\n\n # "
},
{
"path": "lib/querly/cli/rules.rb",
"chars": 1697,
"preview": "module Querly\n class CLI\n class Rules\n attr_reader :config_path\n attr_reader :stdout\n attr_reader :id"
},
{
"path": "lib/querly/cli/test.rb",
"chars": 5266,
"preview": "module Querly\n class CLI\n class Test\n attr_reader :config_path\n attr_reader :stdout\n attr_reader :std"
},
{
"path": "lib/querly/cli.rb",
"chars": 4383,
"preview": "require \"thor\"\nrequire \"json\"\n\nif ENV[\"NO_COLOR\"]\n Rainbow.enabled = false\nend\n\nmodule Querly\n class CLI < Thor\n de"
},
{
"path": "lib/querly/concerns/backtrace_formatter.rb",
"chars": 194,
"preview": "module Querly\n module Concerns\n module BacktraceFormatter\n def format_backtrace(backtrace, indent: 2)\n b"
},
{
"path": "lib/querly/config.rb",
"chars": 2786,
"preview": "module Querly\n class Config\n attr_reader :rules\n attr_reader :preprocessors\n attr_reader :root_dir\n attr_re"
},
{
"path": "lib/querly/node_pair.rb",
"chars": 627,
"preview": "module Querly\n class NodePair\n attr_reader :node\n attr_reader :parent\n\n def initialize(node:, parent: nil)\n "
},
{
"path": "lib/querly/pattern/argument.rb",
"chars": 1191,
"preview": "module Querly\n module Pattern\n module Argument\n class Base\n attr_reader :tail\n\n def initialize(ta"
},
{
"path": "lib/querly/pattern/expr.rb",
"chars": 8796,
"preview": "module Querly\n module Pattern\n module Expr\n class Base\n def =~(pair)\n test_node(pair.node)\n "
},
{
"path": "lib/querly/pattern/kind.rb",
"chars": 1603,
"preview": "module Querly\n module Pattern\n module Kind\n class Base\n attr_reader :expr\n\n def initialize(expr:)"
},
{
"path": "lib/querly/pattern/parser.y",
"chars": 7778,
"preview": "class Querly::Pattern::Parser\nprechigh\n nonassoc EXCLAMATION\n nonassoc LPAREN\n left DOT\npreclow\n\nrule\n\ntarget: kinded"
},
{
"path": "lib/querly/pp/cli.rb",
"chars": 2425,
"preview": "require \"optparse\"\n\nmodule Querly\n module PP\n class CLI\n attr_reader :argv\n attr_reader :command\n att"
},
{
"path": "lib/querly/preprocessor.rb",
"chars": 777,
"preview": "module Querly\n class Preprocessor\n class Error < StandardError\n attr_reader :command\n attr_reader :status\n"
},
{
"path": "lib/querly/rule.rb",
"chars": 3462,
"preview": "module Querly\n class Rule\n class Example\n attr_reader :before\n attr_reader :after\n\n def initialize(be"
},
{
"path": "lib/querly/rules/sample.rb",
"chars": 65,
"preview": "Querly.load_rule File.join(__dir__, \"../../../rules/sample.yml\")\n"
},
{
"path": "lib/querly/script.rb",
"chars": 744,
"preview": "module Querly\n class Script\n attr_reader :path\n attr_reader :node\n\n def self.load(path:, source:)\n parser"
},
{
"path": "lib/querly/script_enumerator.rb",
"chars": 2781,
"preview": "module Querly\n class ScriptEnumerator\n attr_reader :paths\n attr_reader :config\n attr_reader :threads\n\n def "
},
{
"path": "lib/querly/version.rb",
"chars": 38,
"preview": "module Querly\n VERSION = \"1.3.0\"\nend\n"
},
{
"path": "lib/querly.rb",
"chars": 880,
"preview": "require 'pathname'\nrequire \"yaml\"\nrequire \"rainbow\"\nrequire \"parser/ruby30\"\nrequire \"set\"\nrequire \"open3\"\nrequire \"activ"
},
{
"path": "manual/configuration.md",
"chars": 2436,
"preview": "# Overview\n\nThe configuration file, default name is `querly.yml`, will look like the following.\n\n```yml\nrules:\n ...\npre"
},
{
"path": "manual/examples.md",
"chars": 1641,
"preview": "In this page, I will show some rules I have written. They are all real rules from my repos.\n\n# `js: true` option with fe"
},
{
"path": "manual/patterns.md",
"chars": 5379,
"preview": "# Syntax\n\n## Toplevel\n\n* *expr*\n* *expr* `[` *kind* `]` (kinded expr)\n* *expr* `[!` *kind* `]` (negated kinded expr)\n\n##"
},
{
"path": "querly.gemspec",
"chars": 1733,
"preview": "# coding: utf-8\nlib = File.expand_path('../lib', __FILE__)\n$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)\nrequi"
},
{
"path": "rules/sample.yml",
"chars": 815,
"preview": "- id: sample.ruby.pathname\n pattern: Pathname.new\n message: |\n Did you mean `Pathname` method?\n\n- id: sample.ruby.f"
},
{
"path": "sample.yaml",
"chars": 4895,
"preview": "rules:\n - id: sample.delete_all\n pattern:\n - delete_all\n - update_all\n message: |\n Validations and"
},
{
"path": "template.yml",
"chars": 1855,
"preview": "rules:\n - id: sample.debug_print\n pattern:\n - self.p\n - self.pp\n message: Delete debug print\n exampl"
},
{
"path": "test/analyzer_test.rb",
"chars": 177,
"preview": "require_relative \"test_helper\"\n\nclass AnalyzerTest < Minitest::Test\n Analyzer = Querly::Analyzer\n Config = Querly::Con"
},
{
"path": "test/check_test.rb",
"chars": 4402,
"preview": "require_relative \"test_helper\"\n\nclass CheckTest < Minitest::Test\n Check = Querly::Check\n Rule = Querly::Rule\n\n def ro"
},
{
"path": "test/cli/console_test.rb",
"chars": 3849,
"preview": "require_relative \"../test_helper\"\nrequire \"querly/cli/console\"\nrequire \"pty\"\n\nclass ConsoleTest < Minitest::Test\n inclu"
},
{
"path": "test/cli/rules_test.rb",
"chars": 689,
"preview": "require_relative \"../test_helper\"\nrequire \"querly/cli/rules\"\n\nclass RulesTest < Minitest::Test\n include TestHelper\n\n d"
},
{
"path": "test/cli/test_test.rb",
"chars": 4822,
"preview": "require_relative \"../test_helper\"\n\nclass TestTest < Minitest::Test\n Test = Querly::CLI::Test\n Config = Querly::Config\n"
},
{
"path": "test/config_test.rb",
"chars": 4359,
"preview": "require_relative \"test_helper\"\n\nclass ConfigTest < Minitest::Test\n include TestHelper\n\n Config = Querly::Config\n Prep"
},
{
"path": "test/data/test1/querly.yml",
"chars": 258,
"preview": "rules:\n - id: test1.rule1\n pattern:\n - foobar\n message: |\n Use foo.bar instead of foobar\n\n foo.bar"
},
{
"path": "test/data/test1/script.rb",
"chars": 9,
"preview": "foobar()\n"
},
{
"path": "test/data/test2/querly.yml",
"chars": 98,
"preview": "rules:\n - id: test2.rule1\n pattern:\n - foobar\n message: Use foo.bar instead of foobar\n"
},
{
"path": "test/data/test2/script.rb",
"chars": 3,
"preview": "1+\n"
},
{
"path": "test/data/test3/querly.yml",
"chars": 111,
"preview": "rules:\n - id: test.pppp\n message: pp...\n pattern:\n subject: \"'p(...)\"\n where:\n p: /p+/\n"
},
{
"path": "test/data/test3/script.rb",
"chars": 18,
"preview": "p(1)\npp(2)\nppp(3)\n"
},
{
"path": "test/data/test4/querly.yml",
"chars": 81,
"preview": "rules:\n - id: test_rule\n message: This is a test message\n pattern: _.test\n"
},
{
"path": "test/data/test4/script.rb",
"chars": 529,
"preview": "array = [1, 2, 3]\n\n# Endless range since Ruby 2.6\narray[1..]\n\n# Beginless range since Ruby 2.7\narray[..1]\n\n# Numbered bl"
},
{
"path": "test/node_pair_test.rb",
"chars": 409,
"preview": "require_relative \"test_helper\"\n\nclass NodePairTest < Minitest::Test\n include TestHelper\n\n def test_each_subpair\n no"
},
{
"path": "test/pattern_parser_test.rb",
"chars": 8116,
"preview": "require_relative \"test_helper\"\n\nclass PatternParserTest < Minitest::Test\n include TestHelper\n\n def test_parser1\n pa"
},
{
"path": "test/pattern_test_test.rb",
"chars": 11477,
"preview": "require_relative \"test_helper\"\n\nclass PatternTestTest < Minitest::Test\n include TestHelper\n\n def assert_node(node, typ"
},
{
"path": "test/preprocessor_test.rb",
"chars": 821,
"preview": "require_relative \"test_helper\"\n\nclass PreprocessorTest < Minitest::Test\n Preprocessor = Querly::Preprocessor\n\n def wit"
},
{
"path": "test/querly_test.rb",
"chars": 140,
"preview": "require 'test_helper'\n\nclass QuerlyTest < Minitest::Test\n def test_that_it_has_a_version_number\n refute_nil ::Querly"
},
{
"path": "test/rule_test.rb",
"chars": 4512,
"preview": "require_relative \"test_helper\"\n\nclass RuleTest < Minitest::Test\n Rule = Querly::Rule\n E = Querly::Pattern::Expr\n K = "
},
{
"path": "test/script_enumerator_test.rb",
"chars": 1594,
"preview": "require_relative \"test_helper\"\n\nclass ScriptEnumeratorTest < Minitest::Test\n include TestHelper\n\n ScriptEnumerator = Q"
},
{
"path": "test/smoke_test.rb",
"chars": 8600,
"preview": "require_relative \"test_helper\"\n\nrequire \"open3\"\nrequire \"tmpdir\"\n\nclass SmokeTest < Minitest::Test\n include Unification"
},
{
"path": "test/test_helper.rb",
"chars": 1348,
"preview": "$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)\nrequire 'querly'\nrequire \"querly/cli\"\nrequire \"querly/cli/tes"
}
]
About this extraction
This page contains the full source code of the soutaro/querly GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 66 files (143.3 KB), approximately 39.3k tokens, and a symbol index with 432 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.