[
  {
    "path": ".github/workflows/rubocop.yml",
    "content": "name: RuboCop\n\non: pull_request\n\njobs:\n  rubocop:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v2\n    - uses: ruby/setup-ruby@v1\n      with:\n        ruby-version: \"3.0\"\n    - run: gem install rubocop rubocop-rubycw\n    - name: Run RuboCop\n      run: rubocop --format github\n"
  },
  {
    "path": ".github/workflows/ruby.yml",
    "content": "name: Ruby\n\non:\n  push:\n    branches:\n      - master\n  pull_request: {}\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        ruby: [\"2.7\", \"3.0\", head]\n    steps:\n    - uses: actions/checkout@v2\n    - uses: ruby/setup-ruby@v1\n      with:\n        ruby-version: ${{ matrix.ruby }}\n        bundler-cache: true\n    - run: bin/setup\n    - run: bundle exec rake build test\n"
  },
  {
    "path": ".gitignore",
    "content": "/.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/parser.rb\nparser.output\n/Gemfile.lock\n.querly_history\n"
  },
  {
    "path": ".rubocop.yml",
    "content": "require:\n  - rubocop-rubycw\n\nAllCops:\n  DisabledByDefault: true\n  Exclude:\n    - test/data/**/*.rb\n\nRubycw/Rubycw:\n  Enabled: true\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# 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/soutaro/querly/pull/88))\n* Use 3.0 compatible parser by @yubiquitous ([#88](https://github.com/soutaro/querly/pull/88))\n\n## 1.2.0 (2020-12-15)\n\n* Relax Thor version requirements by @y-yagi ([#85](https://github.com/soutaro/querly/pull/85))\n* Fix ERB comment preprocessing by @mallowlabs ([#84](https://github.com/soutaro/querly/pull/84))\n* Better error message for Ruby code syntax error by @ybiquitous ([#83](https://github.com/soutaro/querly/pull/83))\n\n## 1.1.0 (2020-05-17)\n\n* Fix invalid bytes sequence in UTF-8 error by @mallowlabs [#75](https://github.com/soutaro/querly/pull/75)\n* Detect safe navigation operator as a method call by @pocke [#71](https://github.com/soutaro/querly/pull/71)\n\n## 1.0.0 (2019-7-19)\n\n* Add `--config` option for `find` and `console` [#67](https://github.com/soutaro/querly/pull/67)\n* Improve preprocessor performance by processing concurrently [#68](https://github.com/soutaro/querly/pull/68)\n\n## 0.16.0 (2019-04-23)\n\n* Support string literal pattern (@pocke) [#64](https://github.com/soutaro/querly/pull/64)\n* Allow underscore method name pattern (@pocke) [#63](https://github.com/soutaro/querly/pull/63)\n* Add erb support (@hanachin) [#61](https://github.com/soutaro/querly/pull/61)\n* Add `exit` command on console (@wata727) [#59](https://github.com/soutaro/querly/pull/59)\n\n## 0.15.1 (2019-03-12)\n\n* Relax parser version requirement\n\n## 0.15.0 (2019-02-13)\n\n* Fix broken `querly init` template (@ybiquitous) #56\n* Relax `activesupport` requirement (@y-yagi) #57\n\n## 0.14.0 (2019-01-22)\n\n* Allow having `...` pattens anywhere positional argument patterns are valid #54\n* Add `querly find` command (@gfx) #49\n\n## 0.13.0 (2018-08-27)\n\n* Make history file location configurable through `QUERLY_HOME` (defaults to `~/.querly`)\n* Save `console` history (@gfx) #47\n\n## 0.12.0 (2018-08-03)\n\n* Declare MIT license #44\n* Make reading backtrace easier in `console` command (@pocke) #43\n* Highlight matched expression in querly console (@pocke) #42\n* Set exit status = 1 when `querly.yaml` has syntax error (@pocke) #41\n* Fix typos (@koic, @vzvu3k6k) #40, #39\n\n## 0.11.0 (2018-04-22)\n\n* Relax `rainbow` version requirement\n\n## 0.10.0 (2018-04-13)\n\n* Update parser (@yoshoku) #38\n* Use Ruby25 parser\n\n## 0.9.0 (2018-03-02)\n\n* Fix literal testing (@pocke) #37\n\n## 0.8.4 (2018-02-11)\n\n* Loosen the restriction of `thor` version (@shinnn) #36\n\n## 0.8.3 (2018-01-16)\n\n* Fix preprocessor to avoid deadlocking #35\n\n## 0.8.2 (2018-01-13)\n\n* Move `Concerns::BacktraceFormatter` under `Querly`  (@kohtaro24) #34\n\n## 0.8.1 (2017-12-22)\n\n* Update dependencies\n\n## 0.8.0 (2017-12-19)\n\n* Make `[conditional]` be aware of safe-navigation-operator (@pocke) #30\n* Make preprocessors be aware of `bundle exec`.\n  When `querly` is invoked with `bundle exec`, so are preprocessors, and vice vesa.\n* Add `--rule` option for `querly check` to filter rules to test\n* Print rule id in text output\n\n## 0.7.0 (2017-08-22)\n\n* Add Wiki pages to repository in manual directory #25\n* Add named literal pattern `:string: as 'name` with `where: { name: [\"alice\", /bob/] }` #24\n* Add `init` command #28\n\n## 0.6.0 (2017-06-27)\n\n* Load current directory when no path is given (@wata727) #18\n* Require Active Support ~> 5.0 (@gfx) #17\n* Print error message if HAML 5.0 is loaded (@pocke) #16\n\n## 0.5.0 (2017-06-16)\n\n* Exit 1 on test failure #9\n* Fix example index printing in test (@pocke) #8, #10\n* Introduce pattern matching on method name by set of string and regexp\n* Rule definitions in config can have more structured `examples` attribute\n\n## 0.4.0 (2017-05-25)\n\n* Update `parser` to 2.4 compatible version\n* Check more pathnames which looks like Ruby by default (@pocke) #7\n\n## 0.3.1 (2017-02-16)\n\n* Allow `require` rules from config file\n* Add `version` command\n* Fix *with block* and *without block* pattern parsing\n* Prettier backtrace printing\n* Prettier pattern syntax error message\n\n## 0.2.1 (2016-11-24)\n\n* Fix `self` pattern matching\n\n## 0.2.0 (2016-11-24)\n\n* Remove `tagging` section from config\n* Add `check` section to select rules to check\n* Add `import` section to load rules from other file\n* Add `querly rules` sub command to print loaded rules\n* Add *with block* and *without block* pattern (`foo() {}` / `foo() !{}`)\n* Add *some of receiver chain* pattern (`...`)\n* Fix keyword args pattern matching bug\n\n## 0.1.0\n\n* First release.\n"
  },
  {
    "path": "Gemfile",
    "content": "source 'https://rubygems.org'\n\n# Specify your gem's dependencies in querly.gemspec\ngemspec\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2017 Soutaro Matsumoto\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "![Querly logo](https://github.com/soutaro/querly/blob/master/logo/Querly%20horizontal.png)\n\n# Querly - Pattern Based Checking Tool for Ruby\n\n![Ruby](https://github.com/soutaro/querly/workflows/Ruby/badge.svg)\n\nQuerly is a query language and tool to find out method calls from Ruby programs.\nDefine rules to check your program with patterns to find out *bad* pieces.\nQuerly finds out matching pieces from your program.\n\n## Overview\n\nYour project may have many local rules:\n\n* 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)\n* Should not use `root_url` without `locale:` parameter\n* Should not use `Net::HTTP` for Web API calls, but use `HTTPClient`\n\nThese local rule violations will be found during code review.\nReviewers will ask commiter to revise; commiter will fix; fine.\nReally?\nIt is boring and time-consuming.\nWe need some automation!\n\nHowever, that rules cannot be the standard.\nThey make sense only in your project.\nOkay, start writing a plug-in for RuboCop? (or other checking tools)\n\nInstead of writing RuboCop plug-in, just define a Querly rule in a few lines of YAML.\n\n```yml\nrules:\n  - id: my_project.use_faster_email_update\n    pattern: update_mail\n    message: When updating Customer#email, newly written code should use 30x faster Customer.update_all_email\n    justification:\n      - When you are editing old code (it should be refactored...)\n      - You are sure updating only small number of customers, and performance does not matter\n\n  - id: my_project.root_url_without_locale\n    pattern: \"root_url(!locale: _)\"\n    message: Links to top page should be with locale parameter\n\n  - id: my_project.net_http\n    pattern: Net::HTTP\n    message: Use HTTPClient to make HTTP request\n```\n\nWrite down your local rules, and let Querly check conformance with them.\nFocus on spec, design, UX, and other important things during code review!\n\n## Installation\n\nInstall via RubyGems.\n\n    $ gem install querly\n\nOr you can put it in your Gemfile.\n\n```rb\ngem 'querly'\n```\n\n## Quick Start\n\nCopy the following YAML and paste as `querly.yml` in your project's repo.\n\n```yaml\nrules:\n  - id: sample.debug_print\n    pattern:\n      - self.p\n      - self.pp\n    message: Delete debug print\n```\n\nRun `querly` in the repo.\n\n```\n$ querly check .\n```\n\nIf your code contains `p` or `pp` calls, querly will print warning messages.\n\n```\n./app/models/account.rb:44:10                  p(account.id)      Delete debug print\n./app/controllers/accounts_controller.rb:17:2  pp params: params  Delete debug print\n```\n\n## Configuration\n\nSee the following manual for configuration and query language reference.\n\n* [Configuration](https://github.com/soutaro/querly/blob/master/manual/configuration.md)\n* [Patterns](https://github.com/soutaro/querly/blob/master/manual/patterns.md)\n\nUse `querly console` command to test patterns interactively.\n\n## Requiring Rules\n\n`import` section in config file now allows accepts `require` command.\n\n```yaml\nimport:\n  - require: querly/rules/sample\n  - require: your_library/querly/rules\n```\n\nQuerly ships with `querly/rules/sample` rule set. Check `lib/querly/rules/sample.rb` and `rules/sample.yml` for detail.\n\n### Publishing Gems with Querly Rules\n\nQuerly provides `Querly.load_rule` API to allow publishing your rules as part of Ruby library.\nPut rules YAML file in your gem, and add Ruby script in some directory like `lib/your_library/querly/rules.rb`.\n\n```\nQuerly.load_rules File.join(__dir__, relative_path_to_yaml_file)\n```\n\n## Notes\n\n### Querly's analysis is syntactic\n\nThe analysis is currently purely syntactic:\n\n```rb\nrecord.save(validate: false)\n```\n\nand\n\n```rb\nx = false\nrecord.save(validate: x)\n```\n\nwill yield different results.\nThis can be improved by doing very primitive data flow analysis, and I'm planning to do that.\n\n### Too many false positives!\n\nThe analysis itself does not have very good precision.\nThere will be many false positives, and *querly warning free code* does not make much sense.\n\n* TODO: support to ignore warnings through magic comments in code\n\nQuerly is not to ensure *there is nothing wrong in the code*, but just tells you *code fragments you should review with special care*.\nI believe it still improves your software development productivity.\n\n### Incoming updates?\n\nThe following is the list of updates which would make sense.\n\n* Support for importing rule sets, and provide some good default rules\n* Support for ignoring warnings\n* Improve analysis precision by intra procedural data flow analysis\n\n## Development\n\nAfter 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.\n\nTo 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).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/soutaro/querly.\n\n"
  },
  {
    "path": "Rakefile",
    "content": "require \"bundler/gem_tasks\"\nrequire \"rake/testtask\"\n\nRake::TestTask.new(:test) do |t|\n  t.libs << \"test\"\n  t.libs << \"lib\"\n  t.test_files = FileList['test/**/*_test.rb']\nend\n\ntask :default => :test\ntask :build => :racc\ntask :test => :racc\n\nrule %r/\\.rb/ => \".y\" do |t|\n  sh \"racc\", \"-v\", \"-o\", \"#{t.name}\", \"#{t.source}\"\nend\n\ntask :racc => \"lib/querly/pattern/parser.rb\"\n"
  },
  {
    "path": "bin/console",
    "content": "#!/usr/bin/env ruby\n\nrequire \"bundler/setup\"\nrequire \"querly\"\n\n# You can add fixtures and/or initialization code here to make experimenting\n# with your gem easier. You can also use a different console, if you like.\n\n# (If you use this, don't forget to add pry to your Gemfile!)\n# require \"pry\"\n# Pry.start\n\nrequire \"irb\"\nIRB.start\n"
  },
  {
    "path": "bin/setup",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\nIFS=$'\\n\\t'\nset -vx\n\nbundle install\nbundle exec rake racc\n"
  },
  {
    "path": "exe/querly",
    "content": "#!/usr/bin/env ruby\n\n$LOAD_PATH << File.join(__dir__, \"../lib\")\n\nrequire \"querly\"\nrequire \"querly/cli\"\n\nQuerly::CLI.start(ARGV)\n"
  },
  {
    "path": "exe/querly-pp",
    "content": "#!/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",
    "content": "module Querly\n  class Analyzer\n    attr_reader :config\n    attr_reader :scripts\n    attr_reader :rule\n\n    def initialize(config:, rule:)\n      @config = config\n      @scripts = []\n      @rule = rule\n    end\n\n    #\n    # yields(script, rule, node_pair)\n    #\n    def run\n      scripts.each do |script|\n        rules = config.rules_for_path(script.path)\n        script.root_pair.each_subpair do |node_pair|\n          rules.each do |rule|\n            if rule.match?(identifier: self.rule)\n              if rule.patterns.any? {|pattern| test_pair(node_pair, pattern) }\n                yield script, rule, node_pair\n              end\n            end\n          end\n        end\n      end\n    end\n\n    def find(pattern)\n      scripts.each do |script|\n        script.root_pair.each_subpair do |node_pair|\n          if test_pair(node_pair, pattern)\n            yield script, node_pair\n          end\n        end\n      end\n    end\n\n    def test_pair(node_pair, pattern)\n      pattern.expr =~ node_pair && pattern.test_kind(node_pair)\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/check.rb",
    "content": "module Querly\n  class Check\n    Query = Struct.new(:opr, :tags, :identifier) do\n      def apply(current:, all:)\n        case opr\n        when :append\n          current.union(all.select {|rule| match?(rule) })\n        when :except\n          current.reject {|rule| match?(rule) }.to_set\n        when :only\n          all.select {|rule| match?(rule) }.to_set\n        end\n      end\n\n      def match?(rule)\n        rule.match?(identifier: identifier, tags: tags)\n      end\n    end\n\n    attr_reader :patterns\n    attr_reader :rules\n\n    def initialize(pattern:, rules:)\n      @rules = rules\n\n      @has_trailing_slash = pattern.end_with?(\"/\")\n      @has_middle_slash = /\\/./ =~ pattern\n\n      @patterns = []\n\n      pattern.sub!(/\\A\\//, '')\n\n      case\n      when has_trailing_slash? && has_middle_slash?\n        patterns << File.join(pattern, \"**\")\n      when has_trailing_slash?\n        patterns << File.join(pattern, \"**\")\n        patterns << File.join(\"**\", pattern, \"**\")\n      when has_middle_slash?\n        patterns << pattern\n        patterns << File.join(pattern, \"**\")\n      else\n        patterns << pattern\n        patterns << File.join(\"**\", pattern)\n        patterns << File.join(pattern, \"**\")\n        patterns << File.join(\"**\", pattern, \"**\")\n      end\n    end\n\n    def has_trailing_slash?\n      @has_trailing_slash\n    end\n\n    def has_middle_slash?\n      @has_middle_slash\n    end\n\n    def self.load(hash)\n      pattern = hash[\"path\"]\n\n      rules = Array(hash[\"rules\"]).map do |rule|\n        case rule\n        when String\n          parse_rule_query(:append, rule)\n        when Hash\n          case\n          when rule[\"append\"]\n            parse_rule_query(:append, rule[\"append\"])\n          when rule[\"except\"]\n            parse_rule_query(:except, rule[\"except\"])\n          when rule[\"only\"]\n            parse_rule_query(:only, rule[\"only\"])\n          else\n            parse_rule_query(:append, rule)\n          end\n        end\n      end\n\n      self.new(pattern: pattern, rules: rules)\n    end\n\n    def self.parse_rule_query(opr, query)\n      case query\n      when String\n        Query.new(opr, nil, query)\n      when Hash\n        if query['tags']\n          ts = query['tags']\n          if ts.is_a?(String)\n            ts = ts.split\n          end\n          tags = Set.new(ts)\n        end\n        identifier = query['id']\n\n        Query.new(opr, tags, identifier)\n      end\n    end\n\n    def match?(path:)\n      patterns.any? {|pat| File.fnmatch?(pat, path.to_s) }\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/cli/console.rb",
    "content": "require 'readline'\n\nmodule Querly\n  class CLI\n    class Console\n      include Concerns::BacktraceFormatter\n\n      attr_reader :paths\n      attr_reader :history_path\n      attr_reader :history_size\n      attr_reader :config\n      attr_reader :history\n      attr_reader :threads\n\n      def initialize(paths:, history_path:, history_size:, config: nil, threads:)\n        @paths = paths\n        @history_path = history_path\n        @history_size = history_size\n        @config = config\n        @history = []\n        @threads = threads\n      end\n\n      def start\n        puts <<-Message\nQuerly #{VERSION}, interactive console\n\n        Message\n\n        puts_commands\n\n        STDOUT.print \"Loading...\"\n        STDOUT.flush\n        reload!\n        STDOUT.puts \" ready!\"\n\n        load_history\n        start_loop\n      end\n\n      def reload!\n        @analyzer = nil\n        analyzer\n      end\n\n      def analyzer\n        return @analyzer if @analyzer\n\n        @analyzer = Analyzer.new(config: config, rule: nil)\n\n        ScriptEnumerator.new(paths: paths, config: config, threads: threads).each do |path, script|\n          case script\n          when Script\n            @analyzer.scripts << script\n          when StandardError\n            p path: path, script: script.inspect\n            puts script.backtrace\n          end\n        end\n\n        @analyzer\n      end\n\n      def start_loop\n        while line = Readline.readline(\"> \", true)\n          case line\n          when \"quit\", \"exit\"\n            exit\n          when \"reload!\"\n            STDOUT.print \"reloading...\"\n            STDOUT.flush\n            reload!\n            STDOUT.puts \" done\"\n          when /^find (.+)/\n            begin\n              pattern = Pattern::Parser.parse($1, where: {})\n\n              count = 0\n\n              analyzer.find(pattern) do |script, pair|\n                path = script.path.to_s\n                line_no = pair.node.loc.first_line\n                range = pair.node.loc.expression\n                start_col = range.column\n                end_col = range.last_column\n\n                src = range.source_buffer.source_lines[line_no-1]\n                src = Rainbow(src[0...start_col]).blue +\n                  Rainbow(src[start_col...end_col]).bright.blue.bold +\n                  Rainbow(src[end_col..-1]).blue\n\n                puts \"  #{path}:#{line_no}:#{start_col}\\t#{src}\"\n\n                count += 1\n              end\n\n              puts \"#{count} results\"\n\n              save_history line\n            rescue => exn\n              STDOUT.puts Rainbow(\"Error: #{exn}\").red\n              STDOUT.puts \"Backtrace:\"\n              STDOUT.puts format_backtrace(exn.backtrace)\n            end\n          else\n            puts_commands\n          end\n        end\n      end\n\n      def load_history\n        history_path.readlines.each do |line|\n          line.chomp!\n          Readline::HISTORY.push(line)\n          history.push line\n        end\n      rescue Errno::ENOENT\n        # in the first time\n      end\n\n      def save_history(line)\n        history.push line\n        if history.size > history_size\n          @history = history.drop(history.size - history_size)\n        end\n        history_path.write(history.join(\"\\n\") + \"\\n\")\n      end\n\n      def puts_commands\n        puts <<-Message\nCommands:\n  - find PATTERN   Find PATTERN from given paths\n  - reload!        Reload program from paths\n  - quit\n\n        Message\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/cli/find.rb",
    "content": "# frozen_string_literal: true\n\nmodule Querly\n  class CLI\n    class Find\n      include Concerns::BacktraceFormatter\n\n      attr_reader :pattern_str\n      attr_reader :paths\n      attr_reader :config\n      attr_reader :threads\n\n      def initialize(pattern:, paths:, config: nil, threads:)\n        @pattern_str = pattern\n        @paths = paths\n        @config = config\n        @threads = threads\n      end\n\n      def start\n        count = 0\n\n        analyzer.find(pattern) do |script, pair|\n          path = script.path.to_s\n          line_no = pair.node.loc.first_line\n          range = pair.node.loc.expression\n          start_col = range.column\n          end_col = range.last_column\n\n          src = range.source_buffer.source_lines[line_no-1]\n          src = Rainbow(src[0...start_col]).blue +\n            Rainbow(src[start_col...end_col]).bright.blue.bold +\n            Rainbow(src[end_col..-1]).blue\n\n          puts \"  #{path}:#{line_no}:#{start_col}\\t#{src}\"\n\n          count += 1\n        end\n\n        puts \"#{count} results\"\n      rescue => exn\n        STDOUT.puts Rainbow(\"Error: #{exn}\").red\n        STDOUT.puts \"pattern: #{pattern_str}\"\n        STDOUT.puts \"Backtrace:\"\n        STDOUT.puts format_backtrace(exn.backtrace)\n      end\n\n      def pattern\n        Pattern::Parser.parse(pattern_str, where: {})\n      end\n\n      def analyzer\n        return @analyzer if @analyzer\n\n        @analyzer = Analyzer.new(config: config, rule: nil)\n\n        ScriptEnumerator.new(paths: paths, config: config, threads: threads).each do |path, script|\n          case script\n          when Script\n            @analyzer.scripts << script\n          when StandardError\n            p path: path, script: script.inspect\n            puts script.backtrace\n          end\n        end\n\n        @analyzer\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/cli/formatter.rb",
    "content": "module Querly\n  class CLI\n    module Formatter\n      class Base\n        include Concerns::BacktraceFormatter\n\n        # Called when analyzer started\n        def start; end\n\n        # Called when config is successfully loaded\n        def config_load(config); end\n\n        # Called when failed to load config\n        # Exit(status == 0) after the call\n        def config_error(path, error); end\n\n        # Called when script is successfully loaded\n        def script_load(script); end\n\n        # Called when failed to load script\n        # Continue after the call\n        def script_error(path, error); end\n\n        # Called when issue is found\n        def issue_found(script, rule, pair); end\n\n        # Called on other error\n        # Abort(status != 0) after the call\n        def fatal_error(error)\n          STDERR.puts Rainbow(\"Fatal error: #{error}\").red\n          STDERR.puts \"Backtrace:\"\n          STDERR.puts format_backtrace(error.backtrace)\n        end\n\n        # Called on exit/abort\n        def finish; end\n      end\n\n      class Text < Base\n        def config_error(path, error)\n          STDERR.puts Rainbow(\"Failed to load configuration: #{path}\").red\n          STDERR.puts error\n          STDERR.puts \"Backtrace:\"\n          STDERR.puts format_backtrace(error.backtrace)\n        end\n\n        def script_error(path, error)\n          STDERR.puts Rainbow(\"Failed to load script: #{path}\").red\n\n          if error.is_a? Parser::SyntaxError\n            STDERR.puts error.diagnostic.render\n          else\n            STDERR.puts error.inspect\n          end\n        end\n\n        def issue_found(script, rule, pair)\n          path = script.path.to_s\n          src = Rainbow(pair.node.loc.expression.source.split(/\\n/).first).red\n          line = pair.node.loc.first_line\n          col = pair.node.loc.column\n          message = rule.messages.first.split(/\\n/).first\n\n          STDOUT.puts \"#{path}:#{line}:#{col}\\t#{src}\\t#{message} (#{rule.id})\"\n        end\n      end\n\n      class JSON < Base\n        def initialize\n          @issues = []\n          @script_errors = []\n          @config_errors = []\n          @fatal = nil\n        end\n\n        def config_error(path, error)\n          @config_errors << [path, error]\n        end\n\n        def script_error(path, error)\n          @script_errors << [path, error]\n        end\n\n        def issue_found(script, rule, pair)\n          @issues << [script, rule, pair]\n        end\n\n        def finish\n          STDOUT.print as_json.to_json\n        end\n\n        def fatal_error(error)\n          super\n          @fatal = error\n        end\n\n        def as_json\n          case\n          when @fatal\n            # Fatal error found\n            {\n              fatal_error: {\n                message: @fatal.inspect,\n                backtrace: @fatal.backtrace\n              }\n            }\n          when !@config_errors.empty?\n            # Error found during config load\n            {\n              config_errors: @config_errors.map {|(path, error)|\n                {\n                  path: path.to_s,\n                  error: {\n                    message: error.inspect,\n                    backtrace: error.backtrace\n                  }\n                }\n              }\n            }\n          else\n            # Successfully checked\n            {\n              issues: @issues.map {|(script, rule, pair)|\n                {\n                  script: script.path.to_s,\n                  rule: {\n                    id: rule.id,\n                    messages: rule.messages,\n                    justifications: rule.justifications,\n                    examples: rule.examples.map {|example|\n                      {\n                        before: example.before,\n                        after: example.after\n                      }\n                    }\n                  },\n                  location: {\n                    start: [pair.node.loc.first_line, pair.node.loc.column],\n                    end: [pair.node.loc.last_line, pair.node.loc.last_column]\n                  }\n                }\n              },\n              errors: @script_errors.map {|path, error|\n                {\n                  path: path.to_s,\n                  error: {\n                    message: error.inspect,\n                    backtrace: error.backtrace\n                  }\n                }\n              }\n            }\n          end\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/cli/rules.rb",
    "content": "module Querly\n  class CLI\n    class Rules\n      attr_reader :config_path\n      attr_reader :stdout\n      attr_reader :ids\n\n      def initialize(config_path:, ids:, stdout: STDOUT)\n        @config_path = config_path\n        @stdout = stdout\n        @ids = ids\n      end\n\n      def config\n        yaml = YAML.load(config_path.read)\n        @config ||= Config.load(yaml, config_path: config_path, root_dir: config_path.parent.realpath)\n      end\n\n      def run\n        rules = config.rules.select {|rule| test_rule(rule) }\n        stdout.puts YAML.dump(rules.map {|rule| rule_to_yaml(rule) })\n      end\n\n      def test_rule(rule)\n        if ids.empty?\n          true\n        else\n          ids.any? {|id| rule.match?(identifier: id) }\n        end\n      end\n\n      def rule_to_yaml(rule)\n        { \"id\" => rule.id }.tap do |hash|\n          singleton rule.sources do |a|\n            hash[\"pattern\"] = a\n          end\n\n          singleton rule.messages do |a|\n            hash[\"message\"] = a\n          end\n\n          empty rule.tags do |a|\n            hash[\"tags\"] = a\n          end\n\n          singleton rule.justifications do |a|\n            hash[\"justification\"] = a\n          end\n\n          singleton rule.before_examples do |a|\n            hash[\"before\"] = a\n          end\n\n          singleton rule.after_examples do |a|\n            hash[\"after\"] = a\n          end\n        end\n      end\n\n      def empty(array)\n        unless array.empty?\n          yield array.to_a\n        end\n      end\n\n      def singleton(array)\n        empty(array) do\n          if array.length == 1\n            yield array.first\n          else\n            yield array.to_a\n          end\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/cli/test.rb",
    "content": "module Querly\n  class CLI\n    class Test\n      attr_reader :config_path\n      attr_reader :stdout\n      attr_reader :stderr\n\n      def initialize(config_path:, stdout: STDOUT, stderr: STDERR)\n        @config_path = config_path\n        @stdout = stdout\n        @stderr = stderr\n        @success = true\n      end\n\n      def fail!\n        @success = false\n      end\n\n      def failed?\n        !@success\n      end\n\n      def run\n        config = load_config\n\n        unless config\n          stdout.puts \"There is nothing to test at #{config_path} ...\"\n          stdout.puts \"Make a configuration and run test again!\"\n          return 1\n        end\n\n        validate_rule_uniqueness(config.rules)\n        validate_rule_patterns(config.rules)\n\n        failed? ? 1 : 0\n      rescue => exn\n        stderr.puts Rainbow(\"Fatal error:\").red\n        stderr.puts exn.inspect\n        stderr.puts exn.backtrace.map {|x| \"  \" + x }.join(\"\\n\")\n\n        1\n      end\n\n      def validate_rule_uniqueness(rules)\n        ids = Set.new\n\n        stdout.puts \"Checking rule id uniqueness...\"\n\n        duplications = 0\n\n        rules.each do |rule|\n          unless ids.add?(rule.id)\n            stdout.puts Rainbow(\"  Rule id #{rule.id} duplicated!\").red\n            duplications += 1\n          end\n        end\n\n        fail! unless duplications == 0\n      end\n\n      def validate_rule_patterns(rules)\n        stdout.puts \"Checking rule patterns...\"\n\n        tests = 0\n        false_positives = 0\n        false_negatives = 0\n        errors = 0\n\n        rules.each do |rule|\n          rule.before_examples.each.with_index(1) do |example, example_index|\n            tests += 1\n\n            begin\n              unless rule.patterns.any? {|pat| test_pattern(pat, example, expected: true) }\n                stdout.puts(Rainbow(\"  #{rule.id}\").red + \":\\t#{ordinalize example_index} *before* example didn't match with any pattern\")\n                false_negatives += 1\n              end\n            rescue Parser::SyntaxError\n              errors += 1\n              stdout.puts(Rainbow(\"  #{rule.id}\").red + \":\\tParsing failed for #{ordinalize example_index} *before* example\")\n            end\n          end\n\n          rule.after_examples.each.with_index(1) do |example, example_index|\n            tests += 1\n\n            begin\n              unless rule.patterns.all? {|pat| test_pattern(pat, example, expected: false) }\n                stdout.puts(Rainbow(\"  #{rule.id}\").red + \":\\t#{ordinalize example_index} *after* example matched with some of patterns\")\n                false_positives += 1\n              end\n            rescue Parser::SyntaxError\n              errors += 1\n              stdout.puts(Rainbow(\"  #{rule.id}\") + \":\\tParsing failed for #{ordinalize example_index} *after* example\")\n            end\n          end\n\n          rule.examples.each.with_index(1) do |example, index|\n            if example.before\n              tests += 1\n              begin\n                unless rule.patterns.any? {|pat| test_pattern(pat, example.before, expected: true) }\n                  stdout.puts(Rainbow(\"  #{rule.id}\").red + \":\\tbefore of #{ordinalize index} example didn't match with any pattern\")\n                  false_negatives += 1\n                end\n              rescue Parser::SyntaxError\n                errors += 1\n                stdout.puts(Rainbow(\"  #{rule.id}\").red + \":\\tParsing failed on before of #{ordinalize index} example\")\n              end\n            end\n\n            if example.after\n              tests += 1\n              begin\n                unless rule.patterns.all? {|pat| test_pattern(pat, example.after, expected: false) }\n                  stdout.puts(Rainbow(\"  #{rule.id}\").red + \":\\tafter of #{ordinalize index} example matched with some of patterns\")\n                  false_positives += 1\n                end\n              rescue Parser::SyntaxError\n                errors += 1\n                stdout.puts(Rainbow(\"  #{rule.id}\") + \":\\tParsing failed on after of #{ordinalize index} example\")\n              end\n            end\n          end\n        end\n\n        stdout.puts \"Tested #{rules.size} rules with #{tests} tests.\"\n        if false_positives > 0 || false_negatives > 0 || errors > 0\n          stdout.puts \"  #{false_positives} examples found which should not match, but matched\"\n          stdout.puts \"  #{false_negatives} examples found which should match, but didn't\"\n          stdout.puts \"  #{errors} examples raised error\"\n          fail!\n        else\n          stdout.puts Rainbow(\"  All tests green!\").green\n        end\n      end\n\n      def test_pattern(pattern, example, expected:)\n        analyzer = Analyzer.new(config: nil, rule: nil)\n\n        found = false\n\n        node = Parser::Ruby30.parse(example)\n        NodePair.new(node: node).each_subpair do |pair|\n          if analyzer.test_pair(pair, pattern)\n            found = true\n          end\n        end\n\n        found == expected\n      end\n\n      def load_config\n        if config_path.file?\n          yaml = YAML.load(config_path.read)\n          Config.load(yaml, config_path: config_path, root_dir: config_path.parent.realpath, stderr: STDERR)\n        end\n      end\n\n      def ordinalize(number)\n        ActiveSupport::Inflector.ordinalize(number)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/cli.rb",
    "content": "require \"thor\"\nrequire \"json\"\n\nif ENV[\"NO_COLOR\"]\n  Rainbow.enabled = false\nend\n\nmodule Querly\n  class CLI < Thor\n    desc \"check [paths]\", \"Check paths based on configuration\"\n    option :config, default: \"querly.yml\"\n    option :root\n    option :format, default: \"text\", type: :string, enum: %w(text json)\n    option :rule, type: :string\n    option :threads, default: Parallel.processor_count, type: :numeric\n    def check(*paths)\n      require 'querly/cli/formatter'\n\n      formatter = case options[:format]\n                  when \"text\"\n                    Formatter::Text.new\n                  when \"json\"\n                    Formatter::JSON.new\n                  end\n      formatter.start\n\n      threads = Integer(options[:threads])\n\n      begin\n        unless config_path.file?\n          STDERR.puts <<-Message\nConfiguration file #{config_path} does not look a file.\nSpecify configuration file by --config option.\n          Message\n          exit 1\n        end\n\n        begin\n          config = config(root_option: options[:root])\n        rescue => exn\n          formatter.config_error config_path, exn\n        end\n\n        analyzer = Analyzer.new(config: config, rule: options[:rule])\n\n        ScriptEnumerator.new(paths: paths.empty? ? [Pathname.pwd] : paths.map {|path| Pathname(path) }, config: config, threads: threads).each do |path, script|\n          case script\n          when Script\n            analyzer.scripts << script\n            formatter.script_load script\n          when StandardError, LoadError\n            formatter.script_error path, script\n          end\n        end\n\n        analyzer.run do |script, rule, pair|\n          formatter.issue_found script, rule, pair\n        end\n      rescue => exn\n        formatter.fatal_error exn\n        exit 1\n      ensure\n        formatter.finish\n      end\n    end\n\n    desc \"console [paths]\", \"Start console for given paths\"\n    option :config, default: \"querly.yml\"\n    option :threads, default: Parallel.processor_count, type: :numeric\n    def console(*paths)\n      require 'querly/cli/console'\n      home_path = if (path = ENV[\"QUERLY_HOME\"])\n                       Pathname(path)\n                     else\n                       Pathname(Dir.home) + \".querly\"\n                     end\n      home_path.mkdir unless home_path.exist?\n      config = config_path.file? ? config(root_option: nil) : nil\n      threads = Integer(options[:threads])\n\n      Console.new(\n        paths: paths.empty? ? [Pathname.pwd] : paths.map {|path| Pathname(path) },\n        history_path: home_path + \"history\",\n        history_size: ENV[\"QUERLY_HISTORY_SIZE\"]&.to_i || 1_000_000,\n        config: config,\n        threads: threads\n      ).start\n    end\n\n    desc \"find pattern [paths]\", \"Find for the pattern in given paths\"\n    option :config, default: \"querly.yml\"\n    option :threads, default: Parallel.processor_count, type: :numeric\n    def find(pattern, *paths)\n      require 'querly/cli/find'\n\n      config = config_path.file? ? config(root_option: nil) : nil\n      threads = Integer(options[:threads])\n\n      Find.new(\n        pattern: pattern,\n        paths: paths.empty? ? [Pathname.pwd] : paths.map {|path| Pathname(path) },\n        config: config,\n        threads: threads\n      ).start\n    end\n\n    desc \"test\", \"Check configuration\"\n    option :config, default: \"querly.yml\"\n    def test()\n      require \"querly/cli/test\"\n      exit Test.new(config_path: config_path).run\n    end\n\n    desc \"rules\", \"Print loaded rules\"\n    option :config, default: \"querly.yml\"\n    def rules(*ids)\n      require \"querly/cli/rules\"\n      Rules.new(config_path: config_path, ids: ids).run\n    end\n\n    desc \"version\", \"Print version\"\n    def version\n      puts \"Querly #{VERSION}\"\n    end\n\n    def self.source_root\n      File.join(__dir__, \"../..\")\n    end\n\n    include Thor::Actions\n\n    desc \"init\", \"Generate Querly config file (querly.yml)\"\n    def init()\n      copy_file(\"template.yml\", \"querly.yml\")\n    end\n\n    private\n\n    def config(root_option:)\n      root_path = root_option ? Pathname(root_option).realpath : config_path.parent.realpath\n\n      yaml = YAML.load(config_path.read)\n      Config.load(yaml, config_path: config_path, root_dir: root_path, stderr: STDERR)\n    end\n\n    def config_path\n      [Pathname(options[:config]),\n       Pathname(\"querly.yaml\")].compact.find(&:file?) || Pathname(options[:config])\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/concerns/backtrace_formatter.rb",
    "content": "module Querly\n  module Concerns\n    module BacktraceFormatter\n      def format_backtrace(backtrace, indent: 2)\n        backtrace.map {|x| \" \"*indent + x }.join(\"\\n\")\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/config.rb",
    "content": "module Querly\n  class Config\n    attr_reader :rules\n    attr_reader :preprocessors\n    attr_reader :root_dir\n    attr_reader :checks\n    attr_reader :rules_cache\n\n    def initialize(rules:, preprocessors:, root_dir:, checks:)\n      @rules = rules\n      @root_dir = root_dir\n      @preprocessors = preprocessors\n      @checks = checks\n      @rules_cache = {}\n    end\n\n    def self.load(hash, config_path:, root_dir:, stderr: STDERR)\n      Factory.new(hash, config_path: config_path, root_dir: root_dir, stderr: stderr).config\n    end\n\n    def all_rules\n      @all_rules ||= Set.new(rules)\n    end\n\n    def relative_path_from_root(path)\n      path.absolute? ? path.relative_path_from(root_dir) : path.cleanpath\n    end\n\n    def rules_for_path(path)\n      relative_path = relative_path_from_root(path)\n      matching_checks = checks.select {|check| check.match?(path: relative_path) }\n\n      if rules_cache.key?(matching_checks)\n        rules_cache[matching_checks]\n      else\n        matching_checks.flat_map(&:rules).inject(all_rules) do |rules, query|\n          query.apply(current: rules, all: all_rules)\n        end.tap do |rules|\n          rules_cache[matching_checks] = rules\n        end\n      end\n    end\n\n    class Factory\n      attr_reader :yaml\n      attr_reader :root_dir\n      attr_reader :stderr\n      attr_reader :config_path\n\n      def initialize(yaml, config_path:, root_dir:, stderr: STDERR)\n        @yaml = yaml\n        @config_path = config_path\n        @root_dir = root_dir\n        @stderr = stderr\n      end\n\n      def config\n        if yaml[\"tagging\"]\n          stderr.puts \"tagging is deprecated and ignored\"\n        end\n\n        rules = Array(yaml[\"rules\"]).map {|hash| Rule.load(hash) }\n        preprocessors = (yaml[\"preprocessor\"] || {}).each.with_object({}) do |(key, value), hash|\n          hash[key] = Preprocessor.new(ext: key, command: value)\n        end\n\n        imports = Array(yaml[\"import\"])\n        imports.each do |import|\n          if import[\"load\"]\n            load_pattern = Pathname(import[\"load\"])\n            load_pattern = config_path.parent + load_pattern if load_pattern.relative?\n\n            Pathname.glob(load_pattern.to_s) do |path|\n              stderr.puts \"Loading rules from #{path}...\"\n              YAML.load(path.read).each do |hash|\n                rules << Rule.load(hash)\n              end\n            end\n          end\n\n          if import[\"require\"]\n            stderr.puts \"Require rules from #{import[\"require\"]}...\"\n            require import[\"require\"]\n          end\n        end\n\n        rules.concat Querly.required_rules\n\n        checks = Array(yaml[\"check\"]).map {|hash| Check.load(hash) }\n\n        Config.new(rules: rules, preprocessors: preprocessors, checks: checks, root_dir: root_dir)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/node_pair.rb",
    "content": "module Querly\n  class NodePair\n    attr_reader :node\n    attr_reader :parent\n\n    def initialize(node:, parent: nil)\n      @node = node\n      @parent = parent\n    end\n\n    def children\n      node.children.flat_map do |child|\n        if child.is_a?(Parser::AST::Node)\n          self.class.new(node: child, parent: self)\n        else\n          []\n        end\n      end\n    end\n\n    def each_subpair(&block)\n      if block_given?\n        return unless node\n\n        yield self\n\n        children.each do |child|\n          child.each_subpair(&block)\n        end\n      else\n        enum_for :each_subpair\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/pattern/argument.rb",
    "content": "module Querly\n  module Pattern\n    module Argument\n      class Base\n        attr_reader :tail\n\n        def initialize(tail:)\n          @tail = tail\n        end\n\n        def ==(other)\n          other.class == self.class && other.attributes == attributes\n        end\n\n        def attributes\n          instance_variables.each.with_object({}) do |name, hash|\n            hash[name] = instance_variable_get(name)\n          end\n        end\n      end\n\n      class AnySeq < Base\n        def initialize(tail: nil)\n          super(tail: tail)\n        end\n      end\n\n      class Expr < Base\n        attr_reader :expr\n\n        def initialize(expr:, tail:)\n          @expr = expr\n          super(tail: tail)\n        end\n      end\n\n      class KeyValue < Base\n        attr_reader :key\n        attr_reader :value\n        attr_reader :negated\n\n        def initialize(key:, value:, tail:, negated: false)\n          @key = key\n          @value = value\n          @negated = negated\n\n          super(tail: tail)\n        end\n      end\n\n      class BlockPass < Base\n        attr_reader :expr\n\n        def initialize(expr:)\n          @expr = expr\n          super(tail: nil)\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/pattern/expr.rb",
    "content": "module Querly\n  module Pattern\n    module Expr\n      class Base\n        def =~(pair)\n          test_node(pair.node)\n        end\n\n        def test_node(node)\n          false\n        end\n\n        def ==(other)\n          other.class == self.class && other.attributes == attributes\n        end\n\n        def attributes\n          instance_variables.each.with_object({}) do |name, hash|\n            hash[name] = instance_variable_get(name)\n          end\n        end\n      end\n\n      class Any < Base\n        def test_node(node)\n          !!node\n        end\n      end\n\n      class Not < Base\n        attr_reader :pattern\n\n        def initialize(pattern:)\n          @pattern = pattern\n        end\n\n        def test_node(node)\n          !pattern.test_node(node)\n        end\n      end\n\n      class Constant < Base\n        attr_reader :path\n\n        def initialize(path:)\n          @path = path\n        end\n\n        def test_node(node)\n          if path\n            test_constant node, path\n          else\n            node&.type == :const\n          end\n        end\n\n        def test_constant(node, path)\n          if node\n            case node.type\n            when :const\n              parent = node.children[0]\n              name = node.children[1]\n\n              if name == path.last\n                path.count == 1 || test_constant(parent, path.take(path.count - 1))\n              end\n            when :cbase\n              path.empty?\n            end\n          else\n            path.empty?\n          end\n        end\n      end\n\n      class Nil < Base\n        def test_node(node)\n          node&.type == :nil\n        end\n      end\n\n      class Literal < Base\n        attr_reader :type\n        attr_reader :values\n\n        def initialize(type:, values: nil)\n          @type = type\n          @values = values ? Array(values) : nil\n        end\n\n        def with_values(values)\n          self.class.new(type: type, values: values)\n        end\n\n        def test_value(object)\n          if values\n            values.any? {|value| value === object }\n          else\n            true\n          end\n        end\n\n        def test_node(node)\n          case node&.type\n          when :int\n            return false unless type == :int || type == :number\n            test_value(node.children.first)\n\n          when :float\n            return false unless type == :float || type == :number\n            test_value(node.children.first)\n\n          when :true\n            type == :bool && (values == nil || values == [true])\n\n          when :false\n            type == :bool && (values == nil || values == [false])\n\n          when :str\n            return false unless type == :string\n            test_value(node.children.first.scrub)\n\n          when :sym\n            return false unless type == :symbol\n            test_value(node.children.first)\n\n          when :regexp\n            return false unless type == :regexp\n            test_value(node.children.first)\n\n          end\n        end\n      end\n\n      class Send < Base\n        attr_reader :name\n        attr_reader :receiver\n        attr_reader :args\n        attr_reader :block\n\n        def initialize(receiver:, name:, block:, args: Argument::AnySeq.new)\n          @name = Array(name)\n          @receiver = receiver\n          @args = args\n          @block = block\n        end\n\n        def =~(pair)\n          # Skip send node with block\n          type = pair.node.type\n          if (type == :send || type == :csend) && pair.parent\n            if pair.parent.node.type == :block\n              if pair.parent.node.children.first.equal? pair.node\n                return false\n              end\n            end\n          end\n\n          test_node pair.node\n        end\n\n        def test_name(node)\n          name.map do |n|\n            case n\n            when String\n              n.to_sym\n            else\n              n\n            end\n          end.any? {|n| n === node.children[1] }\n        end\n\n        def test_node(node)\n          return false if block == true && node.type != :block\n          return false if block == false && node.type == :block\n\n          node = node.children.first if node&.type == :block\n\n          case node&.type\n          when :send, :csend\n            return false unless test_name(node)\n            return false unless test_receiver(node.children[0])\n            return false unless test_args(node.children.drop(2), args)\n            true\n          end\n        end\n\n        def test_receiver(node)\n          case receiver\n          when Self\n            !node || receiver.test_node(node)\n          when nil\n            true\n          else\n            receiver.test_node(node)\n          end\n        end\n\n        def test_args(nodes, args)\n          first_node = nodes.first\n\n          case args\n          when Argument::AnySeq\n            case args.tail\n            when Argument::KeyValue\n              if first_node\n                case\n                when nodes.last.type == :kwsplat\n                  true\n                when nodes.last.type == :hash && args.tail.is_a?(Argument::KeyValue)\n                  hash = hash_node_to_hash(nodes.last)\n                  test_hash_args(hash, args.tail)\n                else\n                  test_hash_args({}, args.tail)\n                end\n              else\n                test_hash_args({}, args.tail)\n              end\n            when Argument::Expr\n              nodes.size.times.any? do |i|\n                test_args(nodes.drop(i), args.tail)\n              end\n            else\n              true\n            end\n          when Argument::Expr\n            if first_node\n              args.expr.test_node(nodes.first) && test_args(nodes.drop(1), args.tail)\n            end\n          when Argument::KeyValue\n            if first_node\n              types = nodes.map(&:type)\n              if types == [:hash]\n                hash = hash_node_to_hash(nodes.first)\n                test_hash_args(hash, args)\n              elsif types == [:hash, :kwsplat]\n                true\n              else\n                args.negated\n              end\n            else\n              test_hash_args({}, args)\n            end\n          when Argument::BlockPass\n            first_node&.type == :block_pass && args.expr.test_node(first_node.children.first)\n          when nil\n            nodes.empty?\n          end\n        end\n\n        def hash_node_to_hash(node)\n          node.children.each.with_object({}) do |pair, h|\n            key = pair.children[0]\n            value = pair.children[1]\n\n            if key.type == :sym\n              h[key.children[0]] = value\n            end\n          end\n        end\n\n        def test_hash_args(hash, args)\n          while args\n            if args.is_a?(Argument::KeyValue)\n              node = hash[args.key]\n\n              if !args.negated == !!(node && args.value.test_node(node))\n                hash.delete args.key\n              else\n                return false\n              end\n            else\n              break\n            end\n\n            args = args.tail\n          end\n\n          args.is_a?(Argument::AnySeq) || hash.empty?\n        end\n      end\n\n      class ReceiverContext < Base\n        attr_reader :receiver\n\n        def initialize(receiver:)\n          @receiver = receiver\n        end\n\n        def test_node(node)\n          if receiver.test_node(node)\n            true\n          else\n            type = node&.type\n            (type == :send || type == :csend) && test_node(node.children[0])\n          end\n        end\n      end\n\n      class Self < Base\n        def test_node(node)\n          node&.type == :self\n        end\n      end\n\n      class Vcall < Base\n        attr_reader :name\n\n        def initialize(name:)\n          @name = name\n        end\n\n        def =~(pair)\n          node = pair.node\n\n          if node.type == :lvar\n            # We don't want lvar without method call\n            # Skips when the node is not receiver of :send\n            parent_node = pair.parent&.node\n            if parent_node && (parent_node.type == :send || parent_node.type == :csend) && parent_node.children.first.equal?(node)\n              test_node(node)\n            end\n          else\n            test_node(node)\n          end\n        end\n\n        def test_node(node)\n          case node&.type\n          when :send, :csend\n            node.children[1] == name\n          when :lvar\n            node.children.first == name\n          end\n        end\n      end\n\n      class Dstr < Base\n        def test_node(node)\n          node&.type == :dstr\n        end\n      end\n\n      class Ivar < Base\n        attr_reader :name\n\n        def initialize(name:)\n          @name = name\n        end\n\n        def test_node(node)\n          if node&.type == :ivar\n            name.nil? || node.children.first == name\n          end\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/pattern/kind.rb",
    "content": "module Querly\n  module Pattern\n    module Kind\n      class Base\n        attr_reader :expr\n\n        def initialize(expr:)\n          @expr = expr\n        end\n      end\n\n      module Negatable\n        attr_reader :negated\n\n        def initialize(expr:, negated:)\n          @negated = negated\n          super(expr: expr)\n        end\n      end\n\n      class Any < Base\n        def test_kind(pair)\n          true\n        end\n      end\n\n      class Conditional < Base\n        include Negatable\n\n        def test_kind(pair)\n          !negated == !!conditional?(pair)\n        end\n\n        def conditional?(pair)\n          node = pair.node\n          parent = pair.parent&.node\n\n          case parent&.type\n          when :if\n            node.equal? parent.children.first\n          when :while\n            node.equal? parent.children.first\n          when :and\n            node.equal? parent.children.first\n          when :or\n            node.equal? parent.children.first\n          when :csend\n            node.equal? parent.children.first\n          else\n            false\n          end\n        end\n      end\n\n      class Discarded < Base\n        include Negatable\n\n        def test_kind(pair)\n          !negated == !!discarded?(pair)\n        end\n\n        def discarded?(pair)\n          node = pair.node\n          parent = pair.parent&.node\n\n          case parent&.type\n          when :begin\n            if node.equal? parent.children.last\n              discarded? pair.parent\n            else\n              true\n            end\n          else\n            false\n          end\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/pattern/parser.y",
    "content": "class Querly::Pattern::Parser\nprechigh\n  nonassoc EXCLAMATION\n  nonassoc LPAREN\n  left DOT\npreclow\n\nrule\n\ntarget: kinded_expr\n\nkinded_expr: expr { result = Kind::Any.new(expr: val[0]) }\n  | expr CONDITIONAL_KIND { result = Kind::Conditional.new(expr: val[0], negated: val[1]) }\n  | expr DISCARDED_KIND { result = Kind::Discarded.new(expr: val[0], negated: val[1]) }\n\nexpr: constant { result = Expr::Constant.new(path: val[0]) }\n  | send\n  | SELF { result = Expr::Self.new }\n  | EXCLAMATION expr { result = Expr::Not.new(pattern: val[1]) }\n  | BOOL { result = Expr::Literal.new(type: :bool, values: val[0]) }\n  | literal { result = val[0] }\n  | literal AS META { result = val[0].with_values(resolve_meta(val[2])) }\n  | DSTR { result = Expr::Dstr.new() }\n  | UNDERBAR { result = Expr::Any.new }\n  | NIL { result = Expr::Nil.new }\n  | LPAREN expr RPAREN { result = val[1] }\n  | IVAR { result = Expr::Ivar.new(name: val[0]) }\n\nliteral:\n    STRING { result = Expr::Literal.new(type: :string, values: val[0]) }\n  | INT { result = Expr::Literal.new(type: :int, values: val[0]) }\n  | FLOAT { result = Expr::Literal.new(type: :float, values: val[0]) }\n  | SYMBOL { result = Expr::Literal.new(type: :symbol, values: val[0]) }\n  | NUMBER { result = Expr::Literal.new(type: :number, values: val[0]) }\n  | REGEXP { result = Expr::Literal.new(type: :regexp, values: nil) }\n\nargs:  { result = nil }\n  | expr { result = Argument::Expr.new(expr: val[0], tail: nil)}\n  | expr COMMA args { result = Argument::Expr.new(expr: val[0], tail: val[2]) }\n  | AMP expr { result = Argument::BlockPass.new(expr: val[1]) }\n  | kw_args\n  | DOTDOTDOT { result = Argument::AnySeq.new }\n  | DOTDOTDOT COMMA args { result = Argument::AnySeq.new(tail: val[2]) }\n  | DOTDOTDOT COMMA kw_args { result = Argument::AnySeq.new(tail: val[2]) }\n\nkw_args: { result = nil }\n  | AMP expr { result = Argument::BlockPass.new(expr: val[1]) }\n  | DOTDOTDOT { result = Argument::AnySeq.new }\n  | key_value { result = Argument::KeyValue.new(key: val[0][:key],\n                                                value: val[0][:value],\n                                                tail: nil,\n                                                negated: val[0][:negated]) }\n  | key_value COMMA kw_args { result = Argument::KeyValue.new(key: val[0][:key],\n                                                              value: val[0][:value],\n                                                              tail: val[2],\n                                                              negated: val[0][:negated]) }\n\nkey_value: keyword COLON expr { result = { key: val[0], value: val[2], negated: false } }\n  | EXCLAMATION keyword COLON expr { result = { key: val[1], value: val[3], negated: true } }\n\nmethod_name: METHOD\n  | EXCLAMATION\n  | AS\n  | META { result = resolve_meta(val[0]) }\n\nmethod_name_or_ident: method_name\n  | LIDENT\n  | UIDENT\n\nkeyword: LIDENT | UIDENT\n\nconstant: UIDENT { result = [val[0]] }\n  | UIDENT COLONCOLON constant { result = [val[0]] + val[2] }\n\nsend: LIDENT block { result = val[1] != nil ? Expr::Send.new(receiver: nil, name: val[0], block: val[1]) : Expr::Vcall.new(name: val[0]) }\n  | UIDENT block { result = Expr::Send.new(receiver: nil, name: val[0], block: val[1]) }\n  | method_name { result = Expr::Send.new(receiver: nil, name: val[0], block: nil) }\n  | method_name_or_ident LPAREN args RPAREN block { result = Expr::Send.new(receiver: nil,\n                                                                            name: val[0],\n                                                                            args: val[2],\n                                                                            block: val[4]) }\n  | receiver method_name_or_ident block { result = Expr::Send.new(receiver: val[0],\n                                                                  name: val[1],\n                                                                  args: Argument::AnySeq.new,\n                                                                  block: val[2]) }\n  | receiver method_name_or_ident LPAREN args RPAREN block { result = Expr::Send.new(receiver: val[0],\n                                                                                     name: val[1],\n                                                                                     args: val[3],\n                                                                                     block: val[5]) }\n  | receiver UNDERBAR block { result = Expr::Send.new(receiver: val[0], name: /.+/, block: val[2]) }\n  | receiver UNDERBAR LPAREN args RPAREN block { result = Expr::Send.new(receiver: val[0],\n                                                                         name: /.+/,\n                                                                         args: val[3],\n                                                                         block: val[5]) }\n\nreceiver: expr DOT { result = val[0] }\n  | expr DOTDOTDOT { result = Expr::ReceiverContext.new(receiver: val[0]) }\n\nblock: { result = nil }\n  | WITH_BLOCK { result = true }\n  | WITHOUT_BLOCK { result = false }\n\nend\n\n---- inner\n\nrequire \"strscan\"\n\nattr_reader :input\nattr_reader :where\n\ndef initialize(input, where:)\n  super()\n  @input = StringScanner.new(input)\n  @where = where\nend\n\ndef self.parse(str, where:)\n  self.new(str, where: where).do_parse\nend\n\ndef next_token\n  input.scan(/\\s+/)\n\n  case\n  when input.eos?\n    [false, false]\n  when input.scan(/true\\b/)\n    [:BOOL, true]\n  when input.scan(/false\\b/)\n    [:BOOL, false]\n  when input.scan(/nil/)\n    [:NIL, false]\n  when input.scan(/:string:/)\n    [:STRING, nil]\n  when input.scan(/\"([^\"]+)\"/)\n    [:STRING, input[1]]\n  when input.scan(/:dstr:/)\n    [:DSTR, nil]\n  when input.scan(/:int:/)\n    [:INT, nil]\n  when input.scan(/:float:/)\n    [:FLOAT, nil]\n  when input.scan(/:bool:/)\n    [:BOOL, nil]\n  when input.scan(/:symbol:/)\n    [:SYMBOL, nil]\n  when input.scan(/:number:/)\n    [:NUMBER, nil]\n  when input.scan(/:regexp:/)\n    [:REGEXP, nil]\n  when input.scan(/:\\w+/)\n    s = input.matched\n    [:SYMBOL, s[1, s.size - 1].to_sym]\n  when input.scan(/as\\b/)\n    [:AS, :as]\n  when input.scan(/{}/)\n    [:WITH_BLOCK, nil]\n  when input.scan(/!{}/)\n    [:WITHOUT_BLOCK, nil]\n  when input.scan(/[+-]?[0-9]+\\.[0-9]/)\n    [:FLOAT, input.matched.to_f]\n  when input.scan(/[+-]?[0-9]+/)\n    [:INT, input.matched.to_i]\n  when input.scan(/\\_/)\n    [:UNDERBAR, input.matched]\n  when input.scan(/[A-Z]\\w*/)\n    [:UIDENT, input.matched.to_sym]\n  when input.scan(/self/)\n    [:SELF, nil]\n  when input.scan(/'[a-z]\\w*/)\n    s = input.matched\n    [:META, s[1, s.size - 1].to_sym]\n  when input.scan(/[a-z_](\\w)*(\\?|\\!|=)?/)\n    [:LIDENT, input.matched.to_sym]\n  when input.scan(/\\(/)\n    [:LPAREN, input.matched]\n  when input.scan(/\\)/)\n    [:RPAREN, input.matched]\n  when input.scan(/\\.\\.\\./)\n    [:DOTDOTDOT, input.matched]\n  when input.scan(/\\,/)\n    [:COMMA, input.matched]\n  when input.scan(/\\./)\n    [:DOT, input.matched]\n  when input.scan(/\\!/)\n    [:EXCLAMATION, input.matched.to_sym]\n  when input.scan(/\\[conditional\\]/)\n    [:CONDITIONAL_KIND, false]\n  when input.scan(/\\[!conditional\\]/)\n    [:CONDITIONAL_KIND, true]\n  when input.scan(/\\[discarded\\]/)\n    [:DISCARDED_KIND, false]\n  when input.scan(/\\[!discarded\\]/)\n    [:DISCARDED_KIND, true]\n  when input.scan(/\\[\\]=/)\n    [:METHOD, :\"[]=\"]\n  when input.scan(/\\[\\]/)\n    [:METHOD, :\"[]\"]\n  when input.scan(/::/)\n    [:COLONCOLON, input.matched]\n  when input.scan(/:/)\n    [:COLON, input.matched]\n  when input.scan(/\\*/)\n    [:STAR, \"*\"]\n  when input.scan(/@\\w+/)\n    [:IVAR, input.matched.to_sym]\n  when input.scan(/@/)\n    [:IVAR, nil]\n  when input.scan(/&/)\n    [:AMP, nil]\n  end\nend\n\ndef resolve_meta(name)\n  where[name] or raise Racc::ParseError, \"Undefined meta variable: '#{name}\"\nend\n"
  },
  {
    "path": "lib/querly/pp/cli.rb",
    "content": "require \"optparse\"\n\nmodule Querly\n  module PP\n    class CLI\n      attr_reader :argv\n      attr_reader :command\n      attr_reader :load_paths\n      attr_reader :requires\n\n      attr_reader :stdin\n      attr_reader :stderr\n      attr_reader :stdout\n\n      def initialize(argv, stdin: STDIN, stdout: STDOUT, stderr: STDERR)\n        @argv = argv\n        @stdin = stdin\n        @stdout = stdout\n        @stderr = stderr\n\n        @load_paths = []\n        @requires = []\n\n        OptionParser.new do |opts|\n          opts.banner = \"Usage: #{opts.program_name} pp-name [options]\"\n          opts.on(\"-I dir\") {|path| load_paths << path }\n          opts.on(\"-r lib\") {|rq| requires << rq }\n        end.permute!(argv)\n\n        @command = argv.shift&.to_sym\n      end\n\n      def load_libs\n        load_paths.each do |path|\n          $LOAD_PATH << path\n        end\n\n        requires.each do |lib|\n          require lib\n        end\n\n      end\n\n      def run\n        available_commands = [:haml, :erb]\n\n        if available_commands.include?(command)\n          send :\"run_#{command}\"\n        else\n          stderr.puts \"Unknown command: #{command}\"\n          stderr.puts \"  available commands: #{available_commands.join(\", \")}\"\n          exit 1\n        end\n      end\n\n      def run_haml\n        require \"haml\"\n        load_libs\n        source = stdin.read\n\n        if Haml::VERSION >= '5.0.0'\n          stdout.print Haml::Engine.new(source).precompiled\n        else\n          options = Haml::Options.new\n          parser = Haml::Parser.new(source, options)\n          parser.parse\n          compiler = Haml::Compiler.new(options)\n          compiler.compile(parser.root)\n\n          stdout.print compiler.precompiled\n        end\n      end\n\n      def run_erb\n        require 'better_html'\n        require 'better_html/parser'\n        load_libs\n        source = stdin.read\n        source_buffer = Parser::Source::Buffer.new('(erb)')\n        source_buffer.source = source\n        parser = BetterHtml::Parser.new(source_buffer, template_language: :html)\n\n        new_source = source.gsub(/./, ' ')\n        parser.ast.descendants(:erb).each do |erb_node|\n          indicator_node, _, code_node, = *erb_node\n          next if indicator_node&.loc&.source == '#'\n          new_source[code_node.loc.range] = code_node.loc.source\n          new_source[code_node.loc.range.end] = ';'\n        end\n        stdout.puts new_source\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/preprocessor.rb",
    "content": "module Querly\n  class Preprocessor\n    class Error < StandardError\n      attr_reader :command\n      attr_reader :status\n\n      def initialize(command:, status:)\n        @command = command\n        @status = status\n      end\n    end\n\n    attr_reader :ext\n    attr_reader :command\n\n    def initialize(ext:, command:)\n      @ext = ext\n      @command = command\n    end\n\n    def run!(path)\n      stdout_read, stdout_write = IO.pipe\n\n      output = \"\"\n\n      reader = Thread.new do\n        while (line = stdout_read.gets)\n          output << line\n        end\n      end\n\n      succeeded = system(command, in: path.to_s, out: stdout_write)\n      stdout_write.close\n\n      reader.join\n\n      raise Error.new(status: $?, command: command) unless succeeded\n\n      output\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/rule.rb",
    "content": "module Querly\n  class Rule\n    class Example\n      attr_reader :before\n      attr_reader :after\n\n      def initialize(before:, after:)\n        @before = before\n        @after = after\n      end\n\n      def ==(other)\n        other.is_a?(Example) && other.before == before && other.after == after\n      end\n    end\n\n    attr_reader :id\n    attr_reader :patterns\n    attr_reader :messages\n\n    attr_reader :sources\n    attr_reader :justifications\n    attr_reader :before_examples\n    attr_reader :after_examples\n    attr_reader :examples\n    attr_reader :tags\n\n    def initialize(id:, messages:, patterns:, sources:, tags:, before_examples:, after_examples:, justifications:, examples:)\n      @id = id\n      @patterns = patterns\n      @sources = sources\n      @messages = messages\n      @justifications = justifications\n      @before_examples = before_examples\n      @after_examples = after_examples\n      @tags = tags\n      @examples = examples\n    end\n\n    def match?(identifier: nil, tags: nil)\n      if identifier\n        unless id == identifier || id.start_with?(identifier + \".\")\n          return false\n        end\n      end\n\n      if tags\n        unless tags.subset?(self.tags)\n          return false\n        end\n      end\n\n      true\n    end\n\n    class InvalidRuleHashError < StandardError; end\n    class PatternSyntaxError < StandardError; end\n\n    def self.load(hash)\n      id = hash[\"id\"]\n      raise InvalidRuleHashError, \"id is missing\" unless id\n\n      srcs = case hash[\"pattern\"]\n             when Array\n               hash[\"pattern\"]\n             when nil\n               []\n             else\n               [hash[\"pattern\"]]\n             end\n\n      raise InvalidRuleHashError, \"pattern is missing\" if srcs.empty?\n      patterns = srcs.map.with_index do |src, index|\n        case src\n        when String\n          subject = src\n          where = {}\n        when Hash\n          subject = src['subject']\n          where = Hash[src['where'].map {|k,v| [k.to_sym, translate_where(v)] }]\n        end\n\n        begin\n          Pattern::Parser.parse(subject, where: where)\n        rescue Racc::ParseError => exn\n          raise PatternSyntaxError, \"Pattern syntax error: rule=#{hash[\"id\"]}, index=#{index}, pattern=#{Rainbow(subject.split(\"\\n\").first).blue}, where=#{where.inspect}: #{exn}\"\n        end\n      end\n\n      messages = Array(hash[\"message\"])\n      raise InvalidRuleHashError, \"message is missing\" if messages.empty?\n\n      tags = Set.new(Array(hash[\"tags\"]))\n      examples = [hash[\"examples\"]].compact.flatten.map do |example|\n        raise(InvalidRuleHashError, \"Example should have at least before or after, #{example.inspect}\") unless example.key?(\"before\") || example.key?(\"after\")\n        Example.new(before: example[\"before\"], after: example[\"after\"])\n      end\n      before_examples = Array(hash[\"before\"])\n      after_examples = Array(hash[\"after\"])\n      justifications = Array(hash[\"justification\"])\n\n      Rule.new(id: id,\n               messages: messages,\n               patterns: patterns,\n               sources: srcs,\n               tags: tags,\n               before_examples: before_examples,\n               after_examples: after_examples,\n               justifications: justifications,\n               examples: examples)\n    end\n\n    def self.translate_where(value)\n      Array(value).map do |v|\n        case v\n        when /\\A\\/(.*)\\/\\Z/\n          Regexp.new($1)\n        else\n          v\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/rules/sample.rb",
    "content": "Querly.load_rule File.join(__dir__, \"../../../rules/sample.yml\")\n"
  },
  {
    "path": "lib/querly/script.rb",
    "content": "module Querly\n  class Script\n    attr_reader :path\n    attr_reader :node\n\n    def self.load(path:, source:)\n      parser = Parser::Ruby30.new(Builder.new).tap do |parser|\n        parser.diagnostics.all_errors_are_fatal = true\n        parser.diagnostics.ignore_warnings = true\n      end\n      buffer = Parser::Source::Buffer.new(path.to_s, 1)\n      buffer.source = source\n      self.new(path: path, node: parser.parse(buffer))\n    end\n\n    def initialize(path:, node:)\n      @path = path\n      @node = node\n    end\n\n    def root_pair\n      NodePair.new(node: node)\n    end\n\n    class Builder < Parser::Builders::Default\n      def string_value(token)\n        value(token)\n      end\n\n      def emit_lambda\n        true\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/script_enumerator.rb",
    "content": "module Querly\n  class ScriptEnumerator\n    attr_reader :paths\n    attr_reader :config\n    attr_reader :threads\n\n    def initialize(paths:, config:, threads:)\n      @paths = paths\n      @config = config\n      @threads = threads\n    end\n\n    # Yields `Script` object concurrently, in different threads.\n    def each(&block)\n      if block_given?\n        Parallel.each(each_path, in_threads: threads) do |path|\n          load_script_from_path path, &block\n        end\n      else\n        self.enum_for :each\n      end\n    end\n\n    def each_path(&block)\n      if block_given?\n        paths.each do |path|\n          if path.directory?\n            enumerate_files_in_dir(path, &block)\n          else\n            yield path\n          end\n        end\n      else\n        enum_for :each_path\n      end\n    end\n\n    @loaders = []\n\n    def self.register_loader(pattern, loader)\n      @loaders << [pattern, loader]\n    end\n\n    def self.find_loader(path)\n      basename = path.basename.to_s\n      @loaders.find {|pair| pair.first === basename }&.last\n    end\n\n    private\n\n    def load_script_from_path(path, &block)\n      preprocessor = preprocessors[path.extname]\n\n      begin\n        source = if preprocessor\n                   preprocessor.run!(path)\n                 else\n                   path.read\n                 end\n\n        script = Script.load(path: path, source: source)\n      rescue StandardError, LoadError, Preprocessor::Error => exn\n        script = exn\n      end\n\n      yield(path, script)\n    end\n\n    def preprocessors\n      config&.preprocessors || {}\n    end\n\n    def enumerate_files_in_dir(path, &block)\n      if path.basename.to_s =~ /\\A\\.[^\\.]+/\n        # skip hidden paths\n        return\n      end\n\n      case\n      when path.directory?\n        path.children.each do |child|\n          enumerate_files_in_dir child, &block\n        end\n      when path.file?\n\n\n        extensions = %w[\n          .rb .builder .fcgi .gemspec .god .jbuilder .jb .mspec .opal .pluginspec\n          .podspec .rabl .rake .rbuild .rbw .rbx .ru .ruby .spec .thor .watchr\n        ]\n        basenames = %w[\n          .irbrc .pryrc buildfile Appraisals Berksfile Brewfile Buildfile Capfile\n          Cheffile Dangerfile Deliverfile Fastfile Gemfile Guardfile Jarfile Mavenfile\n          Podfile Puppetfile Rakefile Snapfile Thorfile Vagabondfile Vagrantfile\n        ]\n        should_load_file = case\n                           when extensions.include?(path.extname)\n                             true\n                           when basenames.include?(path.basename.to_s)\n                             true\n                           else\n                             preprocessors.key?(path.extname)\n                           end\n\n        yield path if should_load_file\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/querly/version.rb",
    "content": "module Querly\n  VERSION = \"1.3.0\"\nend\n"
  },
  {
    "path": "lib/querly.rb",
    "content": "require 'pathname'\nrequire \"yaml\"\nrequire \"rainbow\"\nrequire \"parser/ruby30\"\nrequire \"set\"\nrequire \"open3\"\nrequire \"active_support/inflector\"\nrequire \"parallel\"\n\nrequire \"querly/version\"\nrequire 'querly/analyzer'\nrequire 'querly/rule'\nrequire 'querly/pattern/expr'\nrequire 'querly/pattern/argument'\nrequire 'querly/script'\nrequire 'querly/script_enumerator'\nrequire 'querly/node_pair'\nrequire \"querly/pattern/parser\"\nrequire 'querly/pattern/kind'\nrequire \"querly/config\"\nrequire \"querly/preprocessor\"\nrequire \"querly/check\"\nrequire \"querly/concerns/backtrace_formatter\"\n\nmodule Querly\n  @@required_rules = []\n\n  def self.required_rules\n    @@required_rules\n  end\n\n  def self.load_rule(*files)\n    files.each do |file|\n      path = Pathname(file)\n      yaml = YAML.load(path.read)\n      rules = yaml.map {|hash| Rule.load(hash) }\n      required_rules.concat rules\n    end\n  end\nend\n"
  },
  {
    "path": "manual/configuration.md",
    "content": "# Overview\n\nThe configuration file, default name is `querly.yml`, will look like the following.\n\n```yml\nrules:\n  ...\npreprocessor:\n  ...\ncheck:\n  ...\n```\n\n# rules\n\n`rules` is array of rule hash.\n\n```yml\n  - id: com.sideci.json\n    pattern: Net::HTTP\n    message: \"Should use HTTPClient instead of Net::HTTP\"\n    justification:\n      - No exception!\n    before:\n      - \"Net::HTTP.get(url)\"\n    after:\n      - HTTPClient.new.get_content(url)\n```\n\nThe rule hash contains following keys:\n\n* `id` Identifier of the rule, must be unique (string)\n* `pattern` Patterns to find out (string, or array of string)\n* `message` Error message to explain why the code fragment needs special care (string)\n* `justification` When the *bad use* is allowed (string, or array of string)\n* `before` Sample ruby code to find out (string, or array of string)\n* `after` Sample ruby code to be fixed (string, or array of string)\n\n# preprocessor\n\nWhen your project contains `.slim`, `.haml`, or any templates which contains Ruby code, preprocessor is to translate the templates to Ruby code.\n`preprocessor` is a hash; key of extension of the templates, value of command line.\n\n```yml\n.slim: slimrb --compile\n.haml: bundle exec querly-pp haml -I lib -r your_custom_plugin\n```\n\nThe command will be executed with stdin of template code, and should emit ruby code to stdout.\n\n## querly-pp\n\nQuerly 0.2.0 ships with `querly-pp` command line tool which compiles given HAML source to Ruby script.\n`-I` and `-r` options can be used to use plugins.\n\n# check\n\nDefine set of rules to check for each file.\n\n```yml\ncheck:\n  - path: /test\n    rules:\n      - com.acme.corp\n      - append: com.acme.corp\n      - except: com.acme.corp\n      - only: com.acme.corp\n  - path: /test/unit\n    rules:\n      - append:\n          tags: foo bar\n      - except:\n          tags: foo bar\n      - only:\n          tags: foo bar\n```\n\n* `path` Files to apply the rules in `.gitignore` syntax\n* `rules` Rules to check\n\nAll matching `check` element against given file name will be applied, sequentially.\n\n* `/lib/bar.rb` => no checks will be applied (all rules)\n* `/test/test_helper.rb` => `/test` check will be applied\n* `/test/unit/account_test.rb` => `/test` and `/test/unit` checks will be applied\n\n## Rules\n\nYou can use `append:`, `except:` and `only:` operation.\n\n* `append:` appends rules to current rule set\n* `except:` removes rules from current rule set\n* `only:` update current rule set\n\n"
  },
  {
    "path": "manual/examples.md",
    "content": "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 feature spec\n\nI 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?\n\nThe magic happens in `rails_helper.rb`.\n\n```rb\nCapybara.configure do |config|\n  config.default_driver    = :poltergeist\n  config.javascript_driver = :poltergeist\nend\n```\n\nOkay, `js: true` does not make any sense, because both `default_driver` and `javascript_driver` are same.\n\nShould 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* 😸 \n\nIt'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.\n\n```yaml\n- id: sample.scenario_with_js_option\n  pattern: \"scenario(..., js: _, ...)\"\n  message: |\n    You do not need js:true option\n\n    We are using Poltergeist as both default_driver and javascript_driver!\n  before:\n    - \"scenario 'hello world', js: true, type: :feature do end\"\n  after:\n    - \"scenario 'foo bar' do end\"\n```\n\nNo 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.\n"
  },
  {
    "path": "manual/patterns.md",
    "content": "# Syntax\n\n## Toplevel\n\n* *expr*\n* *expr* `[` *kind* `]` (kinded expr)\n* *expr* `[!` *kind* `]` (negated kinded expr)\n\n## expr\n\n* `_` (any expr)\n* *method* (method call, with any receiver and any args)\n* *method* `(` *args* `)` *block_spec* (method call with any receiver)\n* *receiver* *method* (method call with any args)\n* *receiver* *method* `(` *args* `)` *block_spec* (method call)\n* *literal*\n* `self` (self)\n* `!` *expr*\n\n### block_spec\n\n* (no spec)\n* `{}` (method call should be with block)\n* `!{}` (method call should not be with block)\n\n### receiver\n\n* *expr* `.` (receiver matching with the pattern)\n* *expr* `...` (some receiver in the chain matching with the pattern)\n\n### Examples\n\n* `p(_)` `p` call with one argument, any receiver\n* `self.p(1)` `p` call with `1`, receiver is `self` or omitted.\n* `foo.bar.baz` `baz` call with receiver of `bar` call of receiver of `foo` call\n* `update_attribute(:symbol:, :string:)` `update_attribute` call with symbol and string literals\n* `File.open(...) !{}` `File.open` call but without block\n\n```rb\np 1        # p(_) matches\np 2        # p(_) matches\np 1, 2, 3  # p(_) does not match\n\np(1)       # self.p(1) matches\n\nfoo(1).bar {|x| x+1 }.baz(3)  # foo.bar.baz matches\n(1+2).foo.bar(*args).baz.bla  # foo.bar.baz matches, partially\nfoo.xyz.bar.baz               # foo.bar.baz does not match\n\nupdate_attribute(:name, \"hoge\")    # f(:symbol:, :string:) matches\nupdate_attribute(:name, name)      # f(:symbol:, :string:) does not match\n\nfoo.bar.baz               # foo.bar.baz matches\nfoo.bar.baz               # foo...baz matches\nbar.foo.baz               # foo...bar...baz does not match\n```\n\n## args & kwargs\n\n### args\n\n* *expr* `,` *args*\n* *expr* `,` *kwargs*\n* *expr*\n* `...` `,` *kwargs* (any argument sequence, followed by keyword arguments)\n* `...` (any argument sequence, including any keyword arguments)\n\n### Literals\n\n* `123` (integer)\n* `1.23` (float)\n* `:foobar` (symbol)\n* `:symbol:` (any symbol literal)\n* `\"foobar\"` (string)\n    * NOTE: It only supports double quotation.\n* `:string:` (any string literal)\n* `:dstr:` (any dstr `\"hi #{name}\"`)\n* `true`, `false` (true and false)\n* `nil` (nil)\n* `:number:`, `:int:`, `:float:` (any number, any integer, any float)\n* `:bool:` (true or false)\n\n### kwargs\n\n* *symbol* `:` *expr* `,` ...\n* `!` *symbol* `:` *expr* `,` ...\n* `...`\n* `&` *expr*\n\n### Examples\n\n```rb\nf(1,2,3)   # f(...), f(1,2,...), and f(1, ...) matches\n           # f(_,_), f(0, ...) does not match\n\nJSON.load(string, symbolize_names: true)   # JSON.load(..., symbolize_names: true) matches\n                                           # JSON.load(symbolize_names: true) does not match\n\nrecord.update(email: email, name: name)    # update(name: _, email: _) matches\n                                           # update(name: _) does not match\n                                           # update(name: _, ...) matches\n                                           # update(!id: _, ...) matches\n\narticle.try(&:author)        # try(&:symbol:) matches\narticle.try(:author)         # try(&:symbol:) does not match\narticle.try {|x| x.author }  # try(&:symbol:) does not match\n```\n\n## kind\n\n* `conditional` (When expr appears in *conditional* context)\n* `discarded` (When expr appears in *discarded* context)\n\nKind allows you to find out something like:\n\n* `save` call but does not check its result for error recovery\n\n*conditional* context is\n\n* Condition of `if` construct\n* Condition of loop constructs\n* LHS of `&&` and `||`\n\n```rb\n# record.save is in conditional context\nunless record.save\n  # error recovery\nend\n\n# record.save is not in conditional context\nx = record.save\n\n# record.save is in conditional context\nrecord.save or abort()\n```\n\n*discarded* context is where the value of the expression is completely discarded, a bit looser than *conditional*.\n\n```rb\ndef f()\n  # record.save is in discarded context\n  foo()\n  record.save()\n  bar\nend\n```\n\n# Interpolation Syntax\n\nIf you want to describe a pattern that can not be described with above syntax, you can use interpolation as follows:\n\n```yaml\nid: find_by_abc_and_def\npattern:\n  subject: \"'finder(...)\"\n  where:\n    finder: \n      - /find_by_\\w+\\_.*/\n      - find_by_id\n```\n\nIt matches with `find_by_email_and_name(...)`.\n\n- Meta variables `'finder` can occur only as method name\n- Unused meta var definition is okay, but undefined meta var reference raises an error\n- If value of meta var is a string `foo`, it matches send nodes with exactly same method name\n- If value of meta var is a regexp `/foo/`, it matches send nodes with method name which `=~` the regexp\n\nYou can also use `as` syntax with `:symbol:` and so on.\n\n```yaml\nid: migration_references\npattern:\n  subject: \"t.integer(:symbol: as 'column, ...)\"\n  where:\n    column: '/.+_id/'\n```\n\n# Difference from Ruby\n\n* Method call parenthesis cannot be omitted (if omitted, it means *any arguments*)\n* `+`, `-`, `[]` or other *operator* should be written as method calls like `_.+(_)`, `[]=(:string:, _)`\n\n# Testing\n\nYou can test patterns by `querly console .` command interactively.\n\n```\nQuerly 0.1.0, interactive console\n\nCommands:\n  - find PATTERN   Find PATTERN from given paths\n  - reload!        Reload program from paths\n  - quit\n\nLoading... ready!\n> \n```\n\nAlso `querly test` will help you.\nIt test configuration file by checking patterns in rules against `before` and `after` examples.\n"
  },
  {
    "path": "querly.gemspec",
    "content": "# coding: utf-8\nlib = File.expand_path('../lib', __FILE__)\n$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)\nrequire 'querly/version'\n\nGem::Specification.new do |spec|\n  spec.name          = \"querly\"\n  spec.version       = Querly::VERSION\n  spec.authors       = [\"Soutaro Matsumoto\"]\n  spec.email         = [\"matsumoto@soutaro.com\"]\n  spec.license       = \"MIT\"\n\n  spec.summary       = %q{Pattern Based Checking Tool for Ruby}\n  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.}\n  spec.homepage      = \"https://github.com/soutaro/querly\"\n\n  spec.files         = `git ls-files -z`\n    .split(\"\\x0\")\n    .reject { |f| f.match(%r{^(test|spec|features)/}) }\n    .push('lib/querly/pattern/parser.rb')\n  spec.bindir        = \"exe\"\n  spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }\n  spec.require_paths = [\"lib\"]\n\n  spec.required_ruby_version = \">= 2.7\"\n\n  spec.add_development_dependency \"bundler\", \">= 1.12\"\n  spec.add_development_dependency \"rake\", \"~> 13.0\"\n  spec.add_development_dependency \"minitest\", \"~> 5.0\"\n  spec.add_development_dependency \"racc\", \">= 1.4.14\"\n  spec.add_development_dependency \"unification_assertion\", \"0.0.1\"\n  spec.add_development_dependency \"better_html\", \"~> 1.0.13\"\n  spec.add_development_dependency \"slim\", \"~> 4.0.1\"\n  spec.add_development_dependency \"haml\", \"~> 5.0.4\"\n\n  spec.add_dependency 'thor', \">= 0.19.0\"\n  spec.add_dependency \"parser\", \">= 3.0\"\n  spec.add_dependency \"rainbow\", \">= 2.1\"\n  spec.add_dependency \"activesupport\", \">= 5.0\"\n  spec.add_dependency \"parallel\", \"~>1.17\"\nend\n"
  },
  {
    "path": "rules/sample.yml",
    "content": "- id: sample.ruby.pathname\n  pattern: Pathname.new\n  message: |\n    Did you mean `Pathname` method?\n\n- id: sample.ruby.file.dir\n  pattern: \"File.dirname(:string:)\"\n  message: |\n    Did you mean `__dir__`?\n\n- id: sample.ruby.file.open\n  pattern: File.open(...) !{}\n  message: |\n    Using block is better in Ruby\n\n- id: sample.ruby.debug_print\n  pattern:\n    - self.p\n    - self.pp\n  message: |\n    Delete debug print\n\n- id: sample.ruby.clone\n  pattern:\n    - clone()\n    - \"clone(!freeze: _)\"\n  message: |\n    Did you mean `dup`?\n\n    `clone` returns frozen object when receiver is frozen.\n\n    * If object is frozen, you usually does not need to clone it\n    * When you need a copy of a object, you probably want to modify that; did you mean dup?\n    * Ruby 2.4 accepts freeze keyword and returns not frozen object\n"
  },
  {
    "path": "sample.yaml",
    "content": "rules:\n  - id: sample.delete_all\n    pattern:\n      - delete_all\n      - update_all\n    message: |\n      Validations and callbacks will be skipped\n\n      It's faster than calling destroy or update each record, but may be dangerous.\n    justification:\n      - You have too much record to update/destroy one by one\n      - When you are sure skipping callbacks/validations is okay\n    before:\n      - records.delete_all\n      - \"records.update_all(people_id: nil)\"\n    after:\n      - records.each(&:destroy)\n      - |\n        records.each do |record|\n          record.update(people_id: nil)\n        end\n  - id: sample.save_not_conditional\n    pattern:\n      - \"save(!validate: false, ...) [!conditional]\"\n      - \"update(...) [!conditional]\"\n      - \"update_attributes(...) [!conditional]\"\n    message: |\n      save or update returns false when it fails.\n\n      Check the return value and try error recovery.\n    justification:\n      - When there is no way to recover from error.\n    before:\n      - record.save()\n      - \"record.update(foo: bar)\"\n      - \"record.update(attrs)\"\n    after:\n      - if record.save() then do_something end\n      - \"record.save(validate: false)\"\n      - \"record.update(foo: 1) or fail\"\n  - id: sample.skip_validation\n    pattern:\n      - \"save(validate: false)\"\n      - \"save!(validate: false)\"\n    message: These calls will skip validation\n    justification:\n      - When you want to skip validation\n      - When you have done validation code in other than ActiveRecord's validation mechanism\n  - id: sample.net_http\n    pattern: Net::HTTP\n    message: Use HTTPClient to make Web api calls\n    before:\n      - Net::HTTP.get(url)\n    after:\n      - HTTPClient.new.get_content(url)\n  - id: sample.root_url_without_locale\n    pattern: \"root_url(!locale: _)\"\n    message: Put locale parameter in the link to top page\n    before: root_url()\n    after: \"root_url(locale: I18n.locale)\"\n  - id: sample.transaction\n    pattern: transaction\n    message: Use with_account_lock helper, generally\n    justification:\n      - It does not need lock over account\n      - It does not access accounts table\n    before:\n      - transaction do something end\n    after:\n      - with_account_lock(account) do something end\n  - id: sample.oj\n    pattern:\n      - JSON.load\n      - JSON.dump\n    message: Use Oj for JSON load and dump\n  - id: sample.metaprogramming_abuse\n    pattern:\n      - classify\n      - constantize\n      - eval\n      - instance_values\n      - safe_constantize\n    message: Consider three times before using meta-programming\n  - id: sample.activesupport.try\n    pattern: \"try(:symbol:, ...)\"\n    message: try returns nil if the method is not defined, try! instead?\n  - id: sample.try_with_block_pass\n    pattern: \"try(&:symbol:)\"\n    message: It's same as just passing symbol, and slower\n  - id: sample.transaction_renew\n    pattern: \"transaction(requires_new: true)\"\n    message: Our RDBMS does not support nested transaction\n  - id: sample.capybara.assertion\n    pattern:\n      - assert_equal\n      - assert\n    message: |\n      Using minitest assertions with Capybara may make test unstable\n\n      Use Capybara assertions like assert_selector\n    justification:\n      - There is no access to Capybara in that test\n      - You have using retrying in test\n    tags:\n      - test\n      - capybara\n  - id: sample.capybara.negations\n    pattern:\n      - refute\n      - assert_nil\n    message: |\n      Negating capybara assertions would make test unstable\n\n      Maybe you can use Capybara helpers like has_no_css?\n    justification:\n      - There is no access to Capybara in the test\n      - You have implemented retrying in test\n    tags:\n      - test\n      - capybara\n  - id: sample.order-group\n    pattern: order...group\n    before:\n      - records.where.order.group\n      - records.order.where.group\n    message: |\n      Using both group and order may generate broken SQL\n  - id: sample.pp_meta\n    message: |\n      Method names can be a meta variable reference\n\n      Meta variable starts with single quote ', and followed with lower letter.\n    pattern:\n      - subject: \"'p(...)\"\n        where:\n          p: /p+/\n  - id: sample.count\n    pattern: count() !{}\n    message: |\n      Use size or length for count, if receiver is an array\n    examples:\n      - before: \"[].count\"\n        after: \"[].size\"\n      - after: \"[].count(:x)\"\n      - after: \"[].count {|x| x > 3 }\"\n\npreprocessor:\n  .slim: slimrb --compile\n  .haml: querly-pp haml\n  .erb: querly-pp erb\n\nimport:\n  - load: querly/rules/*.yml\n  - require: querly/rules/sample\n\ncheck:\n  - path: /\n    rules:\n      - except: minitest\n  - path: /test\n    rules:\n      - minitest\n      - except:\n          tags: capybara minitest\n  - path: /test/integration\n    rules:\n      - append:\n          tags: capybara minitest\n  - path: /features/step_definitions\n    rules:\n      - append:\n          tags: capybara minitest\n"
  },
  {
    "path": "template.yml",
    "content": "rules:\n  - id: sample.debug_print\n    pattern:\n      - self.p\n      - self.pp\n    message: Delete debug print\n    examples:\n      - before: |\n          pp some: error\n\n  - id: sample.file.open\n    pattern: File.open(...) !{}\n    message: |\n      Use block to read/write file\n\n      If you use block, the open method closes file implicitly.\n      You don't have to close files explicitly.\n    examples:\n      - before: |\n          io = File.open(\"foo.txt\")\n          io.write(\"hello world\")\n          io.close\n        after: |\n          File.open(\"foo.txt\") do |io|\n            io.write(\"hello world\")\n          end\n\n  - id: sample.exception\n    pattern: Exception\n    message: |\n      You probably should use StandardError\n\n      If you are trying to define error class, inherit that from StandardError.\n    justification:\n      - You are sure you want to define an exception which is not rescued by default\n    examples:\n      - before: class MyError < Exception; end\n        after: class MyError < StandardError; end\n\n  - id: sample.test.assert_equal_size\n    pattern:\n      subject: \"assert_equal(:int: as 'zero, _.'size, ...)\"\n      where:\n        zero: 0\n        size:\n          - size\n          - count\n    message: |\n      Comparing size of something with 0 can be written using assert_empty\n    examples:\n      - before: |\n          assert_equal 0, some.size\n        after: |\n          assert_empty some.size\n      - before: |\n          assert_equal 0, some.count\n        after: |\n          assert_empty some.count\n\npreprocessor:\n  # .slim: slimrb --compile # Install `slim` gem for slim support\n  # .erb: querly-pp erb     # Install `better_erb` gem for erb support\n  # .haml: querly-pp haml   # Install `haml` gem for haml support\n\ncheck:\n  - path: /\n    rules:\n      - except: sample.test\n  - path: /test\n    rules:\n      - append: sample.test\n"
  },
  {
    "path": "test/analyzer_test.rb",
    "content": "require_relative \"test_helper\"\n\nclass AnalyzerTest < Minitest::Test\n  Analyzer = Querly::Analyzer\n  Config = Querly::Config\n\n  def stderr\n    @stderr ||= StringIO.new\n  end\nend\n"
  },
  {
    "path": "test/check_test.rb",
    "content": "require_relative \"test_helper\"\n\nclass CheckTest < Minitest::Test\n  Check = Querly::Check\n  Rule = Querly::Rule\n\n  def root\n    @root ||= Pathname(\"/root/path\")\n  end\n\n  def test_match1\n    check = Check.new(pattern: \"foo\", rules: [])\n\n    assert check.match?(path: Pathname(\"foo/bar\"))\n    assert check.match?(path: Pathname(\"foo\"))\n    assert check.match?(path: Pathname(\"bar/foo\"))\n    assert check.match?(path: Pathname(\"bar/foo/baz\"))\n\n    refute check.match?(path: Pathname(\"foobar\"))\n    refute check.match?(path: Pathname(\"bar\"))\n    refute check.match?(path: Pathname(\"bazbar\"))\n  end\n\n  def test_match2\n    check = Check.new(pattern: \"foo/bar\", rules: [])\n\n    assert check.match?(path: Pathname(\"foo/bar\"))\n    assert check.match?(path: Pathname(\"foo/bar/baz\"))\n\n    refute check.match?(path: Pathname(\"xyzzy/foo/bar\"))\n    refute check.match?(path: Pathname(\"foo/baz/bar\"))\n  end\n\n  def test_match3\n    check = Check.new(pattern: \"foo/bar/\", rules: [])\n\n    assert check.match?(path: Pathname(\"foo/bar/baz\"))\n\n    refute check.match?(path: Pathname(\"foo/bar\"))\n    refute check.match?(path: Pathname(\"xyz/foo/bar/baz\"))\n  end\n\n  def test_match4\n    check = Check.new(pattern: \"foo/\", rules: [])\n\n    assert check.match?(path: Pathname(\"foo/bar\"))\n    assert check.match?(path: Pathname(\"baz/foo/bar\"))\n\n    refute check.match?(path: Pathname(\"foo\"))\n    refute check.match?(path: Pathname(\"baz/foo\"))\n  end\n\n  def test_match5\n    check = Check.new(pattern: \"/foo\", rules: [])\n\n    assert check.match?(path: Pathname(\"foo/bar\"))\n    assert check.match?(path: Pathname(\"foo\"))\n\n    refute check.match?(path: Pathname(\"baz/foo/bar\"))\n    refute check.match?(path: Pathname(\"baz/foo\"))\n  end\n\n  def test_load\n    check = Check.load('path' => \"foo\",\n                       'rules' => [\n                         \"rails.models\",\n                         { \"id\" => \"ruby\", \"tags\" => [\"foo\", \"bar\"] },\n                         { \"append\" => { \"tags\" => [\"baz\"] } },\n                         { \"only\" => \"minitest\" },\n                         { \"except\" => { \"id\" => \"rspec\", \"tags\" => \"t1 t2\" } },\n                       ])\n\n    assert_equal 5, check.rules.size\n\n    # appending rule by id\n    assert_equal Check::Query.new(:append, nil, \"rails.models\"), check.rules[0]\n    # default operand is append\n    assert_equal Check::Query.new(:append, Set.new([\"foo\", \"bar\"]), \"ruby\"), check.rules[1]\n    # append by explicit tags\n    assert_equal Check::Query.new(:append, Set.new([\"baz\"]), nil), check.rules[2]\n    # only by implicit id\n    assert_equal Check::Query.new(:only, nil, \"minitest\"), check.rules[3]\n    # except by explicit id\n    assert_equal Check::Query.new(:except, Set.new([\"t1\", \"t2\"]), \"rspec\"), check.rules[4]\n  end\n\n  def test_query_match\n    rule = Rule.new(id: \"ruby.pathname\", messages: nil, patterns: nil, sources: nil, tags: Set.new([\"tag1\", \"tag2\"]), before_examples: [], after_examples: [], justifications: [], examples: [])\n\n    assert Check::Query.new(:append, nil, \"ruby.pathname\").match?(rule)\n    assert Check::Query.new(:append, nil, \"ruby\").match?(rule)\n    refute Check::Query.new(:append, nil, \"ruby23\").match?(rule)\n\n    assert Check::Query.new(:append, Set.new([\"tag1\"]), nil).match?(rule)\n    assert Check::Query.new(:append, Set.new([\"tag1\", \"tag2\"]), nil).match?(rule)\n    refute Check::Query.new(:append, Set.new([\"tag1\", \"foo\"]), nil).match?(rule)\n\n    assert Check::Query.new(:append, Set.new([\"tag1\"]), \"ruby\").match?(rule)\n    refute Check::Query.new(:append, Set.new([\"tag1\"]), \"ruby23\").match?(rule)\n    refute Check::Query.new(:append, Set.new([\"tag1\", \"foo\"]), \"ruby\").match?(rule)\n  end\n\n  def test_query_apply\n    r1 = Rule.new(id: \"ruby.pathname\", messages: nil, patterns: nil, sources: nil, tags: Set.new(), before_examples: [], after_examples: [], justifications: [], examples: [])\n    r2 = Rule.new(id: \"minitest.assert\", messages: nil, patterns: nil, sources: nil, tags: Set.new(), before_examples: [], after_examples: [], justifications: [], examples: [])\n    all_rules = Set.new([r1, r2])\n\n    assert_equal Set.new([r1, r2]), Check::Query.new(:append, nil, \"ruby\").apply(current: Set.new([r2]), all: all_rules)\n    assert_equal Set.new([r2]), Check::Query.new(:except, nil, \"ruby\").apply(current: all_rules, all: all_rules)\n    assert_equal Set.new([r1]), Check::Query.new(:only, nil, \"ruby\").apply(current: all_rules, all: all_rules)\n  end\nend\n"
  },
  {
    "path": "test/cli/console_test.rb",
    "content": "require_relative \"../test_helper\"\nrequire \"querly/cli/console\"\nrequire \"pty\"\n\nclass ConsoleTest < Minitest::Test\n  include TestHelper\n\n  def exe_path\n    Pathname(__dir__) + \"../../exe/querly\"\n  end\n\n  def read_for(read, pattern:)\n    timeout_at = Time.now + 3\n    result = \"\"\n\n    while true\n      if Time.now > timeout_at\n        raise \"Timedout waiting for #{pattern}\"\n      end\n\n      buf = \"\"\n      read.read_nonblock 1024, buf rescue IO::EAGAINWaitReadable\n\n      if buf == \"\"\n        sleep 0.1\n      else\n        result << buf.force_encoding(Encoding::UTF_8)\n      end\n\n      if pattern =~ result\n        break\n      end\n    end\n\n    result\n  end\n\n  def test_console\n    mktmpdir do |path|\n      (path + \"foo.rb\").write(<<-EOF)\nclass UsersController\n  def create\n    user = User.create!(params[:user])\n    redirect_to user_path(user)\n  end\nend\n      EOF\n\n      homedir = path + \"home\"\n      homedir.mkdir\n\n      history = path + \"home/.querly/history\"\n\n      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|\n        read_for(read, pattern: /^> $/)\n\n        write.puts \"reload!\"\n        read.gets\n        read_for(read, pattern: /^> $/)\n\n        write.puts \"find create!\"\n        read.gets\n        output = read_for(read, pattern: /^> $/)\n        assert_match %r/#{Regexp.escape \"User.create!(params[:user])\"}/, output\n\n        write.puts \"find redirect_to\"\n        read.gets\n        output = read_for(read, pattern: /^> $/)\n        assert_match %r/#{Regexp.escape \"redirect_to user_path(user)\"}/, output\n\n        write.puts \"find User.find_each\"\n        read.gets\n        output = read_for(read, pattern: /^> $/)\n        assert_match %r/#{Regexp.escape \"0 results\"}/, output\n\n        write.puts \"find crea te !!\"\n        read.gets\n        output = read_for(read, pattern: /^> $/)\n        assert_match %r/#{Regexp.escape \"parse error on value\"}/, output\n\n        write.puts \"no such command\"\n        read.gets\n        output = read_for(read, pattern: /^> $/)\n        assert_match %r/#{Regexp.escape \"Commands:\"}/, output\n\n        write.puts \"quit\"\n        read.gets\n\n        Process.wait pid\n      end\n\n      assert_equal [\"find redirect_to\", \"find User.find_each\"], history.readlines.map(&:chomp)\n\n      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|\n        read_for(read, pattern: /^> $/)\n\n        write.puts \"reload!\"\n        read.gets\n        read_for(read, pattern: /^> $/)\n\n        write.puts \"find create!\"\n        read.gets\n        output = read_for(read, pattern: /^> $/)\n        assert_match %r/#{Regexp.escape \"User.create!(params[:user])\"}/, output\n\n        write.puts \"exit\"\n        read.gets\n\n        Process.wait pid\n      end\n\n      assert_equal [\"find User.find_each\", \"find create!\"], history.readlines.map(&:chomp)\n    end\n  end\n\n  def test_history_location_override\n    mktmpdir do |path|\n      (path + \"foo.rb\").write(<<-EOF)\nclass UsersController\n  def create\n    user = User.create!(params[:user])\n    redirect_to user_path(user)\n  end\nend\n      EOF\n\n      homedir = path + \"querly\"\n      homedir.mkdir\n\n      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|\n        read_for(read, pattern: /^> $/)\n\n        write.puts \"find create!\"\n        read.gets\n        output = read_for(read, pattern: /^> $/)\n        assert_match %r/#{Regexp.escape \"User.create!(params[:user])\"}/, output\n\n        write.puts \"quit\"\n        read.gets\n\n        Process.wait pid\n      end\n\n      history = path + \"querly/history\"\n      assert_equal [\"find create!\"], history.readlines.map(&:chomp)\n    end\n  end\nend\n"
  },
  {
    "path": "test/cli/rules_test.rb",
    "content": "require_relative \"../test_helper\"\nrequire \"querly/cli/rules\"\n\nclass RulesTest < Minitest::Test\n  include TestHelper\n\n  def test_rules_command\n    config = {\n      \"rules\" => [\n        {\n          \"id\" => \"foo.rule1\",\n          \"message\" => \"Sample Message\",\n          \"pattern\" => \"@_\"\n        },\n        {\n          \"id\" => \"bar.rule2\",\n          \"message\" => [\"foo\", \"bar\"],\n          \"pattern\" => [\"@_\", \"foo\"]\n        }\n      ]\n    }\n\n    with_config config do |path|\n      rules = Querly::CLI::Rules.new(config_path: path, ids: [\"foo\"], stdout: stdout)\n      rules.run\n\n      assert_match(/foo\\.rule1/, stdout.string)\n      refute_match(/bar\\.rule2/, stdout.string)\n    end\n  end\nend\n"
  },
  {
    "path": "test/cli/test_test.rb",
    "content": "require_relative \"../test_helper\"\n\nclass TestTest < Minitest::Test\n  Test = Querly::CLI::Test\n  Config = Querly::Config\n\n  attr_accessor :stdout, :stderr\n\n  def setup\n    self.stdout = StringIO.new\n    self.stderr = StringIO.new\n  end\n\n  def test_load_config_failure\n    test = Test.new(config_path: Pathname(\"querly.yaml\"), stdout: stdout, stderr: stderr)\n\n    def test.load_config\n      nil\n    end\n\n    result = test.run\n\n    assert_equal 1, result\n    assert_match %r/There is nothing to test at querly\\.yaml/, stdout.string\n  end\n\n  def test_rule_uniqueness\n    test = Test.new(config_path: Pathname(\"querly.yaml\"), stdout: stdout, stderr: stderr)\n\n    def test.load_config\n      Config.load(\n        {\n          \"rules\" =>\n            [\n              { \"id\" => \"id1\", \"pattern\" => \"_\", \"message\" => \"hello\" },\n              { \"id\" => \"id1\", \"pattern\" => \"_\", \"message\" => \"hello\" },\n              { \"id\" => \"id2\", \"pattern\" => \"_\", \"message\" => \"hello\" }\n            ]\n        },\n        config_path: Pathname.pwd,\n        root_dir: Pathname.pwd,\n        stderr: stderr\n      )\n    end\n\n    result = test.run\n\n    assert_equal 1, result\n    assert_match %r/Rule id id1 duplicated!/, stdout.string\n    refute_match %r/Rule id id2 duplicated!/, stdout.string\n  end\n\n  def test_rule_patterns_pass\n    test = Test.new(config_path: Pathname(\"querly.yaml\"), stdout: stdout, stderr: stderr)\n\n    def test.load_config\n      Config.load(\n        {\n          \"rules\" => [\n            {\n              \"id\" => \"id1\",\n              \"pattern\" => [\n                \"foo()\",\n                \"foo(1)\"\n              ],\n              \"message\" => \"hello\",\n              \"before\" => [\"self.foo()\", \"foo(1)\"],\n              \"after\" => [\"self.foo(x)\", \"bar()\"]\n            },\n          ]\n        },\n        config_path: Pathname.pwd,\n        root_dir: Pathname.pwd,\n        stderr: stderr\n      )\n    end\n\n    result = test.run\n\n    assert_equal 0, result\n    assert_match %r/All tests green!/, stdout.string\n  end\n\n  def test_rule_patterns_before_after_fail\n    test = Test.new(config_path: Pathname(\"querly.yaml\"), stdout: stdout, stderr: stderr)\n\n    def test.load_config\n      Config.load(\n        {\n          \"rules\" => [\n            {\n              \"id\" => \"id1\",\n              \"pattern\" => [\n                \"foo()\",\n                \"foo(1)\"\n              ],\n              \"message\" => \"hello\",\n              \"before\" => [\"self.foo(x)\", \"foo(1)\"],\n              \"after\" => [\"self.foo()\", \"bar(1)\"]\n            },\n          ]\n        },\n        config_path: Pathname.pwd,\n        root_dir: Pathname.pwd,\n        stderr: stderr\n      )\n    end\n\n    result = test.run\n\n    assert_equal 1, result\n    assert_match %r/id1:\\t1st \\*before\\* example didn't match with any pattern/, stdout.string\n    assert_match %r/id1:\\t1st \\*after\\* example matched with some of patterns/, stdout.string\n    assert_match %r/1 examples found which should not match, but matched/, stdout.string\n    assert_match %r/1 examples found which should match, but didn't/, stdout.string\n  end\n\n  def test_rule_patterns_example_fail\n    test = Test.new(config_path: Pathname(\"querly.yaml\"), stdout: stdout, stderr: stderr)\n\n    def test.load_config\n      Config.load(\n        {\n          \"rules\" => [\n            {\n              \"id\" => \"id1\",\n              \"pattern\" => [\n                \"foo()\",\n                \"foo(1)\"\n              ],\n              \"message\" => \"hello\",\n              \"examples\" => [{ \"before\" => \"self.foo(1)\", \"after\" => \"self.foo(1)\" },\n                             { \"before\" => \"foo(0)\", \"after\" => \"bar(1)\" }\n              ]\n            },\n          ]\n        },\n        config_path: Pathname.pwd,\n        root_dir: Pathname.pwd,\n        stderr: stderr\n      )\n    end\n\n    result = test.run\n\n    assert_equal 1, result\n    assert_match %r/id1:\\tafter of 1st example matched with some of patterns/, stdout.string\n    assert_match %r/id1:\\tbefore of 2nd example didn't match with any pattern/, stdout.string\n    assert_match %r/1 examples found which should not match, but matched/, stdout.string\n    assert_match %r/1 examples found which should match, but didn't/, stdout.string\n  end\n\n  def test_rule_patterns_error\n    test = Test.new(config_path: Pathname(\"querly.yaml\"), stdout: stdout, stderr: stderr)\n\n    def test.load_config\n      Config.load(\n        {\n          \"rules\" =>[\n            {\n              \"id\" => \"id1\",\n              \"pattern\" => \"_\",\n              \"message\" => \"hello\",\n              \"examples\" => [{ \"before\" => \"self.foo(\", \"after\" => \"1)\" }]\n            },\n          ]\n        },\n        config_path: Pathname.pwd,\n        root_dir: Pathname.pwd,\n        stderr: stderr\n      )\n    end\n\n    result = test.run\n\n    assert_equal 1, result\n    assert_match %r/2 examples raised error/, stdout.string\n  end\nend\n"
  },
  {
    "path": "test/config_test.rb",
    "content": "require_relative \"test_helper\"\n\nclass ConfigTest < Minitest::Test\n  include TestHelper\n\n  Config = Querly::Config\n  Preprocessor = Querly::Preprocessor\n\n  def stderr\n    @stderr ||= StringIO.new\n  end\n\n  def test_factory_config_returns_empty_config\n    config = Config::Factory.new({}, config_path: Pathname(\"/foo/bar\"), root_dir: Pathname(\"/foo/bar\"), stderr: stderr).config\n\n    assert_instance_of Config, config\n    assert_empty config.rules\n    assert_empty config.preprocessors\n    assert_equal Pathname(\"/foo/bar\"), config.root_dir\n  end\n\n  def test_factory_config_resturns_config_with_rules\n    config = Config::Factory.new(\n      {\n        \"rules\" => [\n          {\n            \"id\" => \"rule.id\",\n            \"pattern\" => \"_\",\n            \"message\" => \"Hello world\"\n          }\n        ],\n        \"preprocessor\" => {\n          \".slim\" => \"slimrb --compile\"\n        },\n        \"check\" => [\n          {\n            \"path\" => \"/test\",\n            \"rules\" => [\"rails\", \"minitest\"]\n          },\n          {\n            \"path\" => \"/test/integration\",\n            \"rules\" => [\"capybara\", { \"except\" => \"minitest\" }]\n          }\n        ]\n      },\n      config_path: Pathname(\"/foo/bar\"),\n      root_dir: Pathname(\"/foo/bar\"),\n      stderr: stderr\n    ).config\n\n    assert_instance_of Config, config\n    assert_equal [\"rule.id\"], config.rules.map(&:id)\n    assert_equal [\".slim\"], config.preprocessors.keys\n    assert_equal Pathname(\"/foo/bar\"), config.root_dir\n  end\n\n  def test_factory_config_prints_warning_on_tagging\n    Config::Factory.new({ \"tagging\" => [] }, config_path: Pathname(\"/foo/bar\"), root_dir: Pathname(\"/foo/bar\"), stderr: stderr).config\n\n    assert_match %r/tagging is deprecated and ignored/, stderr.string\n  end\n\n  def test_relative_path_from_root\n    config = Config::Factory.new({}, config_path: Pathname(\"/foo/bar\"), root_dir: Pathname(\"/foo/bar\"), stderr: stderr).config\n\n    # Relative path from root_dir\n    assert_equal Pathname(\"a/b/c.rb\"), config.relative_path_from_root(Pathname(\"a/b/c.rb\"))\n    assert_equal Pathname(\"a/b/c.rb\"), config.relative_path_from_root(Pathname(\"a/b/../b/c.rb\"))\n    assert_equal Pathname(\"baz/Rakefile\"), config.relative_path_from_root(Pathname(\"/foo/bar/baz/Rakefile\"))\n\n    # Nonsense...\n    assert_equal Pathname(\"../x.rb\"), config.relative_path_from_root(Pathname(\"../x.rb\"))\n    assert_equal Pathname(\"../../a/b/c.rb\"), config.relative_path_from_root(Pathname(\"/a/b/c.rb\"))\n  end\n\n  def test_loading_rules_from_file\n    hash = { \"import\" => [\n      { \"load\" => \"foo.yml\" },\n      { \"load\" => \"rules/*\" }\n    ]}\n\n    with_config hash do |path|\n      dir = path.parent\n\n      (dir + \"foo.yml\").write(YAML.dump([{ \"id\" => \"rule1\", \"pattern\" => \"_\", \"message\" => \"rule1\" }]))\n      (dir + \"rules\").mkpath\n      (dir + \"rules\" + \"1.yml\").write(YAML.dump([{ \"id\" => \"rule2\", \"pattern\" => \"_\", \"message\" => \"rule1\" }]))\n      (dir + \"rules\" + \"2.yml\").write(YAML.dump([\n                                                   { \"id\" => \"rule3\", \"pattern\" => \"_\", \"message\" => \"rule1\" },\n                                                   { \"id\" => \"rule4\", \"pattern\" => \"_\", \"message\" => \"rule1\" }\n                                                 ]))\n\n      config = Config.load(YAML.load(path.read), config_path: path, root_dir: path, stderr: stderr)\n\n      assert_equal [\"rule1\", \"rule2\", \"rule3\", \"rule4\"], config.rules.map(&:id).sort\n    end\n  end\n\n  def test_analyzer_rules_for_path\n    root_dir = Pathname(\"/foo/bar\")\n\n    config = Config.load(\n      {\n        \"rules\" => [{ \"id\" => \"rule1\", \"pattern\" => \"_\", \"message\" => \"\" },\n                    { \"id\" => \"rule2\", \"pattern\" => \"_\", \"message\" => \"\" },\n                    { \"id\" => \"rule3\", \"pattern\" => \"_\", \"message\" => \"\" },\n                    { \"id\" => \"rule4\", \"pattern\" => \"_\", \"message\" => \"\" }],\n        \"check\" => [{ \"path\" => \"/test\", \"rules\" => [{ \"only\" => \"rule2\" }] },\n                    { \"path\" => \"/test/unit\", \"rules\" => [\"rule3\"] }]\n      },\n      config_path: root_dir,\n      root_dir: root_dir,\n      stderr: stderr\n    )\n\n    assert_equal [\"rule1\", \"rule2\", \"rule3\", \"rule4\"], config.rules_for_path(root_dir + \"foo.rb\").map(&:id)\n    assert_equal [\"rule2\"], config.rules_for_path(root_dir + \"test/foo.rb\").map(&:id)\n    assert_equal [\"rule2\", \"rule3\"], config.rules_for_path(root_dir + \"test/unit/foo.rb\").map(&:id)\n  end\nend\n"
  },
  {
    "path": "test/data/test1/querly.yml",
    "content": "rules:\n  - id: test1.rule1\n    pattern:\n      - foobar\n    message: |\n      Use foo.bar instead of foobar\n\n      foo.bar is not good.\n    justification:\n      - Some reason\n      - Another reason\n    examples:\n      - before: foobar\n        after: foobarbaz\n"
  },
  {
    "path": "test/data/test1/script.rb",
    "content": "foobar()\n"
  },
  {
    "path": "test/data/test2/querly.yml",
    "content": "rules:\n  - id: test2.rule1\n    pattern:\n      - foobar\n    message: Use foo.bar instead of foobar\n"
  },
  {
    "path": "test/data/test2/script.rb",
    "content": "1+\n"
  },
  {
    "path": "test/data/test3/querly.yml",
    "content": "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",
    "content": "p(1)\npp(2)\nppp(3)\n"
  },
  {
    "path": "test/data/test4/querly.yml",
    "content": "rules:\n  - id: test_rule\n    message: This is a test message\n    pattern: _.test\n"
  },
  {
    "path": "test/data/test4/script.rb",
    "content": "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 block parameters since Ruby 2.7\narray.map { _1**2 }\n\n# Pattern matching since Ruby 2.7\ncase array\nin [a, *]\n  puts a\nend\n\n# Arguments forwarding since Ruby 2.7\ndef foo(...)\n  bar(...)\nend\n\n# Extended arguments forwarding since Ruby 3.0\ndef foo2(a, ...)\n  bar2(a, ...)\nend\n\n# One-line pattern matching since Ruby 3.0\n{ a: 1, b: 2, c: 3 } => hash\n\n# Endless method definition since Ruby 3.0\ndef square(x) = x * x\n"
  },
  {
    "path": "test/node_pair_test.rb",
    "content": "require_relative \"test_helper\"\n\nclass NodePairTest < Minitest::Test\n  include TestHelper\n\n  def test_each_subpair\n    node = ruby(\"foo(bar(baz))\")\n    pair = Querly::NodePair.new(node: node)\n\n    nodes = pair.each_subpair.map(&:node)\n\n    assert_equal 3, nodes.count\n    assert nodes.include?(ruby(\"baz\"))\n    assert nodes.include?(ruby(\"bar(baz)\"))\n    assert nodes.include?(ruby(\"foo(bar(baz))\"))\n  end\nend\n"
  },
  {
    "path": "test/pattern_parser_test.rb",
    "content": "require_relative \"test_helper\"\n\nclass PatternParserTest < Minitest::Test\n  include TestHelper\n\n  def test_parser1\n    pattern = parse_expr(\"foo().bar\")\n\n    assert_instance_of E::Send, pattern\n    assert_equal [:bar], pattern.name\n    assert_instance_of A::AnySeq, pattern.args\n\n    assert_instance_of E::Send, pattern.receiver\n    assert_equal [:foo], pattern.receiver.name\n    assert_nil pattern.receiver.args\n  end\n\n  def test_aaa\n    # p Querly::Pattern::Parser.parse(\"foo(!foo: bar)\")\n  end\n\n  def test_ivar\n    pat = parse_expr(\"@\")\n    assert_equal E::Ivar.new(name: nil), pat\n  end\n\n  def test_ivar_with_name\n    pat = parse_expr(\"@x_123A\")\n    assert_equal E::Ivar.new(name: :@x_123A), pat\n  end\n\n  def test_pattern\n    pat = parse_expr(\":racc\")\n    assert_equal E::Literal.new(type: :symbol, values: :racc), pat\n  end\n\n  def test_constant\n    pat = parse_expr(\"E\")\n    assert_equal E::Constant.new(path: [:E]), pat\n  end\n\n  def test_dot3_args\n    pat = parse_expr(\"foo(..., 1, ...)\")\n    assert_equal E::Send.new(receiver: nil,\n                             name: :foo,\n                             args: A::AnySeq.new(tail: A::Expr.new(expr: E::Literal.new(type: :int, values: 1),\n                                                                   tail: A::AnySeq.new)),\n                             block: nil), pat\n  end\n\n  def test_keyword_arg\n    pat = parse_expr(\"foo(!x: 1, ...)\")\n    assert_equal E::Send.new(receiver: nil,\n                             name: :foo,\n                             args: A::KeyValue.new(key: :x,\n                                                   value: E::Literal.new(type: :int, values: 1),\n                                                   negated: true,\n                                                   tail: A::AnySeq.new),\n                             block: nil), pat\n  end\n\n  def test_keyword_arg2\n    pat = parse_expr(\"foo(!X: 1, ...)\")\n    assert_equal E::Send.new(receiver: nil,\n                             name: :foo,\n                             args: A::KeyValue.new(key: :X,\n                                                   value: E::Literal.new(type: :int, values: 1),\n                                                   negated: true,\n                                                   tail: A::AnySeq.new),\n                             block: nil), pat\n  end\n\n  def test_send_with_block\n    pat = parse_expr(\"foo() {}\")\n    assert_equal E::Send.new(receiver: nil,\n                             name: :foo,\n                             args: nil,\n                             block: true), pat\n  end\n\n  def test_send_without_block\n    pat = parse_expr(\"foo() !{}\")\n    assert_equal E::Send.new(receiver: nil,\n                             name: :foo,\n                             args: nil,\n                             block: false), pat\n  end\n\n  def test_send_without_block2\n    pat = parse_expr(\"foo !{}\")\n    assert_equal E::Send.new(receiver: nil,\n                             name: :foo,\n                             args: A::AnySeq.new,\n                             block: false), pat\n  end\n\n  def test_send_with_block_uident\n    pat = parse_expr(\"Foo {}\")\n    assert_equal E::Send.new(receiver: nil,\n                             name: :Foo,\n                             args: A::AnySeq.new,\n                             block: true), pat\n  end\n\n  def test_method_names\n    assert_equal [:[]], parse_expr(\"[]()\").name\n    assert_equal [:[]=], parse_expr(\"[]=()\").name\n    assert_equal [:!], parse_expr(\"!()\").name\n  end\n\n  def test_send\n    assert_equal :f, parse_expr(\"f\").name\n    assert_equal [:f], parse_expr(\"f()\").name\n    assert_equal [:f], parse_expr(\"_.f\").name\n    assert_equal [:f], parse_expr(\"_.f()\").name\n    assert_equal [:F], parse_expr(\"F()\").name\n    assert_equal [:F], parse_expr(\"_.F()\").name\n    assert_equal [:F], parse_expr(\"_.F\").name\n  end\n\n  def test_any_method\n    recv = E::Vcall.new(name: :a)\n    assert_equal E::Send.new(receiver: recv,\n                             name: /.+/,\n                             args: A::AnySeq.new,\n                             block: nil), parse_expr(\"a._\")\n    assert_equal E::Send.new(receiver: recv,\n                             name: /.+/,\n                             args: A::AnySeq.new,\n                             block: true), parse_expr(\"a._{}\")\n    assert_equal E::Send.new(receiver: recv,\n                             name: /.+/,\n                             args: A::AnySeq.new,\n                             block: false), parse_expr(\"a._!{}\")\n    assert_equal E::Send.new(receiver: recv,\n                             name: /.+/,\n                             args: nil,\n                             block: nil), parse_expr(\"a._()\")\n    assert_equal E::Send.new(receiver: recv,\n                             name: /.+/,\n                             args: A::Expr.new(expr: E::Vcall.new(name: :b), tail: nil),\n                             block: nil), parse_expr(\"a._(b)\")\n  end\n\n  def test_method_name\n    assert_equal [:f!], parse_expr(\"f!()\").name\n    assert_equal [:f=], parse_expr(\"f=(3)\").name\n    assert_equal [:f?], parse_expr(\"f?()\").name\n  end\n\n  def test_block_pass\n    pat = parse_expr(\"map(&:id)\")\n    args = pat.args\n\n    assert_instance_of A::BlockPass, args\n    assert_equal E::Literal.new(type: :symbol, values: :id), args.expr\n  end\n\n  def test_vcall\n    pat = parse_expr(\"foo\")\n\n    assert_instance_of E::Vcall, pat\n    assert_equal :foo, pat.name\n  end\n\n  def test_dstr\n    pat = parse_expr(\":dstr:\")\n    assert_instance_of E::Dstr, pat\n  end\n\n  def test_any_kinded\n    pat = parse_kinded(\"foo\")\n    assert_instance_of K::Any, pat\n  end\n\n  def test_conditonal_kinded\n    pat = parse_kinded(\"foo [conditional]\")\n    assert_instance_of K::Conditional, pat\n    refute pat.negated\n  end\n\n  def test_conditional_kinded2\n    pat = parse_kinded(\"foo [!conditional]\")\n    assert_instance_of K::Conditional, pat\n    assert pat.negated\n  end\n\n  def test_discarded_kinded\n    pat = parse_kinded(\"foo [discarded]\")\n    assert_instance_of K::Discarded, pat\n    refute pat.negated\n  end\n\n  def test_discarded_kinded2\n    pat = parse_kinded(\"foo [!discarded]\")\n    assert_instance_of K::Discarded, pat\n    assert pat.negated\n  end\n\n  def test_regexp\n    pat = parse_expr(\":regexp:\")\n    assert_instance_of E::Literal, pat\n    assert_equal :regexp, pat.type\n  end\n\n  def test_any_receiver\n    pat = parse_expr(\"foo...bar\")\n\n    assert_instance_of E::Send, pat\n    assert_equal [:bar], pat.name\n\n    assert_instance_of E::ReceiverContext, pat.receiver\n\n    assert_instance_of E::Vcall, pat.receiver.receiver\n    assert_equal :foo, pat.receiver.receiver.name\n  end\n\n  def test_parse_self\n    pat = parse_expr(\"self\")\n    assert_instance_of E::Self, pat\n  end\n\n  def test_send_with_meta\n    pat = parse_expr(\"'g()\", where: { g: [:foo, :bar] })\n    assert_instance_of E::Send, pat\n    assert_equal [:foo, :bar], pat.name\n  end\n\n  def test_send_with_missing_meta\n    assert_raises Racc::ParseError do\n      parse_expr(\"'g('h())\", where: { g: [:foo, :bar] })\n    end\n  end\n\n  def test_as_method\n    pat = parse_expr(\"self.as\")\n\n    assert_instance_of E::Send, pat\n    assert_equal [:as], pat.name\n  end\n\n  def test_string\n    pat = parse_expr(\":string: as 's\", where: { s: [\"foo\"] })\n    assert_instance_of E::Literal, pat\n    assert_equal :string, pat.type\n    assert_equal [\"foo\"], pat.values\n  end\n\n  def test_string_literal\n    pat = parse_expr('\"foo\"')\n    assert_instance_of E::Literal, pat\n    assert_equal :string, pat.type\n    assert_equal [\"foo\"], pat.values\n  end\n\n  def test_string_literal_with_backslash_escape\n    skip(\"Implement escape sequence in the future\")\n\n    pat = parse_expr('\"foo\\n\"')\n    assert_instance_of E::Literal, pat\n    assert_equal :string, pat.type\n    assert_equal [\"foo\\n\"], pat.values\n  end\n\n  def test_as_something\n    pat = parse_expr(\"assert()\")\n    assert_instance_of E::Send, pat\n    assert_equal [:assert], pat.name\n  end\n\n  def test_as_things2\n    %w(assert true_or_false falsey).each do |word|\n      pat = parse_expr(\"#{word}()\")\n      assert_instance_of E::Send, pat\n      assert_equal [word.to_sym], pat.name\n    end\n  end\nend\n"
  },
  {
    "path": "test/pattern_test_test.rb",
    "content": "require_relative \"test_helper\"\n\nclass PatternTestTest < Minitest::Test\n  include TestHelper\n\n  def assert_node(node, type:)\n    refute_nil node\n    assert_equal type, node.type\n    yield node.children if block_given?\n  end\n\n  def test_ivar_without_name\n    nodes = query_pattern(\"@\", \"@x.foo\")\n    assert_equal 1, nodes.size\n    assert_node nodes.first, type: :ivar do |name, *_|\n      assert_equal :@x, name\n    end\n  end\n\n  def test_ivar_with_name\n    nodes = query_pattern(\"@x\", \"@x + @y\")\n    assert_equal 1, nodes.size\n    assert_node nodes.first, type: :ivar do |name, *_|\n      assert_equal :@x, name\n    end\n  end\n\n  def test_constant\n    nodes = query_pattern(\"C\", \"C.f\")\n    assert_equal 1, nodes.size\n\n    assert_node nodes.first, type: :const do |parent, name|\n      assert_nil parent\n      assert_equal :C, name\n    end\n  end\n\n  def test_constant_with_parent\n    nodes = query_pattern(\"A::B\", \"A::B::C\")\n    assert_node nodes.first, type: :const do |parent, name|\n      assert_equal :B, name\n    end\n  end\n\n  def test_constant_with_parent2\n    nodes = query_pattern(\"B::C\", \"A::B::C\")\n    assert_node nodes.first, type: :const do |parent, name|\n      assert_equal :C, name\n    end\n  end\n\n  def test_int\n    nodes = query_pattern(\":int:\", \"[/1/, 1, 3.0, 1i, 1r]\")\n    assert_equal 1, nodes.size\n    assert_equal [ruby(\"1\")], nodes\n  end\n\n  def test_float\n    nodes = query_pattern(\":float:\", \"['42', /1/, 1, 3.0, 1i, 1r]\")\n    assert_equal 1, nodes.size\n    assert_equal [ruby(\"3.0\")], nodes\n  end\n\n  def test_bool\n    nodes = query_pattern(\":bool:\", \"[true, false, nil]\")\n    assert_equal 2, nodes.size\n    assert_equal [ruby(\"true\"), ruby('false')], nodes\n  end\n\n  def test_symbol\n    nodes = query_pattern(\":symbol:\", \":foo\")\n    assert_node nodes.first, type: :sym do |name, *_|\n      assert_equal :foo, name\n    end\n  end\n\n  def test_symbol2\n    nodes = query_pattern(\":foo\", \":foo.bar(:baz)\")\n    assert_equal 1, nodes.size\n    assert_node nodes.first, type: :sym do |name, *_|\n      assert_equal :foo, name\n    end\n  end\n\n  def test_string\n    nodes = E::Literal.new(type: :string, values: [\"foo\"])\n    assert nodes.test_node(ruby('\"foo\"'))\n    refute nodes.test_node(ruby('\"bar\"'))\n  end\n\n  def test_string2\n    nodes = E::Literal.new(type: :string, values: [\"foo\", \"bar\"])\n    assert nodes.test_node(ruby('\"foo\"'))\n    assert nodes.test_node(ruby('\"bar\"'))\n    refute nodes.test_node(ruby('\"baz\"'))\n  end\n\n  def test_string3\n    nodes = E::Literal.new(type: :string, values: [/foo/])\n    assert nodes.test_node(ruby('\"foo bar\"'))\n    refute nodes.test_node(ruby('\"baz\"'))\n  end\n\n  def test_byte_sequence_string\n    nodes = E::Literal.new(type: :string, values: [/foo/])\n    assert nodes.test_node(ruby('\"\\xfffoo\"'))\n    refute nodes.test_node(ruby('\"\\xffbaz\"'))\n  end\n\n  def test_regexp\n    nodes = query_pattern(\":regexp:\", '[/1/, /#{2}/, 3]')\n    assert_equal 2, nodes.size\n    assert_equal [ruby(\"/1/\"), ruby('/#{2}/')], nodes\n  end\n\n  def test_call_without_args\n    nodes = query_pattern(\"foo\", \"foo(); foo(1)\")\n    assert_equal 2, nodes.size\n    assert_equal ruby(\"foo()\"), nodes[0]\n    assert_equal ruby(\"foo(1)\"), nodes[1]\n  end\n\n  def test_call_with_no_arg\n    nodes = query_pattern(\"foo()\", \"foo(); foo(1)\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo()\"), nodes.first\n  end\n\n  def test_call_with_any_args\n    nodes = query_pattern(\"foo(1, ...)\", \"foo(0); foo(1, 2); foo(x, y)\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo(1, 2)\"), nodes.first\n  end\n\n  def test_call_with_any_expr_arg\n    nodes = query_pattern(\"foo(_)\", \"foo(1, 2); foo(x)\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo(x)\"), nodes.first\n  end\n\n  def test_call_with_not_expr_arg\n    nodes = query_pattern(\"foo(!1)\", \"foo(1); foo(2)\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo(2)\"), nodes.first\n  end\n\n  def test_call_with_kw_args1\n    nodes = query_pattern(\"foo(bar: _)\", \"foo(bar: true)\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo(bar: true)\"), nodes.first\n  end\n\n  def test_call_with_kw_args2\n    nodes = query_pattern(\"foo(bar: _)\", \"foo(bar: true, baz: 1)\")\n    assert_empty nodes\n  end\n\n  def test_call_with_kw_args3\n    nodes = query_pattern(\"foo(bar: _)\", \"foo({ bar: true }, baz: 1)\")\n    assert_empty nodes\n  end\n\n  def test_call_with_kw_args4\n    nodes = query_pattern(\"foo(_, baz: 1)\", \"foo({ bar: true }, baz: 1)\")\n    assert nodes.one?\n    assert_equal ruby(\"foo({ bar: true }, baz: 1)\"), nodes.first\n  end\n\n  def test_call_with_kw_args5\n    nodes = query_pattern(\"foo(..., baz: 1)\", \"foo(0, baz: 1)\")\n    assert nodes.one?\n    assert_equal ruby(\"foo(0, baz: 1)\"), nodes.first\n  end\n\n  def test_call_with_kw_args6\n    nodes = query_pattern(\"foo(..., baz: 1)\", \"foo(1, baz: 2)\")\n    assert_empty nodes\n  end\n\n  def test_call_with_kw_args_rest1\n    nodes = query_pattern(\"foo(bar: _, ...)\", \"foo(bar: true, baz: false)\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo(bar: true, baz: false)\"), nodes.first\n  end\n\n  def test_call_with_kw_args_rest2\n    nodes = query_pattern(\"foo(bar: _, ...)\", \"foo(baz: false)\")\n    assert_empty nodes\n  end\n\n  def test_call_with_negated_kw1\n    nodes = query_pattern(\"foo(!bar: 1)\", \"foo(bar: 3)\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo(bar: 3)\"), nodes.first\n  end\n\n  def test_call_with_negated_kw2\n    nodes = query_pattern(\"foo(!bar: 1, ...)\", \"foo(baz: true)\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo(baz: true)\"), nodes.first\n  end\n\n  def test_call_with_negated_kw3\n    nodes = query_pattern(\"foo(!bar: 1, baz: true)\", \"foo(baz: true)\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo(baz: true)\"), nodes.first\n  end\n\n  def test_call_with_negated_kw4\n    nodes = query_pattern(\"foo(!bar: 1)\", \"foo()\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo()\"), nodes.first\n  end\n\n  def test_call_with_negated_kw5\n    nodes = query_pattern(\"foo(!bar: _)\", \"foo(params)\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo(params)\"), nodes.first\n  end\n\n  def test_call_with_rest_and_kw\n    nodes = query_pattern(\"foo(_, ..., key: _, ...)\", \"foo(1); foo(2, key: 3); foo(key: 4); foo(1,2)\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo(2, key: 3)\"), nodes.first\n  end\n\n\n  def test_call_with_two_dot3\n    nodes = query_pattern(\"foo(..., 1, ...)\",\n                          \"foo(1); foo(1, 2, 3); foo(true, false); foo(2)\")\n\n    assert_equal [ruby(\"foo(1)\"), ruby(\"foo(1,2,3)\")], nodes\n  end\n\n  def test_call_with_block_pass\n    nodes = query_pattern(\"map(&:id)\", \"foo.map(&:id)\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo.map(&:id)\"), nodes.first\n  end\n\n  def test_call_with_names\n    node = E::Send.new(name: [:foo, :bar, /baz/], args: nil, receiver: E::Any.new, block: nil)\n    assert node.test_node(ruby(\"a.foo()\"))\n    assert node.test_node(ruby(\"a.bar()\"))\n    assert node.test_node(ruby(\"a.foo_bar_baz()\"))\n    refute node.test_node(ruby(\"a.test()\"))\n  end\n\n  def test_call_without_receiver\n    nodes = query_pattern(\"foo\", \"foo; bar.foo; bar&.foo\")\n    assert_equal 3, nodes.size\n  end\n\n  def test_call_with_any_receiver\n    nodes = query_pattern(\"_.foo\", \"foo; bar.foo; bar&.foo\")\n    assert_equal 2, nodes.size\n    assert_equal [ruby(\"bar.foo\"), ruby(\"bar&.foo\")], nodes\n  end\n\n  def test_call_any_method\n    nodes = query_pattern(\"foo._\", \"foo; foo.bar; foo.baz; foo&.baz\")\n    assert_equal 3, nodes.size\n    assert_equal [ruby(\"foo.bar\"), ruby(\"foo.baz\"), ruby(\"foo&.baz\")], nodes\n  end\n\n  def test_call_any_method_with_args\n    nodes = query_pattern(\"foo._(baz)\", \"foo.bar(baz); foo.bar(bar); foo&.bar(baz)\")\n    assert_equal 2, nodes.size\n    assert_equal [ruby(\"foo.bar(baz)\"), ruby(\"foo&.bar(baz)\")], nodes\n  end\n\n  def test_call_any_method_with_block\n    nodes = query_pattern(\"foo._{}\", \"foo.bar; foo.baz{}; foo&.foobar{}\")\n    assert_equal 2, nodes.size\n    assert_equal [ruby(\"foo.baz{}\"), ruby(\"foo&.foobar{}\")], nodes\n  end\n\n  def test_vcall\n    # Vcall pattern matches with local variable\n    nodes = query_pattern(\"foo\", \"foo = 1; foo.bar; foo&.bar\")\n    assert_equal 2, nodes.size\n    assert_equal :lvar, nodes.first.type\n    assert_equal :foo, nodes.first.children.first\n    assert_equal :lvar, nodes[1].type\n    assert_equal :foo, nodes[1].children.first\n  end\n\n  def test_vcall2\n    # Vcall pattern matches with method call\n    nodes = query_pattern(\"foo\", \"foo(1,2,3)\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo(1,2,3)\"), nodes.first\n  end\n\n  def test_vcall3\n    # If lvar is receiver, it matches\n    nodes = query_pattern(\"foo\", \"foo = 1; foo.bar()\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo = 1; foo\").children.last, nodes.first\n  end\n\n  def test_vcall4\n    # If lvar is not a receiver, it doesn't match\n    nodes = query_pattern(\"foo\", \"foo = 1; f.bar(foo)\")\n    assert_empty nodes\n  end\n\n  def test_dstr\n    nodes = query_pattern(\":dstr:\", 'foo(\"#{1+2}\")')\n    assert_equal 1, nodes.size\n    assert_equal ruby('\"#{1+2}\"'), nodes.first\n  end\n\n  def test_without_block_option\n    nodes = query_pattern(\"foo()\", \"foo() { foo() }\")\n    assert_equal 2, nodes.size\n  end\n\n  def test_with_block\n    nodes = query_pattern(\"foo() {}\", \"foo do foo() end\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo() do foo() end\"), nodes.first\n  end\n\n  def test_without_block\n    nodes = query_pattern(\"foo() !{}\", \"foo do foo() end\")\n    assert_equal 1, nodes.size\n    assert_equal ruby(\"foo()\"), nodes.first\n  end\n\n  def test_any_receiver1\n    nodes = query_pattern(\"f...g\", \"f.g\")\n    assert_equal [ruby(\"f.g\")], nodes\n  end\n\n  def test_any_receiver2\n    nodes = query_pattern(\"f...h\", \"f.g.h\")\n    assert_equal [ruby(\"f.g.h\")], nodes\n  end\n\n  def test_any_receiver3\n    nodes = query_pattern(\"g...h\", \"f(g).h\")\n    assert_equal [], nodes\n  end\n\n  def test_any_receiver4\n    nodes = query_pattern(\"a...b...c\", \"[a.c.b.d.c]\")\n    assert_equal [ruby(\"a.c.b.d.c\")], nodes\n  end\n\n  def test_any_receiver5\n    nodes = query_pattern(\"a...b\", \"[a.b.b]\")\n    assert_equal Set.new([ruby(\"a.b.b\"), ruby(\"a.b\")]), Set.new(nodes)\n  end\n\n  def test_any_receiver6\n    nodes = query_pattern(\"f...h\", \"f&.g&.h\")\n    assert_equal [ruby(\"f&.g&.h\")], nodes\n  end\n\n  def test_self\n    nodes = query_pattern(\"self.f\", \"f(); self.f(); foo.f()\")\n    assert_equal Set.new([ruby(\"self.f\"), ruby(\"f()\")]), Set.new(nodes)\n  end\n\n  def test_string_value\n    nodes = query_pattern(\"has_many(:symbol: as 'children)\", \"has_many(:children); has_many(:repositories)\", where: { children: [:children] })\n    assert_equal Set.new([ruby(\"has_many :children\")]), Set.new(nodes)\n  end\n\n  def test_conditional_if\n    nodes = query_pattern('foo [conditional]', 'if foo; bar; end')\n    assert_equal 1, nodes.size\n    assert_equal ruby('foo'), nodes.first\n  end\n\n  def test_conditional_while\n    nodes = query_pattern('foo [conditional]', 'while foo; bar; end')\n    assert_equal 1, nodes.size\n    assert_equal ruby('foo'), nodes.first\n  end\n\n  def test_conditional_and\n    nodes = query_pattern('foo [conditional]', 'foo && bar')\n    assert_equal 1, nodes.size\n    assert_equal ruby('foo'), nodes.first\n  end\n\n  def test_conditional_or\n    nodes = query_pattern('foo [conditional]', 'foo || bar')\n    assert_equal 1, nodes.size\n    assert_equal ruby('foo'), nodes.first\n  end\n\n  def test_conditional_csend\n    nodes = query_pattern('foo [conditional]', 'foo&.bar')\n    assert_equal 1, nodes.size\n    assert_equal ruby('foo'), nodes.first\n  end\nend\n"
  },
  {
    "path": "test/preprocessor_test.rb",
    "content": "require_relative \"test_helper\"\n\nclass PreprocessorTest < Minitest::Test\n  Preprocessor = Querly::Preprocessor\n\n  def with_temp_file(content)\n    Tempfile.create(\"querly-preprocessor\") do |io|\n      io.write content\n      io.close\n      yield Pathname(io.path)\n    end\n  end\n\n  def test_preprocessing_succeeded\n    preprocessor = Preprocessor.new(ext: \".foo\", command: \"cat -n\")\n\n    target = with_temp_file(<<-EOS) do |path|\nfoo\nbar\n    EOS\n      preprocessor.run!(path)\n    end\n\n    assert_equal(<<-EXPECTED, target)\n     1\\tfoo\n     2\\tbar\n    EXPECTED\n  end\n\n  def test_preprocessing_failed\n    preprocessor = Preprocessor.new(ext: \".foo\", command: \"grep XYZ\")\n\n    assert_raises Preprocessor::Error do\n      with_temp_file(<<-EOS) do |path|\nfoo\nbar\n    EOS\n        preprocessor.run!(path)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/querly_test.rb",
    "content": "require 'test_helper'\n\nclass QuerlyTest < Minitest::Test\n  def test_that_it_has_a_version_number\n    refute_nil ::Querly::VERSION\n  end\nend\n"
  },
  {
    "path": "test/rule_test.rb",
    "content": "require_relative \"test_helper\"\n\nclass RuleTest < Minitest::Test\n  Rule = Querly::Rule\n  E = Querly::Pattern::Expr\n  K = Querly::Pattern::Kind\n\n  def test_load_rule\n    rule = Rule.load(\n      \"id\" => \"foo.bar.baz\",\n      \"pattern\" => \"@\",\n      \"message\" => \"message1\"\n    )\n\n    assert_equal \"foo.bar.baz\", rule.id\n    assert_equal [\"message1\"], rule.messages\n    assert_equal [E::Ivar.new(name: nil)], rule.patterns.map(&:expr)\n    assert_equal Set.new, rule.tags\n    assert_equal [], rule.examples\n    assert_equal [], rule.justifications\n  end\n\n  def test_load_rule3\n    rule = Rule.load(\n      \"id\" => \"foo.bar.baz\",\n      \"pattern\" => [\"@\", \"_\"],\n      \"message\" => \"message1\",\n      \"tags\" => [\"tag1\", \"tag2\"],\n      \"examples\" => { \"before\" => \"foo\", \"after\" => \"bar\"},\n      \"justification\" => [\"some\", \"message\"]\n    )\n\n    assert_equal \"foo.bar.baz\", rule.id\n    assert_equal [\"message1\"], rule.messages\n    assert_equal [E::Ivar.new(name: nil), E::Any.new], rule.patterns.map(&:expr)\n    assert_equal Set.new([\"tag1\", \"tag2\"]), rule.tags\n    assert_equal [Rule::Example.new(before: \"foo\", after: \"bar\")], rule.examples\n    assert_equal [\"some\", \"message\"], rule.justifications\n  end\n\n  def test_load_rule2\n    rule = Rule.load(\n      \"id\" => \"foo.bar.baz\",\n      \"pattern\" => [\"@\", \"_\"],\n      \"message\" => \"message1\",\n      \"tags\" => [\"tag1\", \"tag2\"],\n      \"examples\" => [{ \"before\" => \"foo\", \"after\" => \"bar\"},\n                     { \"before\" => \"foo\" },\n                     { \"after\" => \"bar\" }],\n      \"justification\" => [\"some\", \"message\"]\n    )\n\n    assert_equal \"foo.bar.baz\", rule.id\n    assert_equal [\"message1\"], rule.messages\n    assert_equal [E::Ivar.new(name: nil), E::Any.new], rule.patterns.map(&:expr)\n    assert_equal Set.new([\"tag1\", \"tag2\"]), rule.tags\n    assert_equal [Rule::Example.new(before: \"foo\", after: \"bar\"),\n                  Rule::Example.new(before: \"foo\", after: nil),\n                  Rule::Example.new(before: nil, after: \"bar\")], rule.examples\n    assert_equal [\"some\", \"message\"], rule.justifications\n  end\n\n  def test_load_rule_before_and_after_examples\n    rule = Rule.load(\n      \"id\" => \"foo.bar.baz\",\n      \"pattern\" => [\"@\", \"_\"],\n      \"message\" => \"message1\",\n      \"tags\" => [\"tag1\", \"tag2\"],\n      \"before\" => [\"foo\", \"bar\"],\n      \"after\" => [\"baz\", \"a\"],\n      \"justification\" => [\"some\", \"message\"]\n    )\n\n    assert_equal \"foo.bar.baz\", rule.id\n    assert_equal [\"message1\"], rule.messages\n    assert_equal [E::Ivar.new(name: nil), E::Any.new], rule.patterns.map(&:expr)\n    assert_equal Set.new([\"tag1\", \"tag2\"]), rule.tags\n    assert_equal [], rule.examples\n    assert_equal [\"foo\", \"bar\"], rule.before_examples\n    assert_equal [\"baz\", \"a\"], rule.after_examples\n    assert_equal [\"some\", \"message\"], rule.justifications\n  end\n\n  def test_load_rule_raises_on_pattern_syntax_error\n    exn = assert_raises Rule::PatternSyntaxError do\n      Rule.load(\"id\" => \"id1\", \"pattern\" => \"syntax error\")\n    end\n\n    assert_match(/Pattern syntax error: rule=id1, index=0, pattern=syntax error, where={}:/, exn.message)\n  end\n\n  def test_load_rule_raises_without_id\n    exn = assert_raises Rule::InvalidRuleHashError do\n      Rule.load(\"pattern\" => \"_\", \"message\" => \"message1\")\n    end\n\n    assert_equal \"id is missing\", exn.message\n  end\n\n  def test_load_rule_raises_without_pattern\n    exn = assert_raises Rule::InvalidRuleHashError do\n      Rule.load(\"id\" => \"id1\", \"message\" => \"hello world\")\n    end\n\n    assert_equal \"pattern is missing\", exn.message\n  end\n\n  def test_load_rule_raises_without_message\n    exn = assert_raises Rule::InvalidRuleHashError do\n      Rule.load(\"id\" => \"id1\", \"pattern\" => \"foobar\")\n    end\n\n    assert_equal \"message is missing\", exn.message\n  end\n\n  def test_load_including_pattern_with_where_clause\n    rule = Rule.load(\"id\" => \"id1\", \"message\" => \"message\", \"pattern\" => { 'subject' => \"'g()'\", 'where' => { 'g' => [\"foo\", \"/bar/\"] } })\n    assert_equal 1, rule.patterns.size\n\n    pattern = rule.patterns.first\n    assert_equal [\"foo\", /bar/], pattern.expr.name\n  end\n\n  def test_load_rule_raises_exception_on_invalid_example\n    assert_raises Rule::InvalidRuleHashError do\n      Rule.load(\"id\" => \"id1\", \"message\" => \"message\", \"pattern\" => { 'subject' => \"'g()'\", 'where' => { 'g' => [\"foo\", \"/bar/\"] } }, \"examples\" => [{}])\n    end\n  end\n\n  def test_translate_where\n    w = YAML.load(<<-YAML)\n- foo\n- /bar/\n- :baz\n- 1\n- 2.0\n    YAML\n\n    assert_equal [\"foo\", /bar/, :baz, 1, 2.0], Rule.translate_where(w)\n  end\nend\n"
  },
  {
    "path": "test/script_enumerator_test.rb",
    "content": "require_relative \"test_helper\"\n\nclass ScriptEnumeratorTest < Minitest::Test\n  include TestHelper\n\n  ScriptEnumerator = Querly::ScriptEnumerator\n  Config = Querly::Config\n\n  def test_parsing_ruby\n    mktmpdir do |dir|\n      config = Config.new(rules: [], preprocessors: {}, root_dir: dir, checks: [])\n      e = ScriptEnumerator.new(paths: nil, config: config, threads: 1)\n\n      ruby_path = dir + \"foo.rb\"\n      ruby_path.write <<-EOR\ndef foo()\nend\n      EOR\n\n      e.__send__(:load_script_from_path, ruby_path) do |path, script|\n        assert_equal ruby_path, path\n        assert_instance_of Querly::Script, script\n      end\n    end\n  end\n\n  def test_parse_error_ruby\n    mktmpdir do |dir|\n      config = Config.new(rules: [], preprocessors: {}, root_dir: dir, checks: [])\n      e = ScriptEnumerator.new(paths: nil, config: config, threads: 1)\n\n      ruby_path = dir + \"foo.rb\"\n      ruby_path.write <<-EOR\ndef foo()\n      EOR\n\n      e.__send__(:load_script_from_path, ruby_path) do |path, script|\n        assert_equal ruby_path, path\n        assert_instance_of Parser::SyntaxError, script\n      end\n    end\n  end\n\n  def test_no_parse_error_on_invalid_utf8_sequence\n    mktmpdir do |dir|\n      config = Config.new(rules: [], preprocessors: {}, root_dir: dir, checks: [])\n      e = ScriptEnumerator.new(paths: nil, config: config, threads: 1)\n\n      ruby_path = dir + \"foo.rb\"\n      ruby_path.write '\"\\xFF\"'\n\n      e.__send__(:load_script_from_path, ruby_path) do |path, script|\n        assert_equal ruby_path, path\n        assert_instance_of Querly::Script, script\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/smoke_test.rb",
    "content": "require_relative \"test_helper\"\n\nrequire \"open3\"\nrequire \"tmpdir\"\n\nclass SmokeTest < Minitest::Test\n  include UnificationAssertion\n\n  def dirs\n    @dirs ||= [root]\n  end\n\n  def push_dir(dir)\n    dirs.push dir\n    yield\n  ensure\n    dirs.pop\n  end\n\n  def sh!(*args, **options)\n    output, _, status = Open3.capture3(*args, { chdir: dirs.last.to_s }.merge(options))\n\n    unless status.success?\n      raise \"Failed: #{args.inspect}\"\n      puts output\n    end\n\n    output\n  end\n\n  def querly_path\n    Pathname(__dir__) + \"../exe/querly\"\n  end\n\n  def run_querly(*args, **options)\n    sh!(*args.unshift(querly_path.to_s), **options)\n  end\n\n  def sh(*args, **options)\n    Open3.capture3(*args, { chdir: dirs.last.to_s }.merge(options))\n  end\n\n  def root\n    (Pathname(__dir__) + \"../\").realpath\n  end\n\n  def test_help\n    run_querly(\"help\")\n  end\n\n  def test_rules\n    run_querly(\"--config=sample.yml\", \"rules\")\n  end\n\n  def test_check\n    run_querly(\"--config=sample.yml\", \"check\", \".\")\n  end\n\n  def test_test\n    run_querly(\"--config=sample.yml\", \"test\")\n  end\n\n  def test_console\n    run_querly(\"console\", \".\", stdin_data: [\"help\", \"reload\", \"find self.p\", \"quit\"].join(\"\\n\"))\n  end\n\n  def test_version\n    run_querly(\"version\")\n  end\n\n  def test_check_json_format\n    push_dir root + \"test/data/test1\" do\n      output = JSON.parse(run_querly(\"check\", \"--format=json\", \".\"), symbolize_names: true)\n      assert_unifiable({\n                         issues: [\n                           {\n                             script: \"script.rb\",\n                             location: { start: [1,0], end: [1,8] },\n                             rule: {\n                               id: \"test1.rule1\",\n                               messages: [\"Use foo.bar instead of foobar\\n\\nfoo.bar is not good.\\n\"],\n                               justifications: [\"Some reason\", \"Another reason\"],\n                               examples: [{ before: \"foobar\", after: \"foobarbaz\" }],\n                             }\n                           }\n                         ],\n                         errors: []\n                       }, output)\n    end\n  end\n\n  def test_check_with_rule\n    push_dir root + \"test/data/test1\" do\n      output = JSON.parse(run_querly(\"check\", \"--format=json\", \"--rule=test1.rule1\", \".\"), symbolize_names: true)\n      assert_unifiable({\n                         issues: [\n                           {\n                             script: \"script.rb\",\n                             location: { start: [1,0], end: [1,8] },\n                             rule: {\n                               id: \"test1.rule1\",\n                               messages: [\"Use foo.bar instead of foobar\\n\\nfoo.bar is not good.\\n\"],\n                               justifications: [\"Some reason\", \"Another reason\"],\n                               examples: [{ before: \"foobar\", after: \"foobarbaz\" }],\n                             }\n                           }\n                         ],\n                         errors: []\n                       }, output)\n\n      output = JSON.parse(run_querly(\"check\", \"--format=json\", \"--rule=test1\", \".\"), symbolize_names: true)\n      assert_unifiable({\n                         issues: [\n                           {\n                             script: \"script.rb\",\n                             location: { start: [1,0], end: [1,8] },\n                             rule: {\n                               id: \"test1.rule1\",\n                               messages: [\"Use foo.bar instead of foobar\\n\\nfoo.bar is not good.\\n\"],\n                               justifications: [\"Some reason\", \"Another reason\"],\n                               examples: [{ before: \"foobar\", after: \"foobarbaz\" }],\n                             }\n                           }\n                         ],\n                         errors: []\n                       }, output)\n\n      output = JSON.parse(run_querly(\"check\", \"--format=json\", \"--rule=no_such_rule\", \".\"), symbolize_names: true)\n      assert_unifiable({\n                         issues: [],\n                         errors: []\n                       }, output)\n    end\n  end\n\n  def test_check_when_omit_paths\n    test_dir = root + \"test/data/test1\"\n    push_dir test_dir do\n      output = JSON.parse(run_querly(\"check\", \"--format=json\"), symbolize_names: true)\n      assert_unifiable({\n                         issues: [\n                           {\n                             script: (test_dir + \"script.rb\").cleanpath.to_s,\n                             location: { start: [1,0], end: [1,8] },\n                             rule: {\n                               id: \"test1.rule1\",\n                               messages: [\"Use foo.bar instead of foobar\\n\\nfoo.bar is not good.\\n\"],\n                               justifications: [\"Some reason\", \"Another reason\"],\n                               examples: [{ before: \"foobar\", after: \"foobarbaz\" }],\n                             }\n                           }\n                         ],\n                         errors: []\n                       }, output)\n    end\n  end\n\n  def test_check_json_format_with_not_a_config_file\n    push_dir root + \"test/data/test1\" do\n      out, err, status = sh(\"bundle\", \"exec\", \"querly\", \"check\", \"--format=json\", \"--config=no.such.config\", \".\")\n\n      refute status.success?\n      assert_match(/Configuration file no.such.config does not look a file./, err)\n      assert_unifiable({ issues: [], errors: [] }, JSON.parse(out, symbolize_names: true))\n    end\n  end\n\n  def test_run3\n    push_dir root + \"test/data/test2\" do\n      out, _, status = sh(\"bundle\", \"exec\", \"querly\", \"check\", \"--format=json\", \".\")\n\n      assert status.success?\n\n      # Syntax error recorded in errors\n      assert_unifiable({\n                         issues: [],\n                         errors: [{ path: \"script.rb\", error: :_ }]\n                       }, JSON.parse(out, symbolize_names: true))\n    end\n  end\n\n  def test_run4\n    push_dir root + \"test/data/test3\" do\n      out, _, status = sh(\"bundle\", \"exec\", \"querly\", \"check\", \"--format=json\", \".\")\n\n      assert status.success?\n      assert_unifiable({\n                         issues: [\n                           {\n                             script: \"script.rb\",\n                             location: { start: [1, 0], end: [1, 4] },\n                             rule: { id: \"test.pppp\", messages: :_, justifications: :_, examples: :_ }\n                           },\n                           {\n                             script: \"script.rb\",\n                             location: { start: [2, 0], end: [2, 5] },\n                             rule: { id: \"test.pppp\", messages: :_, justifications: :_, examples: :_ }\n                           },\n                           {\n                             script: \"script.rb\",\n                             location: { start: [3, 0], end: [3, 6] },\n                             rule: { id: \"test.pppp\", messages: :_, justifications: :_, examples: :_ }\n                           },\n                         ],\n                         errors: []\n                       }, JSON.parse(out, symbolize_names: true))\n    end\n  end\n\n  def test_load_file_not_found\n    push_dir root + \"test/data/test3\" do\n      out, _, status = sh(querly_path.to_s, \"check\", \"--format=json\", \"not_found.rb\")\n\n      assert status.success?\n      assert_unifiable({\n                         issues: [],\n                         errors: [{ path: \"not_found.rb\", error: :_ }]\n                       }, JSON.parse(out, symbolize_names: true))\n    end\n  end\n\n  def mktmpdir\n    tmp = root + \"tmp\"\n    tmp.mkdir unless tmp.directory?\n    Dir.mktmpdir(\"a\", root + \"tmp\") do |dir|\n      yield Pathname(dir)\n    end\n  end\n\n  def test_init\n    mktmpdir do |path|\n      push_dir path do\n        _, _ = run_querly(\"init\")\n        assert_operator (path + \"querly.yml\"), :file?\n        _, _ = run_querly(\"test\", \"--config=querly.yml\")\n      end\n    end\n  end\n\n  def test_check_text_format_when_syntax_error\n    push_dir root + \"test/data/test2\" do\n      out, err, status = sh(querly_path.to_s, \"check\", \"--format=text\", \".\")\n\n      assert status.success?\n      assert_empty out\n      assert_equal [\n        \"Failed to load script: script.rb\\n\",\n        \"script.rb:2:1: error: unexpected token $end\\n\",\n        \"script.rb:2: \\n\",\n        \"script.rb:2: \\n\",\n      ].join, err\n    end\n  end\n\n  def test_check_new_syntax\n    push_dir root + \"test/data/test4\" do\n      out, err, status = sh(querly_path.to_s, \"check\", \".\")\n\n      assert status.success?\n      assert_empty out\n      assert_empty err\n    end\n  end\nend\n"
  },
  {
    "path": "test/test_helper.rb",
    "content": "$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)\nrequire 'querly'\nrequire \"querly/cli\"\nrequire \"querly/cli/test\"\n\nrequire 'minitest/autorun'\nrequire \"tmpdir\"\nrequire \"unification_assertion\"\nrequire \"tempfile\"\n\nRainbow.enabled = false\n\nmodule TestHelper\n  E = Querly::Pattern::Expr\n  A = Querly::Pattern::Argument\n  K = Querly::Pattern::Kind\n\n  def parse_expr(src, where: {})\n    Querly::Pattern::Parser.parse(src, where: where).expr\n  end\n\n  def parse_kinded(src, where: {})\n    Querly::Pattern::Parser.parse(src, where: where)\n  end\n\n  def query_pattern(pattern, src, where: {})\n    pat = parse_kinded(pattern, where: where)\n\n    analyzer = Querly::Analyzer.new(config: nil, rule: nil)\n    analyzer.scripts << Querly::Script.new(path: Pathname(\"(input)\"),\n                                           node: Parser::Ruby30.parse(src, \"(input)\"))\n\n    [].tap do |result|\n      analyzer.find(pat) do |script, pair|\n        result << pair.node\n      end\n    end\n  end\n\n  def ruby(src)\n    Querly::Script.load(path: \"(input)\", source: src).node\n  end\n\n  def with_config(hash)\n    Dir.mktmpdir do |dir|\n      path = Pathname(dir) + \"querly.yml\"\n      path.write(YAML.dump(hash))\n      yield path\n    end\n  end\n\n  def stdout\n    @stdout ||= StringIO.new\n  end\n\n  def mktmpdir\n    Dir.mktmpdir do |dir|\n      yield Pathname(dir)\n    end\n  end\nend\n"
  }
]