Class: Shirobai::Cop::Lint::DuplicateMethods

Inherits:
RuboCop::Cop::Base
  • Object
show all
Includes:
RuboCop::Cop::ProjectIndexHelp, BundleEligible
Defined in:
lib/shirobai/cop/lint/duplicate_methods.rb

Overview

Drop-in Rust reimplementation of Lint/DuplicateMethods.

Stock keeps @definitions / @scopes on the cop instance, which RuboCop reuses across every file sharing a config — duplicate detection is deliberately cross-file (Method ... is defined at both first.rb:2 and second.rb:2.). The Rust side therefore returns the per-file part only: the exact stream of stock found_method calls (key, message name, offense range, rescue/ensure scope) in callback order, and this wrapper replays stock's bookkeeping against its own cross-investigation state. The replay is a hash lookup per event; all AST work (scope resolution, parent_module_name, anonymous Class.new blocks, attr/alias/delegator matchers) happens in Rust on the shared walk.

Two event flavors need Ruby-side completion:

  • scope_line >= 0: the key gets an "@#{smart_path}:#{line}" suffix (stock's source_location-based anonymous-block scope id; Rust does not know the buffer name).
  • sexp_start >= 0: stock's lookup_constant failed and (through each_ancestor's block-form return value of self) the key embeds the parser-gem s-expression of the whole defs node. The wrapper finds that node in processed_source.ast and interpolates it with stock's own Node#to_s, staying byte-identical for arbitrary bodies.

No autocorrect (stock has none).

Constant Summary collapse

MSG =
"Method `%<method>s` is defined at both %<defined>s and %<current>s."
INDEXABLE_METHOD_NAME =

Stock 1.89: method names the cop registers that can be looked up in the project index.

/\A(?<owner>[A-Z]\w*(?:::[A-Z]\w*)*)(?<separator>[#.])(?<name>[^#.]+)\z/

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config = nil, options = nil) ⇒ DuplicateMethods

Returns a new instance of DuplicateMethods.



59
60
61
62
63
# File 'lib/shirobai/cop/lint/duplicate_methods.rb', line 59

def initialize(config = nil, options = nil)
  super
  @definitions = {}
  @scopes = { 1 => [], 2 => [] } # 1 = :rescue, 2 = :ensure
end

Class Method Details

.badgeObject



47
# File 'lib/shirobai/cop/lint/duplicate_methods.rb', line 47

def self.badge = RuboCop::Cop::Badge.parse(cop_name)

.bundle_args(config) ⇒ Object

[active_support_extensions_enabled, delegating_methods] (AllCops + the 1.89 DelegatingMethods list).



51
52
53
54
55
56
57
# File 'lib/shirobai/cop/lint/duplicate_methods.rb', line 51

def self.bundle_args(config)
  cop_config = config.for_badge(badge)
  [
    !!config.active_support_extensions_enabled?,
    Array(cop_config.fetch("DelegatingMethods", ["delegate"])).map(&:to_s)
  ]
end

.cop_nameObject



46
# File 'lib/shirobai/cop/lint/duplicate_methods.rb', line 46

def self.cop_name = "Lint/DuplicateMethods"

Instance Method Details

#on_new_investigationObject



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/shirobai/cop/lint/duplicate_methods.rb', line 65

def on_new_investigation
  # Stock 1.89: the self-alias trick declares an intentional
  # redefinition only within the file that uses it.
  @self_aliased = Set.new
  events = resolved_events
  return if events.empty?

  buffer = processed_source.buffer
  path = smart_path(buffer.name)
  off = SourceOffsets.for(bundle_eligible? ? processed_source.raw_source : buffer.source)

  # `flags` bits: 0-1 rescue/ensure scope / 4 self-alias event / 8
  # the key carries a scope id / 16 inside a def.
  events.each do |flags, name, key, sexp_start, _sexp_end, scope_line, scope_begin,
                  off_start, off_end, line|
    if flags.anybits?(4) # track_self_alias (1.89)
      @self_aliased << name
      next
    end
    scope = flags & 3
    if sexp_start >= 0
      node = defs_node_at(off[sexp_start])
      next unless node

      name = "#{node}.#{name}"
      key = "#{key}#{name}"
    end
    # 1.89 `anon_block_identity`: begin_pos joins the id so blocks
    # sharing a line stay distinct.
    key = "#{key}@#{path}:#{scope_line}:#{off[scope_begin]}" if scope_line >= 0
    current = "#{path}:#{line}"

    if @definitions.key?(key)
      defined_display, defined_path = @definitions[key]
      if scope != 0 && !@scopes[scope].include?(key)
        @definitions[key] = [current, path]
        @scopes[scope] << key
      elsif @self_aliased.include?(name) && defined_path != path
        # `intentional_cross_file_redefinition?` (1.89): the
        # self-alias trick marks an intentional redefinition of a
        # method defined in another file.
        @definitions[key] = [current, path]
      else
        range = Parser::Source::Range.new(buffer, off[off_start], off[off_end])
        message = format(MSG, method: name, defined: defined_display, current: current)
        add_offense(range, message: message)
      end
    else
      @definitions[key] = [current, path]
      # Stock 1.89 `check_cross_file_duplicate`: first sighting, no
      # scope id, no rescue/ensure scope, not inside a def, and the
      # runner handed us an index.
      if project_index && scope.zero? && !flags.anybits?(8) && !flags.anybits?(16)
        check_cross_file_duplicate(name, current, off[off_start], off[off_end])
      end
    end
  end
end