diff --git a/changelog/new_add_document_dynamic_eval_definition_cop.md b/changelog/new_add_document_dynamic_eval_definition_cop.md new file mode 100644 index 00000000000..3f839991295 --- /dev/null +++ b/changelog/new_add_document_dynamic_eval_definition_cop.md @@ -0,0 +1 @@ +* [#8940](https://github.com/rubocop-hq/rubocop/pull/8940): Add new `Style/DocumentDynamicEvalDefinition` cop. ([@fatkodima][]) diff --git a/config/default.yml b/config/default.yml index e137d8fe264..04e88c1a1a3 100644 --- a/config/default.yml +++ b/config/default.yml @@ -2946,6 +2946,14 @@ Style/DisableCopsWithinSourceCodeDirective: Enabled: false VersionAdded: '0.82' +Style/DocumentDynamicEvalDefinition: + Description: >- + When using `class_eval` (or other `eval`) with string interpolation, + add a comment block showing its appearance if interpolated. + StyleGuide: '#eval-comment-docs' + Enabled: pending + VersionAdded: '1.1' + Style/Documentation: Description: 'Document classes and non-namespace modules.' Enabled: true diff --git a/docs/modules/ROOT/pages/cops.adoc b/docs/modules/ROOT/pages/cops.adoc index b36cc370fd6..df4e69e0729 100644 --- a/docs/modules/ROOT/pages/cops.adoc +++ b/docs/modules/ROOT/pages/cops.adoc @@ -374,6 +374,7 @@ In the following section you find all available cops: * xref:cops_style.adoc#styledefwithparentheses[Style/DefWithParentheses] * xref:cops_style.adoc#styledir[Style/Dir] * xref:cops_style.adoc#styledisablecopswithinsourcecodedirective[Style/DisableCopsWithinSourceCodeDirective] +* xref:cops_style.adoc#styledocumentdynamicevaldefinition[Style/DocumentDynamicEvalDefinition] * xref:cops_style.adoc#styledocumentation[Style/Documentation] * xref:cops_style.adoc#styledocumentationmethod[Style/DocumentationMethod] * xref:cops_style.adoc#styledoublecopdisabledirective[Style/DoubleCopDisableDirective] diff --git a/docs/modules/ROOT/pages/cops_style.adoc b/docs/modules/ROOT/pages/cops_style.adoc index b3752a7e447..a5de9fd3e37 100644 --- a/docs/modules/ROOT/pages/cops_style.adoc +++ b/docs/modules/ROOT/pages/cops_style.adoc @@ -2195,6 +2195,64 @@ def fixed_method_name_and_no_rubocop_comments end ---- +== Style/DocumentDynamicEvalDefinition + +|=== +| Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged + +| Pending +| Yes +| No +| 1.1 +| - +|=== + +When using `class_eval` (or other `eval`) with string interpolation, +add a comment block showing its appearance if interpolated (a practice used in Rails code). + +=== Examples + +[source,ruby] +---- +# from activesupport/lib/active_support/core_ext/string/output_safety.rb + +# bad +UNSAFE_STRING_METHODS.each do |unsafe_method| + if 'String'.respond_to?(unsafe_method) + class_eval <<-EOT, __FILE__, __LINE__ + 1 + def #{unsafe_method}(*params, &block) + to_str.#{unsafe_method}(*params, &block) + end + + def #{unsafe_method}!(*params) + @dirty = true + super + end + EOT + end +end + +# good +UNSAFE_STRING_METHODS.each do |unsafe_method| + if 'String'.respond_to?(unsafe_method) + class_eval <<-EOT, __FILE__, __LINE__ + 1 + def #{unsafe_method}(*params, &block) # def capitalize(*params, &block) + to_str.#{unsafe_method}(*params, &block) # to_str.capitalize(*params, &block) + end # end + + def #{unsafe_method}!(*params) # def capitalize!(*params) + @dirty = true # @dirty = true + super # super + end # end + EOT + end +end +---- + +=== References + +* https://rubystyle.guide#eval-comment-docs + == Style/Documentation |=== diff --git a/lib/rubocop.rb b/lib/rubocop.rb index bb3569ca409..daddae89079 100644 --- a/lib/rubocop.rb +++ b/lib/rubocop.rb @@ -424,6 +424,7 @@ require_relative 'rubocop/cop/style/disable_cops_within_source_code_directive' require_relative 'rubocop/cop/style/documentation_method' require_relative 'rubocop/cop/style/documentation' +require_relative 'rubocop/cop/style/document_dynamic_eval_definition' require_relative 'rubocop/cop/style/double_cop_disable_directive' require_relative 'rubocop/cop/style/double_negation' require_relative 'rubocop/cop/style/each_for_simple_loop' diff --git a/lib/rubocop/cop/commissioner.rb b/lib/rubocop/cop/commissioner.rb index 98e00fb6ef9..7f3aa25a931 100644 --- a/lib/rubocop/cop/commissioner.rb +++ b/lib/rubocop/cop/commissioner.rb @@ -65,13 +65,13 @@ def initialize(cops, forces = [], options = {}) c = '#' if NO_CHILD_NODES.include?(node_type) # has Children? class_eval(<<~RUBY, __FILE__, __LINE__ + 1) - def on_#{node_type}(node) - trigger_responding_cops(:on_#{node_type}, node) - #{r} trigger_restricted_cops(:on_#{node_type}, node) - #{c} super(node) - #{c} trigger_responding_cops(:after_#{node_type}, node) - #{c}#{r} trigger_restricted_cops(:after_#{node_type}, node) - end + def on_#{node_type}(node) # def on_send(node) + trigger_responding_cops(:on_#{node_type}, node) # trigger_responding_cops(:on_send, node) + #{r} trigger_restricted_cops(:on_#{node_type}, node) # trigger_restricted_cops(:on_send, node) + #{c} super(node) # super(node) + #{c} trigger_responding_cops(:after_#{node_type}, node) # trigger_responding_cops(:after_send, node) + #{c}#{r} trigger_restricted_cops(:after_#{node_type}, node) # trigger_restricted_cops(:after_send, node) + end # end RUBY end diff --git a/lib/rubocop/cop/style/document_dynamic_eval_definition.rb b/lib/rubocop/cop/style/document_dynamic_eval_definition.rb new file mode 100644 index 00000000000..5ed39c0b5ea --- /dev/null +++ b/lib/rubocop/cop/style/document_dynamic_eval_definition.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Style + # When using `class_eval` (or other `eval`) with string interpolation, + # add a comment block showing its appearance if interpolated (a practice used in Rails code). + # + # @example + # # from activesupport/lib/active_support/core_ext/string/output_safety.rb + # + # # bad + # UNSAFE_STRING_METHODS.each do |unsafe_method| + # if 'String'.respond_to?(unsafe_method) + # class_eval <<-EOT, __FILE__, __LINE__ + 1 + # def #{unsafe_method}(*params, &block) + # to_str.#{unsafe_method}(*params, &block) + # end + # + # def #{unsafe_method}!(*params) + # @dirty = true + # super + # end + # EOT + # end + # end + # + # # good + # UNSAFE_STRING_METHODS.each do |unsafe_method| + # if 'String'.respond_to?(unsafe_method) + # class_eval <<-EOT, __FILE__, __LINE__ + 1 + # def #{unsafe_method}(*params, &block) # def capitalize(*params, &block) + # to_str.#{unsafe_method}(*params, &block) # to_str.capitalize(*params, &block) + # end # end + # + # def #{unsafe_method}!(*params) # def capitalize!(*params) + # @dirty = true # @dirty = true + # super # super + # end # end + # EOT + # end + # end + # + class DocumentDynamicEvalDefinition < Base + MSG = 'Add a comment block showing its appearance if interpolated.' + + RESTRICT_ON_SEND = %i[eval class_eval module_eval instance_eval].freeze + + def on_send(node) + arg_node = node.first_argument + return unless arg_node&.dstr_type? + + add_offense(node.loc.selector) unless comment_docs?(arg_node) + end + + private + + def comment_docs?(node) + node.each_child_node(:begin).all? do |begin_node| + source_line = processed_source.lines[begin_node.first_line - 1] + source_line.match?(/\s*#[^{]+/) + end + end + end + end + end +end diff --git a/spec/rubocop/cop/style/document_dynamic_eval_definition_spec.rb b/spec/rubocop/cop/style/document_dynamic_eval_definition_spec.rb new file mode 100644 index 00000000000..7de41586637 --- /dev/null +++ b/spec/rubocop/cop/style/document_dynamic_eval_definition_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Style::DocumentDynamicEvalDefinition do + subject(:cop) { described_class.new } + + it 'registers an offense when using eval-type method with string interpolation without comment docs' do + expect_offense(<<~RUBY) + class_eval <<-EOT, __FILE__, __LINE__ + 1 + ^^^^^^^^^^ Add a comment block showing its appearance if interpolated. + def \#{unsafe_method}(*params, &block) + to_str.\#{unsafe_method}(*params, &block) + end + EOT + RUBY + end + + it 'does not register an offense when using eval-type method without string interpolation' do + expect_no_offenses(<<~RUBY) + class_eval <<-EOT, __FILE__, __LINE__ + 1 + def capitalize(*params, &block) + to_str.capitalize(*params, &block) + end + EOT + RUBY + end + + it 'does not register an offense when using eval-type method with string interpolation with comment docs' do + expect_no_offenses(<<~RUBY) + class_eval <<-EOT, __FILE__, __LINE__ + 1 + def \#{unsafe_method}(*params, &block) # def capitalize(*params, &block) + to_str.\#{unsafe_method}(*params, &block) # to_str.capitalize(*params, &block) + end # end + EOT + RUBY + end +end