java-topology/tools/tickets/defects/rubocop-0001.md
russell@unturf.com db29a08762 undefect. CWE-407 — 92 sites, 42 ecosystems
B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections.
Squash of 94 local commits onto remote master.
2026-03-26 19:48:18 -04:00

2.6 KiB
Raw Blame History

id repo severity status created
rubocop-0001 RuboCop MEDIUM OPEN 2026-03-26

Defect

File: lib/rubocop/cop/ignored_node.rb:32 Pattern: @ignored_nodes ||= [] — Array used as the ignored-node set; linear scan on every membership test Complexity: O(R × S) per file, where R = regexp nodes and S = string nodes Language: Ruby

Description

IgnoredNode is a module included in Cop::Base, which every cop inherits. Two methods use the array:

  • ignored_node?(node) (line 26): ignored_nodes.any? { |n| n.equal?(node) } — O(n) linear scan
  • part_of_ignored_node?(node) (line 12): ignored_nodes.map(&:loc).any? { ... } — O(n) with additional .map allocation per call

The most impactful call path is via the StringHelp mixin (included by Style::StringLiterals, Style::StringLiteralsInInterpolation, Style::CharacterLiteral, Style::IpAddresses):

on_regexp(node) → ignore_node(node) → @ignored_nodes << node     # grow
on_str(node)    → part_of_ignored_node?(node)                    # scan all ignored nodes

For a file containing R regexp literals and S string literals, part_of_ignored_node? is called S times, each scanning up to R entries: O(R × S) total work within that file. On a file with 100 regexps and 1 000 strings that is 100 000 linear comparisons. Across a large project of 10 000 files the cumulative cost is substantial.

Fix

Replace the backing array with a Set using identity comparison:

# ignored_node.rb

def ignored_nodes
  @ignored_nodes ||= Set.new.compare_by_identity
end

def ignore_node(node)
  ignored_nodes.add(node)
end

def ignored_node?(node)
  ignored_nodes.include?(node)          # O(1)
end

def part_of_ignored_node?(node)
  ignored_nodes.any? do |ignored|
    loc = ignored.loc
    # ... same range check as today
  end                                    # O(n) worst-case but Set iteration is cache-friendly
end

For part_of_ignored_node? the range comparison cannot be reduced to a hash lookup, but switching to a Set eliminates the .map allocation and makes ignored_node? O(1).

Speedup estimate

Proportional to R × S per file; practically 210× faster on files with many regexp and string literals. Negligible on files with few regexps.

Work required

  • Patch in defects/rubocop/patch/
  • Unit test — asserts operation counts before/after (in defects/rubocop/unit/)
  • Integration test (in defects/rubocop/integration/)
  • Benchmark — before/after on files with varying R and S (in defects/rubocop/bench/)
  • White paper section