From 7efae1ca34a66bc2bc5503bb637678d7794c1aa6 Mon Sep 17 00:00:00 2001 From: Marc-Andre Lafortune Date: Mon, 10 Aug 2020 11:55:17 -0400 Subject: [PATCH] Add `RegexpNode#each_capture` and `parsed_tree` --- CHANGELOG.md | 2 +- lib/rubocop.rb | 1 + lib/rubocop/ext/regexp_node.rb | 46 ++++++++++++++++++++++++++++ spec/rubocop/ext/regexp_node_spec.rb | 35 +++++++++++++++++++++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 lib/rubocop/ext/regexp_node.rb create mode 100644 spec/rubocop/ext/regexp_node_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e3a906b32aa..5f4b7f46f15c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ * Add new `Lint/TrailingCommaInAttributeDeclaration` cop. ([@drenmi][]) * [#8578](https://github.com/rubocop-hq/rubocop/pull/8578): Add `:restore_registry` context and `stub_cop_class` helper class. ([@marcandre][]) * [#8579](https://github.com/rubocop-hq/rubocop/pull/8579): Add `Cop.documentation_url`. ([@marcandre][]) - +* [#8510](https://github.com/rubocop-hq/rubocop/pull/8510): Add `RegexpNode#each_capture` and `parsed_tree`. ([@marcandre][]) ### Bug fixes diff --git a/lib/rubocop.rb b/lib/rubocop.rb index c458bd750e80..69f3438bc677 100644 --- a/lib/rubocop.rb +++ b/lib/rubocop.rb @@ -9,6 +9,7 @@ require 'unicode/display_width/no_string_ext' require 'rubocop-ast' require_relative 'rubocop/ast_aliases' +require_relative 'rubocop/ext/regexp_node' require_relative 'rubocop/version' diff --git a/lib/rubocop/ext/regexp_node.rb b/lib/rubocop/ext/regexp_node.rb new file mode 100644 index 000000000000..3c8ede20f420 --- /dev/null +++ b/lib/rubocop/ext/regexp_node.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module RuboCop + module Ext + # Extensions to AST::RegexpNode for our cached parsed regexp info + module RegexpNode + ANY = Object.new + def ANY.==(_) + true + end + private_constant :ANY + + class << self + attr_reader :parsed_cache + end + @parsed_cache = {} + + # @return [Regexp::Expression::Root, nil] + def parsed_tree + return if interpolation? + + str = content + Ext::RegexpNode.parsed_cache[str] ||= begin + Regexp::Parser.parse(str) + rescue StandardError + nil + end + end + + def each_capture(named: ANY) + return enum_for(__method__, named: named) unless block_given? + + parsed_tree&.traverse do |event, exp, _index| + yield(exp) if event == :enter && + named == exp.respond_to?(:name) && + exp.respond_to?(:capturing?) && + exp.capturing? + end + + self + end + + AST::RegexpNode.include self + end + end +end diff --git a/spec/rubocop/ext/regexp_node_spec.rb b/spec/rubocop/ext/regexp_node_spec.rb new file mode 100644 index 000000000000..bb1e4f13f932 --- /dev/null +++ b/spec/rubocop/ext/regexp_node_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'timeout' + +RSpec.describe RuboCop::Ext::RegexpNode do + let(:source) { '/(hello)(?world)(?:not captured)/' } + let(:processed_source) { parse_source(source) } + let(:ast) { processed_source.ast } + let(:node) { ast } + + describe '#each_capture' do + subject(:captures) { node.each_capture(**arg).to_a } + + let(:named) { be_instance_of(Regexp::Expression::Group::Named) } + let(:positional) { be_instance_of(Regexp::Expression::Group::Capture) } + + context 'when called without argument' do + let(:arg) { {} } + + it { is_expected.to match [positional, named] } + end + + context 'when called with a `named: false`' do + let(:arg) { { named: false } } + + it { is_expected.to match [positional] } + end + + context 'when called with a `named: true`' do + let(:arg) { { named: true } } + + it { is_expected.to match [named] } + end + end +end