rubocop-rbs_inline 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/hooks/self-review.sh +89 -0
  3. data/.claude/settings.json +8 -0
  4. data/.rubocop.yml +9 -3
  5. data/CHANGELOG.md +19 -0
  6. data/CLAUDE.md +9 -14
  7. data/README.md +54 -6
  8. data/config/default.yml +3 -4
  9. data/lib/rubocop/cop/rbs_inline_cops.rb +15 -7
  10. data/lib/rubocop/cop/style/rbs_inline/data_class_comment_alignment.rb +2 -9
  11. data/lib/rubocop/cop/style/rbs_inline/data_define_with_block.rb +3 -9
  12. data/lib/rubocop/cop/style/rbs_inline/embedded_rbs_spacing.rb +3 -2
  13. data/lib/rubocop/cop/style/rbs_inline/invalid_comment.rb +2 -2
  14. data/lib/rubocop/cop/style/rbs_inline/invalid_types.rb +1 -0
  15. data/lib/rubocop/cop/style/rbs_inline/keyword_separator.rb +2 -2
  16. data/lib/rubocop/cop/style/rbs_inline/method_comment_spacing.rb +3 -2
  17. data/lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb +2 -9
  18. data/lib/rubocop/cop/style/rbs_inline/missing_struct_class_annotation.rb +2 -16
  19. data/lib/rubocop/cop/style/rbs_inline/missing_type_annotation.rb +3 -16
  20. data/lib/rubocop/cop/style/rbs_inline/mixin/annotation_keywords.rb +16 -0
  21. data/lib/rubocop/cop/style/rbs_inline/{ast_utils.rb → mixin/ast_utils.rb} +16 -0
  22. data/lib/rubocop/cop/style/rbs_inline/{comment_parser.rb → mixin/comment_parser.rb} +8 -0
  23. data/lib/rubocop/cop/style/rbs_inline/mixin/data_class_matcher.rb +22 -0
  24. data/lib/rubocop/cop/style/rbs_inline/{data_struct_helper.rb → mixin/data_struct_helper.rb} +14 -5
  25. data/lib/rubocop/cop/style/rbs_inline/mixin/file_filter.rb +110 -0
  26. data/lib/rubocop/cop/style/rbs_inline/{source_code_helper.rb → mixin/source_code_helper.rb} +3 -5
  27. data/lib/rubocop/cop/style/rbs_inline/mixin/struct_class_matcher.rb +32 -0
  28. data/lib/rubocop/cop/style/rbs_inline/parameters_separator.rb +2 -2
  29. data/lib/rubocop/cop/style/rbs_inline/redundant_annotation_with_skip.rb +2 -11
  30. data/lib/rubocop/cop/style/rbs_inline/redundant_instance_variable_annotation.rb +1 -0
  31. data/lib/rubocop/cop/style/rbs_inline/redundant_type_annotation.rb +6 -15
  32. data/lib/rubocop/cop/style/rbs_inline/require_rbs_inline_comment.rb +74 -23
  33. data/lib/rubocop/cop/style/rbs_inline/struct_class_comment_alignment.rb +2 -16
  34. data/lib/rubocop/cop/style/rbs_inline/struct_new_with_block.rb +3 -9
  35. data/lib/rubocop/cop/style/rbs_inline/unmatched_annotations.rb +31 -6
  36. data/lib/rubocop/cop/style/rbs_inline/untyped_instance_variable.rb +16 -6
  37. data/lib/rubocop/cop/style/rbs_inline/variable_comment_spacing.rb +3 -2
  38. data/lib/rubocop/rbs_inline/version.rb +1 -1
  39. data/sig/gems/rubocop/rubocop.rbs +8 -0
  40. data/sig/rubocop/cop/style/rbs_inline/data_class_comment_alignment.rbs +4 -5
  41. data/sig/rubocop/cop/style/rbs_inline/data_define_with_block.rbs +4 -5
  42. data/sig/rubocop/cop/style/rbs_inline/embedded_rbs_spacing.rbs +2 -0
  43. data/sig/rubocop/cop/style/rbs_inline/invalid_comment.rbs +4 -3
  44. data/sig/rubocop/cop/style/rbs_inline/invalid_types.rbs +2 -0
  45. data/sig/rubocop/cop/style/rbs_inline/keyword_separator.rbs +4 -3
  46. data/sig/rubocop/cop/style/rbs_inline/method_comment_spacing.rbs +2 -0
  47. data/sig/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rbs +4 -5
  48. data/sig/rubocop/cop/style/rbs_inline/missing_struct_class_annotation.rbs +4 -10
  49. data/sig/rubocop/cop/style/rbs_inline/missing_type_annotation.rbs +4 -8
  50. data/sig/rubocop/cop/style/rbs_inline/mixin/annotation_keywords.rbs +16 -0
  51. data/sig/rubocop/cop/style/rbs_inline/{ast_utils.rbs → mixin/ast_utils.rbs} +9 -1
  52. data/sig/rubocop/cop/style/rbs_inline/{class_comment_alignment.rbs → mixin/class_comment_alignment.rbs} +1 -1
  53. data/sig/rubocop/cop/style/rbs_inline/{comment_parser.rbs → mixin/comment_parser.rbs} +5 -1
  54. data/sig/rubocop/cop/style/rbs_inline/mixin/data_class_matcher.rbs +18 -0
  55. data/sig/rubocop/cop/style/rbs_inline/{data_struct_helper.rbs → mixin/data_struct_helper.rbs} +6 -1
  56. data/sig/rubocop/cop/style/rbs_inline/mixin/file_filter.rbs +63 -0
  57. data/sig/rubocop/cop/style/rbs_inline/{missing_class_annotation.rbs → mixin/missing_class_annotation.rbs} +1 -1
  58. data/sig/rubocop/cop/style/rbs_inline/{source_code_helper.rbs → mixin/source_code_helper.rbs} +1 -1
  59. data/sig/rubocop/cop/style/rbs_inline/mixin/struct_class_matcher.rbs +26 -0
  60. data/sig/rubocop/cop/style/rbs_inline/parameters_separator.rbs +4 -3
  61. data/sig/rubocop/cop/style/rbs_inline/redundant_annotation_with_skip.rbs +4 -4
  62. data/sig/rubocop/cop/style/rbs_inline/redundant_instance_variable_annotation.rbs +2 -0
  63. data/sig/rubocop/cop/style/rbs_inline/redundant_type_annotation.rbs +4 -4
  64. data/sig/rubocop/cop/style/rbs_inline/require_rbs_inline_comment.rbs +30 -5
  65. data/sig/rubocop/cop/style/rbs_inline/struct_class_comment_alignment.rbs +4 -10
  66. data/sig/rubocop/cop/style/rbs_inline/struct_new_with_block.rbs +4 -5
  67. data/sig/rubocop/cop/style/rbs_inline/unmatched_annotations.rbs +13 -0
  68. data/sig/rubocop/cop/style/rbs_inline/untyped_instance_variable.rbs +10 -0
  69. data/sig/rubocop/cop/style/rbs_inline/variable_comment_spacing.rbs +2 -0
  70. metadata +24 -17
  71. data/sig/rubocop/cop/style/rbs_inline/redundant_argument_type.rbs +0 -85
  72. data/sig/rubocop/cop/style/rbs_inline/redundant_return_type.rbs +0 -109
  73. /data/lib/rubocop/cop/style/rbs_inline/{class_comment_alignment.rb → mixin/class_comment_alignment.rb} +0 -0
  74. /data/lib/rubocop/cop/style/rbs_inline/{missing_class_annotation.rb → mixin/missing_class_annotation.rb} +0 -0
@@ -13,8 +13,16 @@ module RuboCop
13
13
  module CommentParser
14
14
  attr_reader :parsed_comments #: Array[RBS::Inline::AnnotationParser::ParsingResult]
15
15
 
16
+ # Fallback for cops that include CommentParser without prepending FileFilter;
17
+ # `FileFilter` overrides this to return the real per-file decision.
18
+ def rbs_inline_file_skipped? #: bool
19
+ false
20
+ end
21
+
16
22
  # Parse comments from the source code
17
23
  def parse_comments #: Array[RBS::Inline::AnnotationParser::ParsingResult]
24
+ return @parsed_comments = [] if rbs_inline_file_skipped?
25
+
18
26
  parsed_result = Prism.parse(processed_source.buffer.source)
19
27
  @parsed_comments = RBS::Inline::AnnotationParser.parse(parsed_result.comments)
20
28
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ module RbsInline
7
+ # Matcher for `Data.define` calls, shared by the cops that check
8
+ # `Data.define` class definitions.
9
+ module DataClassMatcher
10
+ private
11
+
12
+ # @rbs node: RuboCop::AST::SendNode
13
+ def struct_like_class?(node) #: bool
14
+ return false unless node.method_name == :define
15
+
16
+ (r = node.receiver).is_a?(RuboCop::AST::ConstNode) && r.short_name == :Data
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -57,10 +57,13 @@ module RuboCop
57
57
  comment unless comment&.text&.match?(/\A#:/)
58
58
  end
59
59
 
60
+ # The column is determined by the attribute arguments only; non-attribute
61
+ # arguments (e.g. the `Struct.new` name or a `keyword_init:` keyword argument)
62
+ # never carry an annotation and so must not influence the alignment.
60
63
  # @rbs node: RuboCop::AST::SendNode
61
64
  def annotation_column(node) #: Integer
62
65
  last_arg = node.arguments.last
63
- max_end_col = node.arguments.map do |arg|
66
+ max_end_col = attr_arguments(node).map do |arg|
64
67
  comma_length = arg.equal?(last_arg) ? 0 : 1
65
68
  arg.location.column + source!(arg).length + comma_length
66
69
  end.max || 0
@@ -68,10 +71,12 @@ module RuboCop
68
71
  max_end_col + 2
69
72
  end
70
73
 
74
+ # Only attribute arguments are considered, for the same reason as
75
+ # {#annotation_column}.
71
76
  # @rbs node: RuboCop::AST::SendNode
72
77
  def longest_argname(node) #: String
73
- last_index = node.arguments.size - 1
74
- args = node.arguments.each_with_index.map { |a, i| i < last_index ? "#{a.source}," : a.source.to_s }
78
+ last_arg = node.arguments.last
79
+ args = attr_arguments(node).map { _1.equal?(last_arg) ? _1.source.to_s : "#{_1.source}," }
75
80
  args.max_by(&:length) || ""
76
81
  end
77
82
 
@@ -84,8 +89,12 @@ module RuboCop
84
89
  args_source = node.arguments.each_with_index.map do |arg, i|
85
90
  comma = i < last_index ? "," : ""
86
91
  prefix = "#{base_indent} #{arg.source}#{comma}"
87
- padding = " " * (longest.length - source!(arg).length - comma.length + 2)
88
- attr_argument?(arg) ? "#{prefix}#{padding}#: untyped" : prefix
92
+ if attr_argument?(arg)
93
+ padding = " " * (longest.length - source!(arg).length - comma.length + 2)
94
+ "#{prefix}#{padding}#: untyped"
95
+ else
96
+ prefix
97
+ end
89
98
  end.join("\n")
90
99
 
91
100
  "(\n#{args_source}\n#{base_indent})"
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ module RbsInline
7
+ # Filters files a cop reports offenses on, based on `Mode` configuration.
8
+ #
9
+ # When `Mode` is `opt_in`, offenses are only reported for files that contain
10
+ # a `# rbs_inline: enabled` magic comment. When `Mode` is `opt_out`, all files
11
+ # are checked unless they contain `# rbs_inline: disabled`. When `Mode` is not
12
+ # set, all files are checked (legacy behavior).
13
+ #
14
+ # This module is designed to be `prepend`ed to a cop so that it can short-circuit
15
+ # the cop's heavy work (annotation parsing via `parse_comments`) and suppress any
16
+ # residual offense reporting for files that should be skipped.
17
+ #
18
+ # @rbs module-self RuboCop::Cop::Base
19
+ module FileFilter
20
+ # @rbs! type mode = :opt_in | :opt_out
21
+
22
+ MAGIC_COMMENT_ENABLED = /\A# rbs_inline: enabled\R?\z/ #: Regexp
23
+ MAGIC_COMMENT_DISABLED = /\A# rbs_inline: disabled\R?\z/ #: Regexp
24
+
25
+ SUPPORTED_MODES = %i[opt_in opt_out].freeze #: Array[mode]
26
+
27
+ # Tracks Mode values that have already been reported as invalid, so we
28
+ # only emit one warning per typo across the whole rubocop run instead of
29
+ # one per (file × cop).
30
+ # @rbs self.@warned_invalid_modes: Hash[String, bool]
31
+
32
+ @warned_invalid_modes = {} # rubocop:disable Style/RbsInline/UntypedInstanceVariable
33
+
34
+ # @rbs raw: untyped
35
+ def self.warn_invalid_mode(raw) #: void
36
+ key = raw.to_s
37
+ return if @warned_invalid_modes[key]
38
+
39
+ @warned_invalid_modes[key] = true
40
+ Kernel.warn(
41
+ "[rubocop-rbs_inline] Style/RbsInline Mode #{raw.inspect} is not supported. " \
42
+ "Expected one of: #{SUPPORTED_MODES.join(", ")}. Filtering is disabled for this run."
43
+ )
44
+ end
45
+
46
+ # @rbs @rbs_inline_skip_file: bool
47
+
48
+ def on_new_investigation #: void
49
+ @rbs_inline_skip_file = skip_by_mode?
50
+ super
51
+ end
52
+
53
+ # @rbs *args: untyped
54
+ # @rbs **kwargs: untyped
55
+ def add_offense(*args, **kwargs, &) #: void
56
+ return if @rbs_inline_skip_file
57
+
58
+ super
59
+ end
60
+
61
+ # Exposes the FileFilter's per-file skip decision as a proper method so
62
+ # helper modules (e.g. `CommentParser`) can consult it explicitly instead
63
+ # of poking at `@rbs_inline_skip_file` directly.
64
+ def rbs_inline_file_skipped? #: bool
65
+ @rbs_inline_skip_file == true
66
+ end
67
+
68
+ private
69
+
70
+ def skip_by_mode? #: bool
71
+ mode = configured_mode
72
+ return false if mode.nil?
73
+
74
+ # `# rbs_inline: disabled` always opts a file out, regardless of Mode
75
+ # (matches rbs-inline itself: rbs-inline skips disabled files in both
76
+ # opt_in and opt_out modes).
77
+ return true if rbs_inline_disabled?
78
+
79
+ mode == :opt_in && !rbs_inline_enabled?
80
+ end
81
+
82
+ def configured_mode #: mode?
83
+ raw = cop_config["Mode"]
84
+ return nil if raw.nil?
85
+
86
+ # `raw.to_s.to_sym` handles YAML-native Integer / Boolean without
87
+ # raising NoMethodError on the plain `to_sym`.
88
+ mode = raw.to_s.to_sym
89
+ return mode if SUPPORTED_MODES.include?(mode)
90
+
91
+ FileFilter.warn_invalid_mode(raw)
92
+ nil
93
+ end
94
+
95
+ # Iterate the already-materialized `processed_source.comments` so this
96
+ # matches RequireRbsInlineComment's detection exactly (line endings,
97
+ # indented comments, heredoc/string content are all handled correctly
98
+ # by the parser).
99
+ def rbs_inline_enabled? #: bool
100
+ processed_source.comments.any? { _1.text.match?(MAGIC_COMMENT_ENABLED) }
101
+ end
102
+
103
+ def rbs_inline_disabled? #: bool
104
+ processed_source.comments.any? { _1.text.match?(MAGIC_COMMENT_DISABLED) }
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -12,11 +12,9 @@ module RuboCop
12
12
  # Convert byte offset to character offset
13
13
  # @rbs byte_offset: Integer
14
14
  def character_offset(byte_offset) #: Integer
15
- source = processed_source.buffer.source.dup.force_encoding("ASCII")
16
- text = source[...byte_offset] or raise
17
- text.force_encoding(processed_source.buffer.source.encoding).size
18
- rescue StandardError
19
- byte_offset
15
+ source = processed_source.buffer.source
16
+ prefix = source.byteslice(0, byte_offset) or raise
17
+ prefix.size
20
18
  end
21
19
 
22
20
  # @rbs pos: Integer
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ module RbsInline
7
+ # Matcher for `Struct.new` calls, shared by the cops that check `Struct.new`
8
+ # class definitions.
9
+ #
10
+ # Also overrides {DataStructHelper#attr_argument?} for `Struct.new` semantics,
11
+ # so cops that include both must include this module last.
12
+ module StructClassMatcher
13
+ private
14
+
15
+ # @rbs node: RuboCop::AST::SendNode
16
+ def struct_like_class?(node) #: bool
17
+ return false unless node.method_name == :new
18
+
19
+ (r = node.receiver).is_a?(RuboCop::AST::ConstNode) && r.short_name == :Struct
20
+ end
21
+
22
+ # `Struct.new` treats a leading string argument as the struct name, so only
23
+ # symbol arguments are attributes.
24
+ # @rbs arg: RuboCop::AST::Node
25
+ def attr_argument?(arg) #: bool
26
+ arg.sym_type?
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -20,13 +20,13 @@ module RuboCop
20
20
  # # good
21
21
  # # @rbs %a{pure}
22
22
  class ParametersSeparator < Base
23
+ prepend FileFilter
23
24
  extend AutoCorrector
25
+ include AnnotationKeywords
24
26
  include RangeHelp
25
27
 
26
28
  MSG = "Use `:` as a separator between parameter name and type."
27
29
 
28
- # refs: https://github.com/soutaro/rbs-inline/blob/main/lib/rbs/inline/annotation_parser/tokenizer.rb
29
- RBS_INLINE_KEYWORDS = %w[inherits override use module-self generic skip module class].freeze #: Array[String]
30
30
  RBS_INLINE_REGEXP_KEYWORDS = [/%a{(\w|-)+}/, /%a\((\w|-)+\)/, /%a\[(\w|-)+\]/].freeze #: Array[Regexp]
31
31
 
32
32
  def on_new_investigation #: void
@@ -58,7 +58,9 @@ module RuboCop
58
58
  # end
59
59
  #
60
60
  class RedundantAnnotationWithSkip < Base
61
+ prepend FileFilter
61
62
  extend AutoCorrector
63
+ include ASTUtils
62
64
  include CommentParser
63
65
  include RangeHelp
64
66
  include SourceCodeHelper
@@ -182,17 +184,6 @@ module RuboCop
182
184
  corrector.remove(removal_range)
183
185
  end
184
186
  end
185
-
186
- # Returns the last line of the method parameter list (the closing ) line, or the def line if no parens).
187
- # @rbs node: RuboCop::AST::DefNode
188
- def method_parameter_list_end_line(node) #: Integer
189
- args_node = case node.type
190
- when :def then node.children[1]
191
- when :defs then node.children[2]
192
- else raise
193
- end
194
- args_node.location.end&.line || node.location.line
195
- end
196
187
  end
197
188
  end
198
189
  end
@@ -28,6 +28,7 @@ module RuboCop
28
28
  # attr_reader :foo
29
29
  #
30
30
  class RedundantInstanceVariableAnnotation < Base
31
+ prepend FileFilter
31
32
  extend AutoCorrector
32
33
  include ASTUtils
33
34
  include CommentParser
@@ -80,7 +80,9 @@ module RuboCop
80
80
  # end
81
81
  #
82
82
  class RedundantTypeAnnotation < Base
83
+ prepend FileFilter
83
84
  extend AutoCorrector
85
+ include ASTUtils
84
86
  include CommentParser
85
87
  include ConfigurableEnforcedStyle
86
88
  include RangeHelp
@@ -129,17 +131,6 @@ module RuboCop
129
131
  end
130
132
  end
131
133
 
132
- # Returns the last line of the method parameter list (the closing ) line, or the def line if no parens).
133
- # @rbs node: RuboCop::AST::DefNode
134
- def method_parameter_list_end_line(node) #: Integer
135
- args_node = case node.type
136
- when :def then node.children[1]
137
- when :defs then node.children[2]
138
- else raise
139
- end
140
- args_node.location.end&.line || node.location.line
141
- end
142
-
143
134
  # @rbs def_line: Integer
144
135
  # @rbs parameter_list_end_line: Integer
145
136
  def check_return_type_redundancy(def_line, parameter_list_end_line) #: void
@@ -165,7 +156,7 @@ module RuboCop
165
156
  last = comments.last or return
166
157
  range = first.loc.expression.join(last.loc.expression)
167
158
  add_offense(range, message: MSG_METHOD_TYPE_SIGNATURE) do |corrector|
168
- unexpected_style_detected(:method_type_signature)
159
+ no_acceptable_style!
169
160
  corrector.remove(range_by_whole_lines(range, include_final_newline: true))
170
161
  end
171
162
  end
@@ -178,7 +169,7 @@ module RuboCop
178
169
  def add_offense_for_doc_style_param(annotation) #: void
179
170
  range = annotation_range(annotation) or return
180
171
  add_offense(range, message: MSG_DOC_STYLE_PARAM) do |corrector|
181
- unexpected_style_detected(:doc_style)
172
+ no_acceptable_style!
182
173
  corrector.remove(range_by_whole_lines(range, include_final_newline: true))
183
174
  end
184
175
  end
@@ -199,7 +190,7 @@ module RuboCop
199
190
  # @rbs comment: Parser::Source::Comment
200
191
  def add_offense_for_trailing_return(comment) #: void
201
192
  add_offense(comment, message: MSG_TRAILING_RETURN) do |corrector|
202
- unexpected_style_detected(:return_type_annotation)
193
+ no_acceptable_style!
203
194
  removal_range = range_with_surrounding_space(range: comment.loc.expression, side: :left, newlines: false)
204
195
  corrector.remove(removal_range)
205
196
  end
@@ -209,7 +200,7 @@ module RuboCop
209
200
  def add_offense_for_doc_style_return(annotation) #: void
210
201
  range = annotation_range(annotation) or return
211
202
  add_offense(range, message: MSG_DOC_STYLE_RETURN) do |corrector|
212
- unexpected_style_detected(:doc_style)
203
+ no_acceptable_style!
213
204
  corrector.remove(range_by_whole_lines(range, include_final_newline: true))
214
205
  end
215
206
  end
@@ -10,7 +10,7 @@ module RuboCop
10
10
  # opt-out (processes all files by default). This cop enforces consistency in which
11
11
  # mode your codebase uses.
12
12
  #
13
- # @example EnforcedStyle: always (default)
13
+ # @example Mode: opt_in
14
14
  # # bad
15
15
  # # (no rbs_inline comment)
16
16
  # class Foo
@@ -26,7 +26,17 @@ module RuboCop
26
26
  # class Foo
27
27
  # end
28
28
  #
29
- # @example EnforcedStyle: never
29
+ # @example Mode: opt_in, AllowMissingComment: true
30
+ # # good - the cop does not enforce the magic comment
31
+ # class Foo
32
+ # end
33
+ #
34
+ # # good
35
+ # # rbs_inline: enabled
36
+ # class Foo
37
+ # end
38
+ #
39
+ # @example Mode: opt_out
30
40
  # # bad
31
41
  # # rbs_inline: enabled
32
42
  # class Foo
@@ -49,16 +59,29 @@ module RuboCop
49
59
  MSG_MISSING = "Missing `# rbs_inline:` magic comment."
50
60
  MSG_FORBIDDEN = "Remove `# rbs_inline:` magic comment."
51
61
 
62
+ # @rbs self.@enforced_style_deprecation_warned: bool
63
+
64
+ @enforced_style_deprecation_warned = false # rubocop:disable Style/RbsInline/UntypedInstanceVariable
65
+
66
+ def self.enforced_style_deprecation_warned? #: bool
67
+ @enforced_style_deprecation_warned == true
68
+ end
69
+
70
+ def self.mark_enforced_style_deprecation_warned! #: void
71
+ @enforced_style_deprecation_warned = true
72
+ end
73
+
52
74
  def on_new_investigation #: void
53
75
  return if processed_source.buffer.source.empty?
54
76
 
77
+ warn_deprecated_enforced_style
78
+
55
79
  magic_comment = find_rbs_inline_magic_comment
56
80
  return if disabled?(magic_comment)
57
81
 
58
- if style == :always
59
- check_always_style(magic_comment)
60
- elsif style == :never
61
- check_never_style(magic_comment)
82
+ case effective_mode
83
+ when :opt_in then check_opt_in(magic_comment)
84
+ when :opt_out then check_opt_out(magic_comment)
62
85
  end
63
86
  end
64
87
 
@@ -76,41 +99,45 @@ module RuboCop
76
99
  end
77
100
 
78
101
  # @rbs magic_comment: Parser::Source::Comment?
79
- def check_always_style(magic_comment) #: void
80
- # disabled is already filtered out by early return
81
- # magic_comment is either nil or enabled
102
+ def check_opt_in(magic_comment) #: void
82
103
  return if magic_comment
104
+ return if allow_missing_comment?
83
105
 
84
- # Insert after the first comment block or at the beginning of the file
85
106
  insert_position = find_insert_position
86
107
  add_offense(first_line_range, message: MSG_MISSING) do |corrector|
87
108
  insert_range = Parser::Source::Range.new(processed_source.buffer, insert_position, insert_position)
88
- corrector.insert_before(insert_range, "# rbs_inline: enabled\n")
109
+ corrector.insert_before(insert_range, insertion_text(insert_position))
89
110
  end
90
111
  end
91
112
 
92
113
  # @rbs magic_comment: Parser::Source::Comment?
93
- def check_never_style(magic_comment) #: void
94
- # disabled is already filtered out by early return
95
- # magic_comment is either nil or enabled
114
+ def check_opt_out(magic_comment) #: void
96
115
  return unless magic_comment
97
116
 
98
117
  add_offense(magic_comment.source_range, message: MSG_FORBIDDEN) do |corrector|
99
- # Remove the entire line including newline
100
118
  range = range_with_surrounding_space(magic_comment.source_range, side: :right, newlines: true)
101
119
  corrector.remove(range)
102
120
  end
103
121
  end
104
122
 
105
123
  def find_insert_position #: Integer
106
- # Find the end of the first comment block (e.g., magic comments)
107
- # and insert after it
108
124
  first_comment = processed_source.comments.first
109
- # If the first comment doesn't exist or doesn't start at line 1, insert at the beginning
110
125
  return 0 unless first_comment&.source_range&.first_line == 1
111
126
 
112
127
  last_comment_in_block = find_last_comment_in_first_block
113
- last_comment_in_block.source_range.end_pos + 1
128
+ # `end_pos` points at the newline terminating the comment, so `+ 1` moves past it.
129
+ # A file without a trailing newline has none, so clamp to the end of the buffer.
130
+ (last_comment_in_block.source_range.end_pos + 1).clamp(0, processed_source.buffer.source.length)
131
+ end
132
+
133
+ # The insert position lands at the end of the buffer when the file has no trailing
134
+ # newline. The magic comment then needs its own newline to stay on a separate line.
135
+ # @rbs insert_position: Integer
136
+ def insertion_text(insert_position) #: String
137
+ source = processed_source.buffer.source
138
+ return "# rbs_inline: enabled\n" if insert_position.zero? || source[insert_position - 1] == "\n"
139
+
140
+ "\n# rbs_inline: enabled\n"
114
141
  end
115
142
 
116
143
  def find_last_comment_in_first_block #: Parser::Source::Comment
@@ -127,13 +154,37 @@ module RuboCop
127
154
  comments[last_idx] || raise
128
155
  end
129
156
 
130
- def style #: Symbol
131
- cop_config["EnforcedStyle"]&.to_sym || :always
157
+ def effective_mode #: FileFilter::mode
158
+ mode = cop_config["Mode"]
159
+ if mode
160
+ # `raw.to_s.to_sym` handles YAML-native Integer / Boolean safely.
161
+ sym = mode.to_s.to_sym
162
+ return sym if FileFilter::SUPPORTED_MODES.include?(sym)
163
+
164
+ FileFilter.warn_invalid_mode(mode)
165
+ end
166
+
167
+ cop_config["EnforcedStyle"]&.to_sym == :never ? :opt_out : :opt_in
168
+ end
169
+
170
+ def allow_missing_comment? #: bool
171
+ cop_config["AllowMissingComment"] == true
172
+ end
173
+
174
+ def warn_deprecated_enforced_style #: void
175
+ return if self.class.enforced_style_deprecation_warned?
176
+ return if cop_config["EnforcedStyle"].nil?
177
+
178
+ self.class.mark_enforced_style_deprecation_warned!
179
+ Kernel.warn(
180
+ "[rubocop-rbs_inline] Style/RbsInline/RequireRbsInlineComment.EnforcedStyle is deprecated. " \
181
+ "Please migrate to `Style/RbsInline: Mode: opt_in` (was `EnforcedStyle: always`) or " \
182
+ "`Style/RbsInline: Mode: opt_out` (was `EnforcedStyle: never`). " \
183
+ "EnforcedStyle will be removed in the next major version."
184
+ )
132
185
  end
133
186
 
134
187
  def first_line_range #: Parser::Source::Range
135
- # Find the first line of actual code (not comment-only lines)
136
- # If there's no AST (e.g., file with only comments), use the first line
137
188
  first_line = processed_source.ast&.source_range&.first_line || 1
138
189
  processed_source.buffer.line_range(first_line)
139
190
  end
@@ -24,7 +24,9 @@ module RuboCop
24
24
  # )
25
25
  #
26
26
  class StructClassCommentAlignment < Base
27
+ prepend FileFilter
27
28
  include ClassCommentAlignment
29
+ include StructClassMatcher
28
30
  extend AutoCorrector
29
31
 
30
32
  MSG = "Misaligned inline type annotation for Struct attribute."
@@ -35,22 +37,6 @@ module RuboCop
35
37
 
36
38
  check_comment_alignment(node)
37
39
  end
38
-
39
- private
40
-
41
- # @rbs node: RuboCop::AST::SendNode
42
- def struct_like_class?(node) #: bool
43
- return false unless node.method_name == :new
44
-
45
- (r = node.receiver).is_a?(RuboCop::AST::ConstNode) && r.short_name == :Struct
46
- end
47
-
48
- # `Struct.new` treats a leading string argument as the struct name, so only
49
- # symbol arguments are attributes.
50
- # @rbs arg: RuboCop::AST::Node
51
- def attr_argument?(arg) #: bool
52
- arg.sym_type?
53
- end
54
40
  end
55
41
  end
56
42
  end
@@ -25,6 +25,9 @@ module RuboCop
25
25
  # end
26
26
  #
27
27
  class StructNewWithBlock < Base
28
+ prepend FileFilter
29
+ include StructClassMatcher
30
+
28
31
  MSG = "Do not use `Struct.new` with a block. RBS::Inline does not parse block contents, " \
29
32
  "so methods defined in the block will not be recognized. " \
30
33
  "Use a separate class definition instead."
@@ -38,15 +41,6 @@ module RuboCop
38
41
 
39
42
  add_offense(node)
40
43
  end
41
-
42
- private
43
-
44
- # @rbs node: RuboCop::AST::SendNode
45
- def struct_like_class?(node) #: bool
46
- return false unless node.method_name == :new
47
-
48
- (r = node.receiver).is_a?(RuboCop::AST::ConstNode) && r.short_name == :Struct
49
- end
50
44
  end
51
45
  end
52
46
  end
@@ -18,14 +18,18 @@ module RuboCop
18
18
  # def method(arg); end
19
19
  #
20
20
  class UnmatchedAnnotations < Base
21
+ prepend FileFilter
21
22
  include CommentParser
22
23
  include RangeHelp
23
24
  include SourceCodeHelper
24
25
 
25
26
  MSG = "target parameter not found."
26
27
 
28
+ attr_reader :processed_comments #: Set[RBS::Inline::AnnotationParser::ParsingResult] -- matched to a def node
29
+
27
30
  def on_new_investigation #: void
28
31
  super
32
+ @processed_comments = Set.new
29
33
  parse_comments
30
34
  end
31
35
 
@@ -41,6 +45,8 @@ module RuboCop
41
45
 
42
46
  def on_investigation_end #: void
43
47
  parsed_comments.each do |comment|
48
+ next if processed?(comment)
49
+
44
50
  comment.each_annotation do |annotation|
45
51
  case annotation
46
52
  when RBS::Inline::AST::Annotations::BlockType,
@@ -57,15 +63,13 @@ module RuboCop
57
63
  private
58
64
 
59
65
  # @rbs node: RuboCop::AST::DefNode
60
- def process(node) #: void # rubocop:disable Metrics/CyclomaticComplexity
66
+ def process(node) #: void
61
67
  arguments = arguments_for(node)
62
68
 
63
- comment = parsed_comments.find do |r|
64
- r.comments.map(&:location).map(&:start_line).include?(node.location.line - 1)
65
- end
69
+ comment = leading_comment_for(node)
66
70
  return unless comment
67
71
 
68
- parsed_comments.delete(comment)
72
+ mark_processed(comment)
69
73
  comment.each_annotation do |annotation|
70
74
  case annotation
71
75
  when RBS::Inline::AST::Annotations::IvarType
@@ -76,6 +80,25 @@ module RuboCop
76
80
  end
77
81
  end
78
82
 
83
+ # @rbs node: RuboCop::AST::DefNode
84
+ def leading_comment_for(node) #: RBS::Inline::AnnotationParser::ParsingResult?
85
+ parsed_comments.find do |comment|
86
+ next if processed?(comment)
87
+
88
+ comment.comments.map(&:location).map(&:start_line).include?(node.location.line - 1)
89
+ end
90
+ end
91
+
92
+ # @rbs comment: RBS::Inline::AnnotationParser::ParsingResult
93
+ def mark_processed(comment) #: void
94
+ processed_comments << comment
95
+ end
96
+
97
+ # @rbs comment: RBS::Inline::AnnotationParser::ParsingResult
98
+ def processed?(comment) #: bool
99
+ processed_comments.include?(comment)
100
+ end
101
+
79
102
  # @rbs node: RuboCop::AST::DefNode
80
103
  def arguments_for(node) #: Array[String] # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
81
104
  args_for(node).children.flat_map do |argument| # rubocop:disable Metrics/BlockLength
@@ -104,7 +127,9 @@ module RuboCop
104
127
  when :forward_arg
105
128
  ["..."]
106
129
  else
107
- raise
130
+ # Unnamed parameters (ex. destructuring arguments; `def foo((a, b))`) are not
131
+ # annotatable because RBS::Inline does not generate types for them.
132
+ []
108
133
  end
109
134
  end
110
135
  end