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