Add cop for Metrics/PerceivedComplexity number gaming #1015

Open
opened 2026-10-15 17:55:27 +00:00 by jared · 0 comments
Owner

Problem

The built-in cop Metrics/PerceivedComplexity can be gamed by two edits that keep the code as complex. RuboCop cannot change that cop. Analysis: docs/research/lint-lessons/metrics-perceived-complexity.md, sections 2c, 6a, and 6b.

  1. Numbered parameters. RuboCop 1.90 scores a block { _1 ... } (and it) as 0. It scores { |x| ... } as 1. Section 6b: @watch.to_h { [_1, ... ? ... : nil] } if @watch scores 4 against 5 for the named form.
  2. Split predicate. paths.select { a && b } becomes paths.select { a }.select { b }. The && point disappears. Section 6a: this takes lint_target from 5 to 4.

Both edits pass the gate and keep the smell.

Reproduce

n/a: design ticket. Sketch for the numbered-parameter case: bundle exec rubocop --only Metrics/PerceivedComplexity on a method with paths.select { _1.end_with?(".rb") && _1.start_with?(root) } scores one point less than the named-parameter form.

Approach

Add a new Sdlc cop. The implementer picks the name and the department (Sdlc/Metrics or Sdlc/Structural). The implementer probes each shape first and ships only the shapes that are decidable with no false positives.

(a) Numbered-parameter and it blocks. Decidable. Candidate rule: report a conditional, ternary, &&, or || inside a numbered-parameter or it block whose method is on RuboCop's own iterating-method list, because that is the point the built-in cop drops. Consider the alternative: a perceived-complexity variant that scores numbered blocks like named blocks. Use RuboCop's own list of iterating methods, not a hand-kept list.

(b) Two consecutive select, reject, or filter calls on one receiver chain. OPEN QUESTION (Jared to rule): this shape may have false positives. select on a Sequel dataset selects columns, not rows. A receiver that is not an Enumerable cannot be told from syntax. The two blocks may be intentionally staged for readability. Probe plugins/ first. Ship (b) only if a deterministic receiver check removes the false positives. Otherwise leave (b) out and record the miss.

Required work:

  1. Add the new cop and register it — check: present Sdlc/Metrics in plugins/os-sdlc/.rubocop.yml

Acceptance criteria

  1. Given a method with paths.select { _1.end_with?(".rb") && _1.start_with?(root) }, when the cop runs, then it reports the numbered-parameter block.
  2. Given the same block with it in place of _1, when the cop runs, then it reports the block.
  3. Given a numbered-parameter block with no conditional, such as paths.map { _1.to_s }, when the cop runs, then it reports nothing.
  4. Given shape (b) is shipped, paths.select { a }.select { b } on an Enumerable receiver, when the cop runs, then it reports the second call. Given a Sequel dataset select chain, then it reports nothing.

Owner sketch

class NumberedBlockHidesBranch < RuboCop::Cop::Base
  def on_numblock(node)
end

Non-goals

Changing Metrics/PerceivedComplexity. Fixes at the #1004 and #1005 sites. The lesson text.

Origin

  • Skeptic: VERDICT: CREATE (user-requested ticket)
  • Trigger: lint-lesson analysis of the Metrics/PerceivedComplexity offenses in #1004 and #1005 (docs/research/lint-lessons/metrics-perceived-complexity.md)
  • Improvised this session: none
  • Chain: complexity points dropped by number-gaming edits ← built-in cop skips numbered blocks and counts one && ← DESIGN (analysis sections 2c, 6a, 6b)
  • Root candidate: none
  • Where: plugins/os-sdlc/.rubocop.yml
  • Session: 270dfee2-6a92-4e87-828c-9ac8c32c1cd7
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-prism-architect/270dfee2-6a92-4e87-828c-9ac8c32c1cd7.jsonl
## Problem The built-in cop `Metrics/PerceivedComplexity` can be gamed by two edits that keep the code as complex. RuboCop cannot change that cop. Analysis: docs/research/lint-lessons/metrics-perceived-complexity.md, sections 2c, 6a, and 6b. 1. Numbered parameters. RuboCop 1.90 scores a block `{ _1 ... }` (and `it`) as 0. It scores `{ |x| ... }` as 1. Section 6b: `@watch.to_h { [_1, ... ? ... : nil] } if @watch` scores 4 against 5 for the named form. 2. Split predicate. `paths.select { a && b }` becomes `paths.select { a }.select { b }`. The `&&` point disappears. Section 6a: this takes `lint_target` from 5 to 4. Both edits pass the gate and keep the smell. ## Reproduce n/a: design ticket. Sketch for the numbered-parameter case: `bundle exec rubocop --only Metrics/PerceivedComplexity` on a method with `paths.select { _1.end_with?(".rb") && _1.start_with?(root) }` scores one point less than the named-parameter form. ## Approach Add a new Sdlc cop. The implementer picks the name and the department (Sdlc/Metrics or Sdlc/Structural). The implementer probes each shape first and ships only the shapes that are decidable with no false positives. (a) Numbered-parameter and `it` blocks. Decidable. Candidate rule: report a conditional, ternary, `&&`, or `||` inside a numbered-parameter or `it` block whose method is on RuboCop's own iterating-method list, because that is the point the built-in cop drops. Consider the alternative: a perceived-complexity variant that scores numbered blocks like named blocks. Use RuboCop's own list of iterating methods, not a hand-kept list. (b) Two consecutive `select`, `reject`, or `filter` calls on one receiver chain. OPEN QUESTION (Jared to rule): this shape may have false positives. `select` on a Sequel dataset selects columns, not rows. A receiver that is not an Enumerable cannot be told from syntax. The two blocks may be intentionally staged for readability. Probe plugins/ first. Ship (b) only if a deterministic receiver check removes the false positives. Otherwise leave (b) out and record the miss. Required work: 1. Add the new cop and register it — check: present `Sdlc/Metrics` in `plugins/os-sdlc/.rubocop.yml` ## Acceptance criteria 1. Given a method with `paths.select { _1.end_with?(".rb") && _1.start_with?(root) }`, when the cop runs, then it reports the numbered-parameter block. 2. Given the same block with `it` in place of `_1`, when the cop runs, then it reports the block. 3. Given a numbered-parameter block with no conditional, such as `paths.map { _1.to_s }`, when the cop runs, then it reports nothing. 4. Given shape (b) is shipped, `paths.select { a }.select { b }` on an Enumerable receiver, when the cop runs, then it reports the second call. Given a Sequel dataset `select` chain, then it reports nothing. ## Owner sketch ```ruby class NumberedBlockHidesBranch < RuboCop::Cop::Base def on_numblock(node) end ``` ## Non-goals Changing `Metrics/PerceivedComplexity`. Fixes at the #1004 and #1005 sites. The lesson text. ## Origin - Skeptic: VERDICT: CREATE (user-requested ticket) - Trigger: lint-lesson analysis of the `Metrics/PerceivedComplexity` offenses in #1004 and #1005 (docs/research/lint-lessons/metrics-perceived-complexity.md) - Improvised this session: none - Chain: complexity points dropped by number-gaming edits ← built-in cop skips numbered blocks and counts one `&&` ← DESIGN (analysis sections 2c, 6a, 6b) - Root candidate: none - Where: plugins/os-sdlc/.rubocop.yml - Session: 270dfee2-6a92-4e87-828c-9ac8c32c1cd7 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-prism-architect/270dfee2-6a92-4e87-828c-9ac8c32c1cd7.jsonl
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#1015
No description provided.