Class: RuboCop::Cop::Style::RbsInline::MissingDataClassAnnotation

Inherits:
Base
  • Object
show all
Extended by:
AutoCorrector
Includes:
RangeHelp, ASTUtils, SourceCodeHelper
Defined in:
lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb,
sig/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rbs

Overview

Checks that Data.define attributes have inline type annotations.

Each attribute passed to Data.define should have a trailing #: type annotation comment on the same line.

Examples:

# bad
MethodEntry = Data.define(:name, :node, :visibility)

# good
MethodEntry = Data.define(
  :name,       #: Symbol
  :node,       #: Parser::AST::Node
  :visibility  #: Symbol
)

Constant Summary collapse

MSG =

Returns:

  • (::String)
'Missing inline type annotation for Data attribute (e.g., `#: Type`).'

Instance Method Summary collapse

Methods included from SourceCodeHelper

#annotation_range, #blank_line?, #char_at, #character_offset, #comment_at, #comment_range, #line_range, #location_to_range, #source_code_at

Methods included from ASTUtils

#end_line, #name_location, #source!, #value_to_sym

Instance Method Details

#add_offenses_for_folded_data_class(node) ⇒ void

This method returns an undefined value.

Parameters:

  • node (RuboCop::AST::SendNode)


82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb', line 82

def add_offenses_for_folded_data_class(node) #: void
  corrected = false
  data_attributes(node).each do |arg|
    if corrected
      add_offense(arg)
    else
      replacement = build_multiline_replacement(node)
      loc = node.loc
      if loc.begin && loc.end
        add_offense(arg) { _1.replace(loc.begin.join(loc.end), replacement) }
        corrected = true
      end
    end
  end
end

#annotation_column(node) ⇒ Integer

Parameters:

  • node (RuboCop::AST::SendNode)

Returns:

  • (Integer)


159
160
161
162
163
164
165
166
167
# File 'lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb', line 159

def annotation_column(node) #: Integer
  last_arg = node.arguments.last
  max_end_col = node.arguments.map do |arg|
    comma_length = arg.equal?(last_arg) ? 0 : 1
    arg.location.column + source!(arg).length + comma_length
  end.max || 0

  max_end_col + 2
end

#build_multiline_replacement(node) ⇒ String

Parameters:

  • node (RuboCop::AST::SendNode)

Returns:

  • (String)


106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb', line 106

def build_multiline_replacement(node) #: String # rubocop:disable Metrics/AbcSize
  base_indent = source_code_at(node.location.line)[/\A\s*/]
  last_index = node.arguments.size - 1
  longest = longest_argname(node)

  args_source = node.arguments.each_with_index.map do |arg, i|
    comma = i < last_index ? ',' : ''
    prefix = "#{base_indent}  #{arg.source}#{comma}"
    padding = ' ' * (longest.length - source!(arg).length - comma.length + 2)
    data_attribute?(arg) ? "#{prefix}#{padding}#: untyped" : prefix
  end.join("\n")

  "(\n#{args_source}\n#{base_indent})"
end

#check_multiline_data_class(node) ⇒ void

This method returns an undefined value.

Parameters:

  • node (RuboCop::AST::SendNode)


122
123
124
125
126
127
128
# File 'lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb', line 122

def check_multiline_data_class(node) #: void
  data_attributes(node).each do |arg|
    next if inline_type_annotation?(arg.location.line)

    add_offense(arg) { |corrector| correct_multiline(corrector, node, arg) }
  end
end

#correct_multiline(corrector, node, arg) ⇒ void

This method returns an undefined value.

Parameters:

  • corrector (RuboCop::Cop::Corrector)
  • node (RuboCop::AST::SendNode)
  • arg (RuboCop::AST::Node)


133
134
135
136
137
138
139
140
141
142
# File 'lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb', line 133

def correct_multiline(corrector, node, arg) #: void
  existing_comment = find_regular_comment(arg.location.line)

  if existing_comment
    comment_text = existing_comment.text.sub(/\A#\s*/, '')
    corrector.replace(existing_comment.source_range, "#: untyped -- #{comment_text}")
  else
    insert_annotation(corrector, node, arg)
  end
end

#data_attribute?(arg) ⇒ Boolean

Parameters:

  • arg (RuboCop::AST::Node)

Returns:

  • (Boolean)


57
58
59
# File 'lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb', line 57

def data_attribute?(arg) #: bool
  arg.sym_type? || arg.str_type?
end

#data_attributes(node) ⇒ Array[RuboCop::AST::Node]

Parameters:

  • node (RuboCop::AST::SendNode)

Returns:

  • (Array[RuboCop::AST::Node])


62
63
64
# File 'lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb', line 62

def data_attributes(node) #: Array[RuboCop::AST::Node]
  node.arguments.select { data_attribute?(_1) }
end

#data_define?(node) ⇒ Boolean

Parameters:

  • node (RuboCop::AST::SendNode)

Returns:

  • (Boolean)


45
46
47
48
49
50
51
52
53
54
# File 'lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb', line 45

def data_define?(node) #: bool
  return false unless node.method_name == :define

  case (r = node.receiver)
  when RuboCop::AST::ConstNode
    r.short_name == :Data
  else
    false
  end
end

#find_regular_comment(line) ⇒ Parser::Source::Comment?

Parameters:

  • line (Integer)

Returns:



170
171
172
173
# File 'lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb', line 170

def find_regular_comment(line) #: Parser::Source::Comment?
  comment = comment_at(line)
  comment unless comment&.text&.match?(/\A#:/)
end

#folded_data_class?(node) ⇒ Boolean

Parameters:

  • node (RuboCop::AST::SendNode)

Returns:

  • (Boolean)


73
74
75
76
77
78
79
# File 'lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb', line 73

def folded_data_class?(node) #: bool
  attrs = data_attributes(node)
  return false if attrs.empty?

  lines = attrs.map { _1.location.line }
  lines.uniq.length < attrs.length || lines.include?(node.location.line)
end

#inline_type_annotation?(line) ⇒ boolish

Parameters:

  • line (Integer)

Returns:

  • (boolish)


67
68
69
70
# File 'lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb', line 67

def inline_type_annotation?(line) #: boolish
  comment = comment_at(line)
  comment&.text&.match?(/\A#:/)
end

#insert_annotation(corrector, node, arg) ⇒ void

This method returns an undefined value.

Parameters:

  • corrector (RuboCop::Cop::Corrector)
  • node (RuboCop::AST::SendNode)
  • arg (RuboCop::AST::Node)


147
148
149
150
151
152
153
154
155
156
# File 'lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb', line 147

def insert_annotation(corrector, node, arg) #: void
  line = arg.location.line
  line_source = source_code_at(line)
  content_end_col = line_source.rstrip.length
  padding = [annotation_column(node) - content_end_col, 1].max || raise
  line_begin = processed_source.buffer.line_range(line).begin_pos
  insert_pos = line_begin + content_end_col

  corrector.insert_before(range_between(insert_pos, insert_pos), "#{' ' * padding}#: untyped")
end

#longest_argname(node) ⇒ String

Parameters:

  • node (RuboCop::AST::SendNode)

Returns:

  • (String)


99
100
101
102
103
# File 'lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb', line 99

def longest_argname(node) #: String
  last_index = node.arguments.size - 1
  args = node.arguments.each_with_index.map { |a, i| i < last_index ? "#{a.source}," : a.source.to_s }
  args.max_by(&:length) || ''
end

#on_send(node) ⇒ void

This method returns an undefined value.

Parameters:

  • node (RuboCop::AST::SendNode)


32
33
34
35
36
37
38
39
40
# File 'lib/rubocop/cop/style/rbs_inline/missing_data_class_annotation.rb', line 32

def on_send(node) #: void
  return unless data_define?(node)

  if folded_data_class?(node)
    add_offenses_for_folded_data_class(node)
  else
    check_multiline_data_class(node)
  end
end