diff --git a/CHANGELOG.md b/CHANGELOG.md index c04d0bd8169..80253828946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * [#7978](https://github.com/rubocop-hq/rubocop/pull/7978): Add new option `OnlyFor` to the `Bundler/GemComment` cop. ([@ric2b][]) * [#8063](https://github.com/rubocop-hq/rubocop/issues/8063): Add new `AllowedNames` option for `Naming/ClassAndModuleCamelCase`. ([@tejasbubane][]) * New option `--display-only-failed` that can be used with `--format junit`. Speeds up test report processing for large codebases and helps address the sorts of concerns raised at [mikian/rubocop-junit-formatter #18](https://github.com/mikian/rubocop-junit-formatter/issues/18). ([@burnettk][]) +* [#7746](https://github.com/rubocop-hq/rubocop/issues/7746): Add new `Lint/MixedRegexpCaptureTypes` cop. ([@pocke][]) ### Bug fixes diff --git a/config/default.yml b/config/default.yml index 73231e26bce..394db3bd634 100644 --- a/config/default.yml +++ b/config/default.yml @@ -1533,6 +1533,11 @@ Lint/MissingCopEnableDirective: # .inf for any size MaximumRangeSize: .inf +Lint/MixedRegexpCaptureTypes: + Description: 'Do not mix named captures and numbered captures in a Regexp literal.' + Enabled: pending + VersionAdded: '0.85' + Lint/MultipleComparison: Description: "Use `&&` operator to compare multiple values." Enabled: true diff --git a/docs/modules/ROOT/pages/cops.adoc b/docs/modules/ROOT/pages/cops.adoc index 46c111dff1c..0ef0ef01c58 100644 --- a/docs/modules/ROOT/pages/cops.adoc +++ b/docs/modules/ROOT/pages/cops.adoc @@ -214,6 +214,7 @@ In the following section you find all available cops: * xref:cops_lint.adoc#lintliteralininterpolation[Lint/LiteralInInterpolation] * xref:cops_lint.adoc#lintloop[Lint/Loop] * xref:cops_lint.adoc#lintmissingcopenabledirective[Lint/MissingCopEnableDirective] +* xref:cops_lint.adoc#lintmixedregexpcapturetypes[Lint/MixedRegexpCaptureTypes] * xref:cops_lint.adoc#lintmultiplecomparison[Lint/MultipleComparison] * xref:cops_lint.adoc#lintnestedmethoddefinition[Lint/NestedMethodDefinition] * xref:cops_lint.adoc#lintnestedpercentliteral[Lint/NestedPercentLiteral] diff --git a/docs/modules/ROOT/pages/cops_lint.adoc b/docs/modules/ROOT/pages/cops_lint.adoc index 20301ac61ab..afc2248b5da 100644 --- a/docs/modules/ROOT/pages/cops_lint.adoc +++ b/docs/modules/ROOT/pages/cops_lint.adoc @@ -1552,6 +1552,35 @@ x += 1 | Float |=== +== Lint/MixedRegexpCaptureTypes + +|=== +| Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged + +| Pending +| Yes +| No +| 0.85 +| - +|=== + +Do not mix named captures and numbered captures in a Regexp literal +because numbered capture is ignored if they're mixed. +Replace numbered captures with non-capturing groupings or +named captures. + + # bad + /(?FOO)(BAR)/ + + # good + /(?FOO)(?BAR)/ + + # good + /(?FOO)(?:BAR)/ + + # good + /(FOO)(BAR)/ + == Lint/MultipleComparison |=== diff --git a/legacy-docs/cops.md b/legacy-docs/cops.md index c6088078a14..d03595ad19e 100644 --- a/legacy-docs/cops.md +++ b/legacy-docs/cops.md @@ -212,6 +212,7 @@ In the following section you find all available cops: * [Lint/LiteralInInterpolation](cops_lint.md#lintliteralininterpolation) * [Lint/Loop](cops_lint.md#lintloop) * [Lint/MissingCopEnableDirective](cops_lint.md#lintmissingcopenabledirective) +* [Lint/MixedRegexpCaptureTypes](cops_lint.md#lintmixedregexpcapturetypes) * [Lint/MultipleComparison](cops_lint.md#lintmultiplecomparison) * [Lint/NestedMethodDefinition](cops_lint.md#lintnestedmethoddefinition) * [Lint/NestedPercentLiteral](cops_lint.md#lintnestedpercentliteral) diff --git a/legacy-docs/cops_lint.md b/legacy-docs/cops_lint.md index 06c30b72fe4..31a564e9d7f 100644 --- a/legacy-docs/cops_lint.md +++ b/legacy-docs/cops_lint.md @@ -1210,6 +1210,29 @@ Name | Default value | Configurable values --- | --- | --- MaximumRangeSize | `Infinity` | Float +## Lint/MixedRegexpCaptureTypes + +Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged +--- | --- | --- | --- | --- +Pending | Yes | No | 0.85 | - + +Do not mix named captures and numbered captures in a Regexp literal +because numbered capture is ignored if they're mixed. +Replace numbered captures with non-capturing groupings or +named captures. + + # bad + /(?FOO)(BAR)/ + + # good + /(?FOO)(?BAR)/ + + # good + /(?FOO)(?:BAR)/ + + # good + /(FOO)(BAR)/ + ## Lint/MultipleComparison Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged diff --git a/lib/rubocop.rb b/lib/rubocop.rb index 6851359ad80..30a886a9c71 100644 --- a/lib/rubocop.rb +++ b/lib/rubocop.rb @@ -4,6 +4,8 @@ require 'English' require 'set' +require 'forwardable' +require 'regexp_parser' require 'unicode/display_width/no_string_ext' require 'rubocop-ast' require_relative 'rubocop/ast_aliases' @@ -266,6 +268,7 @@ require_relative 'rubocop/cop/lint/literal_in_interpolation' require_relative 'rubocop/cop/lint/loop' require_relative 'rubocop/cop/lint/missing_cop_enable_directive' +require_relative 'rubocop/cop/lint/mixed_regexp_capture_types' require_relative 'rubocop/cop/lint/multiple_comparison' require_relative 'rubocop/cop/lint/nested_method_definition' require_relative 'rubocop/cop/lint/nested_percent_literal' diff --git a/lib/rubocop/cop/lint/mixed_regexp_capture_types.rb b/lib/rubocop/cop/lint/mixed_regexp_capture_types.rb new file mode 100644 index 00000000000..d23b94a4620 --- /dev/null +++ b/lib/rubocop/cop/lint/mixed_regexp_capture_types.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Lint + # Do not mix named captures and numbered captures in a Regexp literal + # because numbered capture is ignored if they're mixed. + # Replace numbered captures with non-capturing groupings or + # named captures. + # + # # bad + # /(?FOO)(BAR)/ + # + # # good + # /(?FOO)(?BAR)/ + # + # # good + # /(?FOO)(?:BAR)/ + # + # # good + # /(FOO)(BAR)/ + # + class MixedRegexpCaptureTypes < Cop + MSG = 'Do not mix named captures and numbered captures ' \ + 'in a Regexp literal.' + + def on_regexp(node) + tree = Regexp::Parser.parse(node.content) + return unless named_capture?(tree) + return unless numbered_capture?(tree) + + add_offense(node) + end + + private + + def named_capture?(tree) + tree.each_expression.any? do |e| + e.instance_of?(Regexp::Expression::Group::Capture) + end + end + + def numbered_capture?(tree) + tree.each_expression.any? do |e| + e.instance_of?(Regexp::Expression::Group::Named) + end + end + end + end + end +end diff --git a/rubocop.gemspec b/rubocop.gemspec index 43d39cd6070..a2a58dd78a7 100644 --- a/rubocop.gemspec +++ b/rubocop.gemspec @@ -36,6 +36,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency('parallel', '~> 1.10') s.add_runtime_dependency('parser', '>= 2.7.0.1') s.add_runtime_dependency('rainbow', '>= 2.2.2', '< 4.0') + s.add_runtime_dependency('regexp_parser', '>= 1.7') s.add_runtime_dependency('rexml') s.add_runtime_dependency('rubocop-ast', '>= 0.0.3') s.add_runtime_dependency('ruby-progressbar', '~> 1.7') diff --git a/spec/rubocop/cop/lint/mixed_regexp_capture_types_spec.rb b/spec/rubocop/cop/lint/mixed_regexp_capture_types_spec.rb new file mode 100644 index 00000000000..1d7adfb9e7c --- /dev/null +++ b/spec/rubocop/cop/lint/mixed_regexp_capture_types_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Lint::MixedRegexpCaptureTypes do + subject(:cop) { described_class.new(config) } + + let(:config) { RuboCop::Config.new } + + it 'registers an offense when both of named and numbered captures are used' do + expect_offense(<<~RUBY) + /(?bar)(baz)/ + ^^^^^^^^^^^^^^^^^^ Do not mix named captures and numbered captures in a Regexp literal. + RUBY + end + + it 'does not register offense to a regexp with named capture only' do + expect_no_offenses(<<~RUBY) + /(?foo?bar)/ + RUBY + end + + it 'does not register offense to a regexp with numbered capture only' do + expect_no_offenses(<<~RUBY) + /(foo)(bar)/ + RUBY + end + + it 'does not register offense to a regexp with named capture and ' \ + 'non-capturing group' do + expect_no_offenses(<<~RUBY) + /(?bar)(?:bar)/ + RUBY + end +end