Full Code of soutaro/querly for AI

master 9a44873ed766 cached
66 files
143.3 KB
39.3k tokens
432 symbols
1 requests
Download .txt
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 logo](https://github.com/soutaro/querly/blob/master/logo/Querly%20horizontal.png)

# Querly - Pattern Based Checking Tool for Ruby

![Ruby](https://github.com/soutaro/querly/workflows/Ruby/badge.svg)

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
Download .txt
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
Download .txt
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": "![Querly logo](https://github.com/soutaro/querly/blob/master/logo/Querly%20horizontal.png)\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.

Copied to clipboard!