rubocop-rbs_inline 1.6.0 → 1.7.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +48 -19
  3. data/CHANGELOG.md +14 -0
  4. data/README.md +70 -0
  5. data/Rakefile +19 -19
  6. data/Steepfile +2 -2
  7. data/config/default.yml +23 -51
  8. data/lib/rubocop/cop/rbs_inline_cops.rb +26 -20
  9. data/lib/rubocop/cop/style/rbs_inline/class_comment_alignment.rb +66 -0
  10. data/lib/rubocop/cop/style/rbs_inline/comment_parser.rb +5 -5
  11. data/lib/rubocop/cop/style/rbs_inline/data_class_comment_alignment.rb +6 -87
  12. data/lib/rubocop/cop/style/rbs_inline/data_define_with_block.rb +6 -7
  13. data/lib/rubocop/cop/style/rbs_inline/data_struct_helper.rb +97 -0
  14. data/lib/rubocop/cop/style/rbs_inline/embedded_rbs_spacing.rb +3 -3
  15. data/lib/rubocop/cop/style/rbs_inline/invalid_comment.rb +7 -7
  16. data/lib/rubocop/cop/style/rbs_inline/invalid_types.rb +2 -2
  17. data/lib/rubocop/cop/style/rbs_inline/keyword_separator.rb +3 -3
  18. data/lib/rubocop/cop/style/rbs_inline/method_comment_spacing.rb +6 -6
  19. data/lib/rubocop/cop/style/rbs_inline/missing_class_annotation.rb +85 -0
  20. data/lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb +7 -137
  21. data/lib/rubocop/cop/style/rbs_inline/missing_struct_class_annotation.rb +56 -0
  22. data/lib/rubocop/cop/style/rbs_inline/missing_type_annotation.rb +16 -16
  23. data/lib/rubocop/cop/style/rbs_inline/parameters_separator.rb +5 -5
  24. data/lib/rubocop/cop/style/rbs_inline/redundant_annotation_with_skip.rb +11 -11
  25. data/lib/rubocop/cop/style/rbs_inline/redundant_instance_variable_annotation.rb +2 -2
  26. data/lib/rubocop/cop/style/rbs_inline/redundant_type_annotation.rb +5 -5
  27. data/lib/rubocop/cop/style/rbs_inline/require_rbs_inline_comment.rb +3 -3
  28. data/lib/rubocop/cop/style/rbs_inline/source_code_helper.rb +2 -2
  29. data/lib/rubocop/cop/style/rbs_inline/struct_class_comment_alignment.rb +58 -0
  30. data/lib/rubocop/cop/style/rbs_inline/struct_new_with_block.rb +54 -0
  31. data/lib/rubocop/cop/style/rbs_inline/unmatched_annotations.rb +13 -13
  32. data/lib/rubocop/cop/style/rbs_inline/untyped_instance_variable.rb +10 -10
  33. data/lib/rubocop/cop/style/rbs_inline/variable_comment_spacing.rb +4 -4
  34. data/lib/rubocop/rbs_inline/plugin.rb +7 -7
  35. data/lib/rubocop/rbs_inline/version.rb +1 -1
  36. data/lib/rubocop/rbs_inline.rb +1 -1
  37. data/lib/rubocop-rbs_inline.rb +5 -5
  38. data/rbs_collection.lock.yaml +19 -11
  39. data/sig/rubocop/cop/style/rbs_inline/class_comment_alignment.rbs +34 -0
  40. data/sig/rubocop/cop/style/rbs_inline/data_class_comment_alignment.rbs +2 -30
  41. data/sig/rubocop/cop/style/rbs_inline/data_define_with_block.rbs +1 -1
  42. data/sig/rubocop/cop/style/rbs_inline/data_struct_helper.rbs +55 -0
  43. data/sig/rubocop/cop/style/rbs_inline/missing_class_annotation.rbs +41 -0
  44. data/sig/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rbs +2 -46
  45. data/sig/rubocop/cop/style/rbs_inline/missing_struct_class_annotation.rbs +46 -0
  46. data/sig/rubocop/cop/style/rbs_inline/struct_class_comment_alignment.rbs +48 -0
  47. data/sig/rubocop/cop/style/rbs_inline/struct_new_with_block.rbs +40 -0
  48. metadata +14 -2
@@ -26,106 +26,25 @@ module RuboCop
26
26
  # )
27
27
  #
28
28
  class DataClassCommentAlignment < Base
29
- include ASTUtils
30
- include RangeHelp
31
- include SourceCodeHelper
29
+ include ClassCommentAlignment
32
30
  extend AutoCorrector
33
31
 
34
- MSG = 'Misaligned inline type annotation for Data attribute.'
32
+ MSG = "Misaligned inline type annotation for Data attribute."
35
33
 
36
34
  # @rbs node: RuboCop::AST::SendNode
37
35
  def on_send(node) #: void
38
- return unless data_define?(node)
39
- return if folded_data_class?(node)
36
+ return unless struct_like_class?(node)
40
37
 
41
- check_annotation_alignment(node)
38
+ check_comment_alignment(node)
42
39
  end
43
40
 
44
41
  private
45
42
 
46
43
  # @rbs node: RuboCop::AST::SendNode
47
- def data_define?(node) #: bool
44
+ def struct_like_class?(node) #: bool
48
45
  return false unless node.method_name == :define
49
46
 
50
- case (r = node.receiver)
51
- when RuboCop::AST::ConstNode
52
- r.short_name == :Data
53
- else
54
- false
55
- end
56
- end
57
-
58
- # @rbs arg: RuboCop::AST::Node
59
- def data_attribute?(arg) #: bool
60
- arg.sym_type? || arg.str_type?
61
- end
62
-
63
- # @rbs node: RuboCop::AST::SendNode
64
- def data_attributes(node) #: Array[RuboCop::AST::Node]
65
- node.arguments.select { data_attribute?(_1) }
66
- end
67
-
68
- # @rbs node: RuboCop::AST::SendNode
69
- def folded_data_class?(node) #: bool
70
- args = node.arguments
71
- return false if args.empty?
72
-
73
- lines = args.map { _1.location.line }
74
- lines.uniq.length < args.length || lines.include?(node.location.line)
75
- end
76
-
77
- # @rbs node: RuboCop::AST::SendNode
78
- def check_annotation_alignment(node) #: void
79
- annotated = data_attributes(node).filter_map do |arg|
80
- comment = inline_type_comment(arg.location.line)
81
- [arg, comment] if comment #: [RuboCop::AST::Node, Parser::Source::Comment]
82
- end
83
- return if annotated.size < 2
84
-
85
- expected_col = annotation_column(node)
86
- annotated.each do |arg, comment|
87
- actual_col = comment.location.column
88
- next if actual_col == expected_col
89
-
90
- add_offense(comment.source_range) do |corrector|
91
- correct_alignment(corrector, arg, comment, expected_col)
92
- end
93
- end
94
- end
95
-
96
- # @rbs line: Integer
97
- def inline_type_comment(line) #: Parser::Source::Comment?
98
- comment = comment_at(line)
99
- comment if comment&.text&.match?(/\A#:/)
100
- end
101
-
102
- # @rbs node: RuboCop::AST::SendNode
103
- def annotation_column(node) #: Integer
104
- last_arg = node.arguments.last
105
- max_end_col = node.arguments.map do |arg|
106
- comma_length = arg.equal?(last_arg) ? 0 : 1
107
- arg.location.column + source!(arg).length + comma_length
108
- end.max || 0
109
-
110
- max_end_col + 2
111
- end
112
-
113
- # @rbs corrector: RuboCop::Cop::Corrector
114
- # @rbs arg: RuboCop::AST::Node
115
- # @rbs comment: Parser::Source::Comment
116
- # @rbs expected_col: Integer
117
- def correct_alignment(corrector, arg, comment, expected_col) #: void # rubocop:disable Metrics/AbcSize
118
- line = arg.location.line
119
- line_source = source_code_at(line)
120
- source = line_source[...comment.location.column] || raise
121
- content_end_col = source.rstrip.length
122
- padding = [expected_col - content_end_col, 1].max || raise
123
-
124
- line_begin = processed_source.buffer.line_range(line).begin_pos
125
- replace_start = line_begin + content_end_col
126
- replace_end = line_begin + comment.location.column
127
-
128
- corrector.replace(range_between(replace_start, replace_end), ' ' * padding)
47
+ (r = node.receiver).is_a?(RuboCop::AST::ConstNode) && r.short_name == :Data
129
48
  end
130
49
  end
131
50
  end
@@ -28,13 +28,13 @@ module RuboCop
28
28
  # end
29
29
  #
30
30
  class DataDefineWithBlock < Base
31
- MSG = 'Do not use `Data.define` with a block. RBS::Inline does not parse block contents, ' \
32
- 'so methods defined in the block will not be recognized. ' \
33
- 'Use a separate class definition instead.'
31
+ MSG = "Do not use `Data.define` with a block. RBS::Inline does not parse block contents, " \
32
+ "so methods defined in the block will not be recognized. " \
33
+ "Use a separate class definition instead."
34
34
 
35
35
  # @rbs node: RuboCop::AST::SendNode
36
36
  def on_send(node) #: void
37
- return unless data_define?(node)
37
+ return unless struct_like_class?(node)
38
38
 
39
39
  block_node = node.parent
40
40
  return unless block_node&.block_type?
@@ -45,11 +45,10 @@ module RuboCop
45
45
  private
46
46
 
47
47
  # @rbs node: RuboCop::AST::SendNode
48
- def data_define?(node) #: bool
48
+ def struct_like_class?(node) #: bool
49
49
  return false unless node.method_name == :define
50
50
 
51
- receiver = node.receiver
52
- receiver.is_a?(RuboCop::AST::ConstNode) && receiver.short_name == :Data
51
+ (r = node.receiver).is_a?(RuboCop::AST::ConstNode) && r.short_name == :Data
53
52
  end
54
53
  end
55
54
  end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ module RbsInline
7
+ # Shared logic for cops that check inline type annotations on struct-like
8
+ # class definitions such as `Data.define` and `Struct.new`.
9
+ #
10
+ # Including cops decide which nodes to check by defining their own matcher
11
+ # (e.g. `struct_like_class?`); this module only provides helpers that operate
12
+ # on an already-matched definition node. Cops may override {#attr_argument?}
13
+ # when the set of attribute arguments differs from the default (e.g.
14
+ # `Struct.new` treats a leading string argument as the struct name rather than
15
+ # an attribute).
16
+ #
17
+ # @rbs module-self RuboCop::Cop::Base
18
+ module DataStructHelper
19
+ include ASTUtils
20
+ include SourceCodeHelper
21
+ include RangeHelp
22
+
23
+ # @rbs arg: RuboCop::AST::Node
24
+ def attr_argument?(arg) #: bool
25
+ arg.sym_type? || arg.str_type?
26
+ end
27
+
28
+ # @rbs node: RuboCop::AST::SendNode
29
+ def attr_arguments(node) #: Array[RuboCop::AST::Node]
30
+ node.arguments.select { attr_argument?(_1) }
31
+ end
32
+
33
+ # @rbs node: RuboCop::AST::SendNode
34
+ def folded?(node) #: bool
35
+ attrs = attr_arguments(node)
36
+ return false if attrs.empty?
37
+
38
+ lines = attrs.map { _1.location.line }
39
+ lines.uniq.length < attrs.length || lines.include?(node.location.line)
40
+ end
41
+
42
+ # @rbs line: Integer
43
+ def inline_type_annotation?(line) #: boolish
44
+ comment = comment_at(line)
45
+ comment&.text&.match?(/\A#:/)
46
+ end
47
+
48
+ # @rbs line: Integer
49
+ def inline_type_comment(line) #: Parser::Source::Comment?
50
+ comment = comment_at(line)
51
+ comment if comment&.text&.match?(/\A#:/)
52
+ end
53
+
54
+ # @rbs line: Integer
55
+ def find_regular_comment(line) #: Parser::Source::Comment?
56
+ comment = comment_at(line)
57
+ comment unless comment&.text&.match?(/\A#:/)
58
+ end
59
+
60
+ # @rbs node: RuboCop::AST::SendNode
61
+ def annotation_column(node) #: Integer
62
+ last_arg = node.arguments.last
63
+ max_end_col = node.arguments.map do |arg|
64
+ comma_length = arg.equal?(last_arg) ? 0 : 1
65
+ arg.location.column + source!(arg).length + comma_length
66
+ end.max || 0
67
+
68
+ max_end_col + 2
69
+ end
70
+
71
+ # @rbs node: RuboCop::AST::SendNode
72
+ 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 }
75
+ args.max_by(&:length) || ""
76
+ end
77
+
78
+ # @rbs node: RuboCop::AST::SendNode
79
+ def build_multiline_replacement(node) #: String # rubocop:disable Metrics/AbcSize
80
+ base_indent = source_code_at(node.location.line)[/\A\s*/]
81
+ last_index = node.arguments.size - 1
82
+ longest = longest_argname(node)
83
+
84
+ args_source = node.arguments.each_with_index.map do |arg, i|
85
+ comma = i < last_index ? "," : ""
86
+ prefix = "#{base_indent} #{arg.source}#{comma}"
87
+ padding = " " * (longest.length - source!(arg).length - comma.length + 2)
88
+ attr_argument?(arg) ? "#{prefix}#{padding}#: untyped" : prefix
89
+ end.join("\n")
90
+
91
+ "(\n#{args_source}\n#{base_indent})"
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'source_code_helper'
4
- require_relative 'comment_parser'
3
+ require_relative "source_code_helper"
4
+ require_relative "comment_parser"
5
5
 
6
6
  module RuboCop
7
7
  module Cop
@@ -30,7 +30,7 @@ module RuboCop
30
30
  include SourceCodeHelper
31
31
  include CommentParser
32
32
 
33
- MSG = '`@rbs!` comment must be followed by a blank line.'
33
+ MSG = "`@rbs!` comment must be followed by a blank line."
34
34
 
35
35
  def on_new_investigation #: void
36
36
  check_embedded_rbs_spacing
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rbs/inline/annotation_parser/tokenizer'
3
+ require "rbs/inline/annotation_parser/tokenizer"
4
4
 
5
5
  module RuboCop
6
6
  module Cop
@@ -22,11 +22,11 @@ module RuboCop
22
22
  class InvalidComment < Base
23
23
  extend AutoCorrector
24
24
 
25
- MSG = 'Invalid RBS annotation comment found.'
25
+ MSG = "Invalid RBS annotation comment found."
26
26
 
27
27
  # refs: https://github.com/soutaro/rbs-inline/blob/main/lib/rbs/inline/annotation_parser/tokenizer.rb
28
28
  RBS_INLINE_KEYWORDS = %w[inherits override use module-self generic skip module class].freeze #: Array[String]
29
- RBS_INLINE_KEYWORD_PATTERN = RBS_INLINE_KEYWORDS.join('|') #: String
29
+ RBS_INLINE_KEYWORD_PATTERN = RBS_INLINE_KEYWORDS.join("|") #: String
30
30
 
31
31
  SIGNATURE_PATTERN = '\(.*\)\s*(\??\s*{.*?}\s*)?->\s*.*'
32
32
 
@@ -56,10 +56,10 @@ module RuboCop
56
56
  # @rbs text: String
57
57
  def corrected_text(text) #: String?
58
58
  case text
59
- when MISSING_HASH_COLON then text.sub(/\A#\s+/, '#: ')
60
- when SPACE_BEFORE_COLON then text.sub(/\A#\s+:\s*/, '#: ')
61
- when MIXED_ANNOTATION then text.sub(/\A#:\s+/, '# ')
62
- when MISSING_AT_SIGN then text.sub(/\A#\s*rbs\s+/, '# @rbs ')
59
+ when MISSING_HASH_COLON then text.sub(/\A#\s+/, "#: ")
60
+ when SPACE_BEFORE_COLON then text.sub(/\A#\s+:\s*/, "#: ")
61
+ when MIXED_ANNOTATION then text.sub(/\A#:\s+/, "# ")
62
+ when MISSING_AT_SIGN then text.sub(/\A#\s*rbs\s+/, "# @rbs ")
63
63
  end
64
64
  end
65
65
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rbs/inline'
3
+ require "rbs/inline"
4
4
 
5
5
  module RuboCop
6
6
  module Cop
@@ -27,7 +27,7 @@ module RuboCop
27
27
  include RBS::Inline::AST::Annotations
28
28
  include RBS::Inline::AST::Members
29
29
 
30
- MSG = 'Invalid annotation found.'
30
+ MSG = "Invalid annotation found."
31
31
 
32
32
  def on_new_investigation #: void
33
33
  parse_comments.each do |result|
@@ -19,7 +19,7 @@ module RuboCop
19
19
  include CommentParser
20
20
  include RangeHelp
21
21
 
22
- MSG = 'Do not use `:` after the keyword.'
22
+ MSG = "Do not use `:` after the keyword."
23
23
 
24
24
  # refs: https://github.com/soutaro/rbs-inline/blob/main/lib/rbs/inline/annotation_parser/tokenizer.rb
25
25
  RBS_INLINE_KEYWORDS = %w[inherits override use module-self generic skip module class].freeze #: Array[String]
@@ -49,7 +49,7 @@ module RuboCop
49
49
 
50
50
  def on_investigation_end #: void
51
51
  processed_source.comments.each do |comment|
52
- matched = comment.text.match(/\A#\s+@rbs\s+(#{RBS_INLINE_KEYWORDS.join('|')}):/)
52
+ matched = comment.text.match(/\A#\s+@rbs\s+(#{RBS_INLINE_KEYWORDS.join("|")}):/)
53
53
  next unless matched
54
54
  next if valid_method_annotation?(comment)
55
55
 
@@ -86,7 +86,7 @@ module RuboCop
86
86
  # followed by `:` but no type.
87
87
  # @rbs comment: Parser::Source::Comment
88
88
  def no_argument_keyword_without_type?(comment) #: bool
89
- comment.text.match?(/\A#\s+@rbs\s+(#{NO_ARGUMENT_KEYWORDS.join('|')}):\s*\z/)
89
+ comment.text.match?(/\A#\s+@rbs\s+(#{NO_ARGUMENT_KEYWORDS.join("|")}):\s*\z/)
90
90
  end
91
91
 
92
92
  # @rbs comment: Parser::Source::Comment
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'source_code_helper'
4
- require_relative 'comment_parser'
3
+ require_relative "source_code_helper"
4
+ require_relative "comment_parser"
5
5
 
6
6
  module RuboCop
7
7
  module Cop
@@ -61,8 +61,8 @@ module RuboCop
61
61
  include SourceCodeHelper
62
62
  include CommentParser
63
63
 
64
- MSG = 'Method-related `@rbs` annotation must be immediately before a method definition.'
65
- MSG_WITH_BLANK_LINE = 'Remove blank line between method annotation and method definition.'
64
+ MSG = "Method-related `@rbs` annotation must be immediately before a method definition."
65
+ MSG_WITH_BLANK_LINE = "Remove blank line between method annotation and method definition."
66
66
  METHOD_DEFINITION_PATTERN =
67
67
  /\A(?:(?:private|protected|public|private_class_method|module_function)\s+)?def\s/ #: Regexp
68
68
 
@@ -115,7 +115,7 @@ module RuboCop
115
115
  when RBS::Inline::AST::Annotations::VarType
116
116
  # VarType includes both parameter types and variable types (@ivar, @@cvar)
117
117
  # Only consider it method-related if it's a parameter annotation (name doesn't start with @)
118
- return true unless annotation.name.start_with?('@')
118
+ return true unless annotation.name.start_with?("@")
119
119
  when RBS::Inline::AST::Annotations::ReturnType,
120
120
  RBS::Inline::AST::Annotations::BlockType,
121
121
  RBS::Inline::AST::Annotations::Override,
@@ -166,7 +166,7 @@ module RuboCop
166
166
  # @rbs comment: RBS::Inline::AnnotationParser::ParsingResult
167
167
  def skip_only_annotation?(comment) #: bool
168
168
  annotations = [] #: Array[RBS::Inline::AST::Annotations::t]
169
- comment.each_annotation { |a| annotations << a }
169
+ comment.each_annotation { annotations << _1 }
170
170
  annotations.any? && annotations.all?(RBS::Inline::AST::Annotations::Skip)
171
171
  end
172
172
 
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ module RbsInline
7
+ # Shared behavior for cops that ensure struct-like class attributes have
8
+ # inline type annotations (`Data.define` / `Struct.new`).
9
+ #
10
+ # The including cop matches the definition node (e.g. via `struct_like_class?`)
11
+ # and calls {#check_missing_annotations}.
12
+ #
13
+ # @rbs module-self RuboCop::Cop::Base
14
+ module MissingClassAnnotation
15
+ include DataStructHelper
16
+
17
+ # @rbs node: RuboCop::AST::SendNode
18
+ def check_missing_annotations(node) #: void
19
+ if folded?(node)
20
+ add_offenses_for_folded(node)
21
+ else
22
+ check_multiline(node)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ # @rbs node: RuboCop::AST::SendNode
29
+ def add_offenses_for_folded(node) #: void
30
+ corrected = false
31
+ attr_arguments(node).each do |arg|
32
+ if corrected
33
+ add_offense(arg)
34
+ else
35
+ replacement = build_multiline_replacement(node)
36
+ loc = node.loc
37
+ if loc.begin && loc.end
38
+ add_offense(arg) { _1.replace(loc.begin.join(loc.end), replacement) }
39
+ corrected = true
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ # @rbs node: RuboCop::AST::SendNode
46
+ def check_multiline(node) #: void
47
+ attr_arguments(node).each do |arg|
48
+ next if inline_type_annotation?(arg.location.line)
49
+
50
+ add_offense(arg) { correct_multiline(_1, node, arg) }
51
+ end
52
+ end
53
+
54
+ # @rbs corrector: RuboCop::Cop::Corrector
55
+ # @rbs node: RuboCop::AST::SendNode
56
+ # @rbs arg: RuboCop::AST::Node
57
+ def correct_multiline(corrector, node, arg) #: void
58
+ existing_comment = find_regular_comment(arg.location.line)
59
+
60
+ if existing_comment
61
+ comment_text = existing_comment.text.sub(/\A#\s*/, "")
62
+ corrector.replace(existing_comment.source_range, "#: untyped -- #{comment_text}")
63
+ else
64
+ insert_annotation(corrector, node, arg)
65
+ end
66
+ end
67
+
68
+ # @rbs corrector: RuboCop::Cop::Corrector
69
+ # @rbs node: RuboCop::AST::SendNode
70
+ # @rbs arg: RuboCop::AST::Node
71
+ def insert_annotation(corrector, node, arg) #: void
72
+ line = arg.location.line
73
+ line_source = source_code_at(line)
74
+ content_end_col = line_source.rstrip.length
75
+ padding = [annotation_column(node) - content_end_col, 1].max || raise
76
+ line_begin = processed_source.buffer.line_range(line).begin_pos
77
+ insert_pos = line_begin + content_end_col
78
+
79
+ corrector.insert_before(range_between(insert_pos, insert_pos), "#{" " * padding}#: untyped")
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -20,156 +20,26 @@ module RuboCop
20
20
  # :visibility #: Symbol
21
21
  # )
22
22
  #
23
- class MissingDataClassAnnotation < Base # rubocop:disable Metrics/ClassLength
24
- include ASTUtils
25
- include RangeHelp
26
- include SourceCodeHelper
23
+ class MissingDataClassAnnotation < Base
24
+ include MissingClassAnnotation
27
25
  extend AutoCorrector
28
26
 
29
- MSG = 'Missing inline type annotation for Data attribute (e.g., `#: Type`).'
27
+ MSG = "Missing inline type annotation for Data attribute (e.g., `#: Type`)."
30
28
 
31
29
  # @rbs node: RuboCop::AST::SendNode
32
30
  def on_send(node) #: void
33
- return unless data_define?(node)
31
+ return unless struct_like_class?(node)
34
32
 
35
- if folded_data_class?(node)
36
- add_offenses_for_folded_data_class(node)
37
- else
38
- check_multiline_data_class(node)
39
- end
33
+ check_missing_annotations(node)
40
34
  end
41
35
 
42
36
  private
43
37
 
44
38
  # @rbs node: RuboCop::AST::SendNode
45
- def data_define?(node) #: bool
39
+ def struct_like_class?(node) #: bool
46
40
  return false unless node.method_name == :define
47
41
 
48
- case (r = node.receiver)
49
- when RuboCop::AST::ConstNode
50
- r.short_name == :Data
51
- else
52
- false
53
- end
54
- end
55
-
56
- # @rbs arg: RuboCop::AST::Node
57
- def data_attribute?(arg) #: bool
58
- arg.sym_type? || arg.str_type?
59
- end
60
-
61
- # @rbs node: RuboCop::AST::SendNode
62
- def data_attributes(node) #: Array[RuboCop::AST::Node]
63
- node.arguments.select { data_attribute?(_1) }
64
- end
65
-
66
- # @rbs line: Integer
67
- def inline_type_annotation?(line) #: boolish
68
- comment = comment_at(line)
69
- comment&.text&.match?(/\A#:/)
70
- end
71
-
72
- # @rbs node: RuboCop::AST::SendNode
73
- def folded_data_class?(node) #: bool
74
- attrs = data_attributes(node)
75
- return false if attrs.empty?
76
-
77
- lines = attrs.map { _1.location.line }
78
- lines.uniq.length < attrs.length || lines.include?(node.location.line)
79
- end
80
-
81
- # @rbs node: RuboCop::AST::SendNode
82
- def add_offenses_for_folded_data_class(node) #: void
83
- corrected = false
84
- data_attributes(node).each do |arg|
85
- if corrected
86
- add_offense(arg)
87
- else
88
- replacement = build_multiline_replacement(node)
89
- loc = node.loc
90
- if loc.begin && loc.end
91
- add_offense(arg) { _1.replace(loc.begin.join(loc.end), replacement) }
92
- corrected = true
93
- end
94
- end
95
- end
96
- end
97
-
98
- # @rbs node: RuboCop::AST::SendNode
99
- def longest_argname(node) #: String
100
- last_index = node.arguments.size - 1
101
- args = node.arguments.each_with_index.map { |a, i| i < last_index ? "#{a.source}," : a.source.to_s }
102
- args.max_by(&:length) || ''
103
- end
104
-
105
- # @rbs node: RuboCop::AST::SendNode
106
- def build_multiline_replacement(node) #: String # rubocop:disable Metrics/AbcSize
107
- base_indent = source_code_at(node.location.line)[/\A\s*/]
108
- last_index = node.arguments.size - 1
109
- longest = longest_argname(node)
110
-
111
- args_source = node.arguments.each_with_index.map do |arg, i|
112
- comma = i < last_index ? ',' : ''
113
- prefix = "#{base_indent} #{arg.source}#{comma}"
114
- padding = ' ' * (longest.length - source!(arg).length - comma.length + 2)
115
- data_attribute?(arg) ? "#{prefix}#{padding}#: untyped" : prefix
116
- end.join("\n")
117
-
118
- "(\n#{args_source}\n#{base_indent})"
119
- end
120
-
121
- # @rbs node: RuboCop::AST::SendNode
122
- def check_multiline_data_class(node) #: void
123
- data_attributes(node).each do |arg|
124
- next if inline_type_annotation?(arg.location.line)
125
-
126
- add_offense(arg) { |corrector| correct_multiline(corrector, node, arg) }
127
- end
128
- end
129
-
130
- # @rbs corrector: RuboCop::Cop::Corrector
131
- # @rbs node: RuboCop::AST::SendNode
132
- # @rbs arg: RuboCop::AST::Node
133
- def correct_multiline(corrector, node, arg) #: void
134
- existing_comment = find_regular_comment(arg.location.line)
135
-
136
- if existing_comment
137
- comment_text = existing_comment.text.sub(/\A#\s*/, '')
138
- corrector.replace(existing_comment.source_range, "#: untyped -- #{comment_text}")
139
- else
140
- insert_annotation(corrector, node, arg)
141
- end
142
- end
143
-
144
- # @rbs corrector: RuboCop::Cop::Corrector
145
- # @rbs node: RuboCop::AST::SendNode
146
- # @rbs arg: RuboCop::AST::Node
147
- def insert_annotation(corrector, node, arg) #: void
148
- line = arg.location.line
149
- line_source = source_code_at(line)
150
- content_end_col = line_source.rstrip.length
151
- padding = [annotation_column(node) - content_end_col, 1].max || raise
152
- line_begin = processed_source.buffer.line_range(line).begin_pos
153
- insert_pos = line_begin + content_end_col
154
-
155
- corrector.insert_before(range_between(insert_pos, insert_pos), "#{' ' * padding}#: untyped")
156
- end
157
-
158
- # @rbs node: RuboCop::AST::SendNode
159
- def annotation_column(node) #: Integer
160
- last_arg = node.arguments.last
161
- max_end_col = node.arguments.map do |arg|
162
- comma_length = arg.equal?(last_arg) ? 0 : 1
163
- arg.location.column + source!(arg).length + comma_length
164
- end.max || 0
165
-
166
- max_end_col + 2
167
- end
168
-
169
- # @rbs line: Integer
170
- def find_regular_comment(line) #: Parser::Source::Comment?
171
- comment = comment_at(line)
172
- comment unless comment&.text&.match?(/\A#:/)
42
+ (r = node.receiver).is_a?(RuboCop::AST::ConstNode) && r.short_name == :Data
173
43
  end
174
44
  end
175
45
  end