Skip to content

[bot] Fix Rails/Blank#186

Closed
6[bot] wants to merge 19 commits intomainfrom
fix/rails-blank-23517202617
Closed

[bot] Fix Rails/Blank#186
6[bot] wants to merge 19 commits intomainfrom
fix/rails-blank-23517202617

Conversation

@6
Copy link
Copy Markdown
Contributor

@6 6 bot commented Mar 24, 2026

Automated fix fix for Rails/Blank corpus conformance.

Refs #160

Details

Cop Rails/Blank
Backend claude-oauth / hard
Model Claude Opus 4.6 (OAuth, high)
Mode fix
Turns 22
Run #85

Result

Done. The fix was straightforward:

**Root cause:** The `check_nil_or_empty` method hardcoded `||` in the diagnostic message (`left_str || right_str`). Ruby's `or` keyword also produces an `OrNode` in Prism, so detection worked fine, but the message said `||` instead of `or`. The corpus comparison flagged these as FN because the expected message (from RuboCop using `node.source`) used `or`.

**Fix:**
- Changed the message to use `or_node.location().as_slice()` (the full OrNode source text) instead of concatenating left/right with hardcoded `||`
- Added 6 test cases covering: standalone `or` expression, modifier `if` with `or`, indexed receiver with `or`, modifier `unless` with `||`, `break if` with `or`, and chained `or` expressions

Changes

  2 files changed
Agent conversation excerpt

ToolSearch

Read: /home/runner/work/nitrocop/nitrocop/docs/agent-ci.md

Read: /home/runner/work/nitrocop/nitrocop/src/cop/rails/blank.rs

Read: /home/runner/work/nitrocop/nitrocop/tests/fixtures/cops/rails/blank/offense.rb

Let me run the existing tests to see the current state of failures.

Bash: cargo test --lib -- cop::rails::blank 2>&1 | tail -60

The test reveals the detection works but two things are wrong:

  1. The message hardcodes || instead of using the actual operator (or vs ||)
  2. The fixture ^ positions are at column 0 instead of the OrNode's actual column

Let me fix both issues.

TodoWrite

Edit: /home/runner/work/nitrocop/nitrocop/src/cop/rails/blank.rs

Now I need to remove the unused left_src from nil_check_receiver's return type — wait, actually left_src is still returned from nil_check_receiver but no longer used in the message. Let me check if it's used elsewhere.

Grep: left_src

Edit: /home/runner/work/nitrocop/nitrocop/src/cop/rails/blank.rs

TodoWrite

Now fix the fixture annotations. I need to calculate correct ^ positions for each new test case. Let me also simplify by removing duplicate test cases and ensuring clean Ruby syntax.

Edit: /home/runner/work/nitrocop/nitrocop/tests/fixtures/cops/rails/blank/offense.rb

TodoWrite

Bash: cargo test --lib -- cop::rails::blank 2>&1 | tail -30

Both tests pass. Now let me add the doc comment and verify the fixture looks correct.

Read: /home/runner/work/nitrocop/nitrocop/tests/fixtures/cops/rails/blank/offense.rb

Now let me add the doc comment on the cop struct documenting this fix.

Edit: /home/runner/work/nitrocop/nitrocop/src/cop/rails/blank.rs

Bash: cargo test --lib -- cop::rails::blank 2>&1 | tail -10

Tests pass. Let me run clippy and fmt on the changed file.

Bash: cargo fmt -- src/cop/rails/blank.rs && cargo clippy --release -- -D warnings 2>&1 | tail -15

All clean — no clippy warnings, no format changes needed. Now let me commit.

TodoWrite

Bash: git diff --stat

Bash: `git add src/cop/rails/blank.rs tests/fixtures/cops/rails/blank/offense.rb && git commit -m "$(cat <<'EOF'
Fix Rails/Blank: use actual operator in NilOrEmpty message (or vs ||)

The NilOrEmpty check ha`

TodoWrite

Done. The fix was straightforward:

Root cause: The check_nil_or_empty method hardcoded || in the diagnostic message (left_str || right_str). Ruby's or keyword also produces an OrNode in Prism, so detection worked fine, but the message said || instead of or. The corpus comparison flagged these as FN because the expected message (from RuboCop using node.source) used or.

Fix:

  • Changed the message to use or_node.location().as_slice() (the full OrNode source text) instead of concatenating left/right with hardcoded ||
  • Added 6 test cases covering: standalone or expression, modifier if with or, indexed receiver with or, modifier unless with ||, break if with or, and chained or expressions
Task prompt (10435 tokens)

Fix Rails/Blank — 0 FP, 14 FN

Instructions

You are fixing ONE cop in nitrocop, a Rust Ruby linter that uses Prism for parsing.

Current state: 6,472 matches, 0 false positives, 14 false negatives.
Focus on: FN (RuboCop flags code nitrocop misses).

Workflow

  1. Read the Pre-diagnostic Results and Corpus FP/FN Examples sections below first
  2. Verify with RuboCop first (for FP fixes): before writing any code, confirm RuboCop's
    behavior on BOTH the specific FP case AND the general pattern:
    echo '<specific FP case>' > /tmp/test.rb && rubocop --only Rails/Blank /tmp/test.rb
    echo '<general pattern>' > /tmp/test.rb && rubocop --only Rails/Blank /tmp/test.rb
    If RuboCop flags the general pattern, your fix must be narrow enough to not suppress it.
  3. Add a test case FIRST:
    • FN fix: add the missed pattern to tests/fixtures/cops/rails/blank/offense.rb with ^ annotation
    • FP fix: add the false-positive pattern to tests/fixtures/cops/rails/blank/no_offense.rb
  4. Verify test fails: cargo test --lib -- cop::rails::blank
  5. Fix src/cop/rails/blank.rs
  6. Verify test passes: cargo test --lib -- cop::rails::blank
  7. Add a /// doc comment on the cop struct documenting what you found and fixed
  8. Commit only your cop's files

Fixture Format

Mark offenses with ^ markers on the line AFTER the offending source line:

x = 1
     ^^ Rails/Blank: Trailing whitespace detected.

The ^ characters must align with the offending columns. The message format is Rails/Blank: <message text>.

If your test passes immediately

If you add a test case and it passes without code changes, the corpus mismatch is
caused by config/context differences, not a detection bug.
Do NOT loop trying to make the test fail. Instead:

  1. Investigate config resolution (Include/Exclude, cop enablement, disable comments)
  2. The fix is likely in src/config/ or the cop's config handling, not detection logic
  3. If you cannot determine the root cause within 5 minutes, document your findings as
    a /// comment on the cop struct and commit

CRITICAL: Avoid regressions in the opposite direction

When fixing FPs, your change MUST NOT suppress legitimate detections. When fixing FNs,
your change MUST NOT flag code that RuboCop accepts. A fix that eliminates a few issues
in one direction but introduces hundreds in the other is a catastrophic regression.

Before exempting a category of patterns, verify with RuboCop that the general case
is still an offense:

rubocop --only Rails/Blank /tmp/test.rb

If RuboCop flags the general pattern but not your specific case, the difference is in
a narrow context (e.g., enclosing structure, receiver type, argument count) — your fix
must target that specific context, not the broad category.

Rule of thumb: if your fix adds an early return or continue that skips a whole
node type, operator class, or naming pattern, it's probably too broad. Prefer adding a
condition that matches the SPECIFIC differentiating context.

Rules

  • Only modify src/cop/rails/blank.rs and tests/fixtures/cops/rails/blank/
  • Run cargo test --lib -- cop::rails::blank to verify your fix (do NOT run the full test suite)
  • Do NOT touch unrelated files
  • Do NOT use git stash

Start Here

Use the existing corpus data to focus on the most concentrated regressions first.

Helpful local commands:

  • python3 scripts/investigate_cop.py Rails/Blank --repos-only
  • python3 scripts/investigate_cop.py Rails/Blank --context
  • python3 scripts/verify_cop_locations.py Rails/Blank

Top FN repos:

  • databasically__lowdown__d593927 (5 FN) — example vendor/rails/actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb:594
  • cjstewart88__Tubalr__f6956c8 (4 FN) — example heroku/ruby/1.9.1/gems/rdoc-3.8/lib/rdoc/parser/c.rb:784
  • pitluga__supply_drop__d64c50c (4 FN) — example examples/vendored-puppet/vendor/puppet-2.7.8/lib/puppet/util/adsi.rb:161

Representative FN examples:

  • cjstewart88__Tubalr__f6956c8: heroku/ruby/1.9.1/gems/rdoc-3.8/lib/rdoc/parser/c.rb:784 — Use elements.blank? instead of elements.nil? or elements.empty?.
  • cjstewart88__Tubalr__f6956c8: heroku/ruby/1.9.1/gems/rdoc-3.8/lib/rdoc/ri/driver.rb:865 — Use name.blank? instead of name.nil? or name.empty?.
  • cjstewart88__Tubalr__f6956c8: heroku/ruby/1.9.1/gems/rdoc-3.9.4/lib/rdoc/parser/c.rb:782 — Use elements.blank? instead of elements.nil? or elements.empty?.

Pre-diagnostic Results

Diagnosis Summary

Each example was tested by running nitrocop on the extracted source in isolation
with --force-default-config to determine if the issue is a code bug or config issue.
Note: source context is truncated and may not parse perfectly. If a diagnosis
seems wrong (e.g., your test passes immediately for a 'CODE BUG'), treat it as
a config/context issue instead.

  • FN: 14 code bug(s), 0 config/context issue(s)

FN #1: cjstewart88__Tubalr__f6956c8: heroku/ruby/1.9.1/gems/rdoc-3.8/lib/rdoc/parser/c.rb:784

NOT DETECTED — CODE BUG
The cop fails to detect this pattern. Fix the detection logic.

Enclosing structure: if branch (line: if type.downcase == 'const' then)
The offense is inside this structure — the cop may need
to handle this context to detect the pattern.

Message: Use elements.blank?instead ofelements.nil? or elements.empty?.

Ready-made test snippet (add to offense.rb, adjust ^ count):

      if elements.nil? or elements.empty? then
^ Rails/Blank: Use `elements.blank?` instead of `elements.nil? or elements.empty?`.

Full source context:

    # In the case of rb_define_const, the definition and comment are in
    # "/* definition: comment */" form.  The literal ':' and '\' characters
    # can be escaped with a backslash.
    if type.downcase == 'const' then
      elements = comment.split ':'

      if elements.nil? or elements.empty? then
        con = RDoc::Constant.new const_name, definition, comment
      else
        new_definition = elements[0..-2].join(':')

        if new_definition.empty? then # Default to literal C definition
          new_definition = definition
        else

FN #2: cjstewart88__Tubalr__f6956c8: heroku/ruby/1.9.1/gems/rdoc-3.8/lib/rdoc/ri/driver.rb:865

NOT DETECTED — CODE BUG
The cop fails to detect this pattern. Fix the detection logic.

Message: Use name.blank?instead ofname.nil? or name.empty?.

Ready-made test snippet (add to offense.rb, adjust ^ count):

      return if name.nil? or name.empty?
^ Rails/Blank: Use `name.blank?` instead of `name.nil? or name.empty?`.

Full source context:

      name = if defined? Readline then
               Readline.readline ">> "
             else
               print ">> "
               $stdin.gets
             end

      return if name.nil? or name.empty?

      name = expand_name name.strip

      begin
        display_name name
      rescue NotFoundError => e
        puts e.message

FN #3: cjstewart88__Tubalr__f6956c8: heroku/ruby/1.9.1/gems/rdoc-3.9.4/lib/rdoc/parser/c.rb:782

NOT DETECTED — CODE BUG
The cop fails to detect this pattern. Fix the detection logic.

Enclosing structure: if branch (line: if type.downcase == 'const' then)
The offense is inside this structure — the cop may need
to handle this context to detect the pattern.

Message: Use elements.blank?instead ofelements.nil? or elements.empty?.

Ready-made test snippet (add to offense.rb, adjust ^ count):

      if elements.nil? or elements.empty? then
^ Rails/Blank: Use `elements.blank?` instead of `elements.nil? or elements.empty?`.

Full source context:

    # In the case of rb_define_const, the definition and comment are in
    # "/* definition: comment */" form.  The literal ':' and '\' characters
    # can be escaped with a backslash.
    if type.downcase == 'const' then
      elements = comment.split ':'

      if elements.nil? or elements.empty? then
        con = RDoc::Constant.new const_name, definition, comment
      else
        new_definition = elements[0..-2].join(':')

        if new_definition.empty? then # Default to literal C definition
          new_definition = definition
        else

FN #4: cjstewart88__Tubalr__f6956c8: heroku/ruby/1.9.1/gems/rdoc-3.9.4/lib/rdoc/ri/driver.rb:878

NOT DETECTED — CODE BUG
The cop fails to detect this pattern. Fix the detection logic.

Message: Use name.blank?instead ofname.nil? or name.empty?.

Ready-made test snippet (add to offense.rb, adjust ^ count):

      return if name.nil? or name.empty?
^ Rails/Blank: Use `name.blank?` instead of `name.nil? or name.empty?`.

Full source context:

      name = if defined? Readline then
               Readline.readline ">> "
             else
               print ">> "
               $stdin.gets
             end

      return if name.nil? or name.empty?

      name = expand_name name.strip

      begin
        display_name name
      rescue NotFoundError => e
        puts e.message

FN #5: databasically__lowdown__d593927: vendor/rails/actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb:594

NOT DETECTED — CODE BUG
The cop fails to detect this pattern. Fix the detection logic.

Enclosing structure: method body (line: def __format(to_wrap) #:nodoc:)
The offense is inside this structure — the cop may need
to handle this context to detect the pattern.

Message: Use words[0].blank?instead ofwords[0].nil? or words[0].empty?.

Ready-made test snippet (add to offense.rb, adjust ^ count):

      words.shift if words[0].nil? or words[0].empty?
^ Rails/Blank: Use `words[0].blank?` instead of `words[0].nil? or words[0].empty?`.

Full source context:

  private
    def __do_split_word(word, size) #:nodoc:
      [word[0 .. (size - 1)], word[size .. -1]]
    end

    def __format(to_wrap) #:nodoc:
      words = to_wrap.split(/\s+/).compact
      words.shift if words[0].nil? or words[0].empty?
      to_wrap = []

      abbrev = false
      width = @columns - @first_indent - @left_margin - @right_margin
      indent_str = ' ' * @first_indent
      first_line = true
      line = words.shift

FN #6: databasically__lowdown__d593927: vendor/rails/actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb:602

NOT DETECTED — CODE BUG
The cop fails to detect this pattern. Fix the detection logic.

Message: Use line.blank?instead ofline.nil? || line.empty?.

Ready-made test snippet (add to offense.rb, adjust ^ count):

      abbrev = __is_abbrev(line) unless line.nil? || line.empty?
^ Rails/Blank: Use `line.blank?` instead of `line.nil? || line.empty?`.

Full source context:

      to_wrap = []

      abbrev = false
      width = @columns - @first_indent - @left_margin - @right_margin
      indent_str = ' ' * @first_indent
      first_line = true
      line = words.shift
      abbrev = __is_abbrev(line) unless line.nil? || line.empty?

      while w = words.shift
        if (w.size + line.size < (width - 1)) ||
           ((line !~ LEQ_RE || abbrev) && (w.size + line.size < width))
          line << " " if (line =~ LEQ_RE) && (not abbrev)
          line << " #{w}"
        else

FN #7: databasically__lowdown__d593927: vendor/rails/actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb:630

NOT DETECTED — CODE BUG
The cop fails to detect this pattern. Fix the detection logic.

Enclosing structure: block (do..end) (line: loop do)
The offense is inside this structure — the cop may need
to handle this context to detect the pattern.

Message: Use line.blank?instead ofline.nil? or line.empty?.

Ready-made test snippet (add to offense.rb, adjust ^ count):

        break if line.nil? or line.empty?
^ Rails/Blank: Use `line.blank?` instead of `line.nil? or line.empty?`.

Full source context:

          line = w
        end

        abbrev = __is_abbrev(w) unless w.nil?
      end

      loop do
        break if line.nil? or line.empty?
        line, w = __do_hyphenate(line, w, width) if @hard_margins
        to_wrap << __make_line(line, indent_str, width, w.nil?)
        line = w
      end

      if (@tag_paragraph && (to_wrap.size > 0)) then
        clr = %r{`(\w+)'}.match([caller(1)].flatten[0])[1]

FN #8: databasically__lowdown__d593927: vendor/rails/actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb:790

NOT DETECTED — CODE BUG
The cop fails to detect this pattern. Fix the detection logic.

Enclosing structure: enclosing line: else
The offense is inside this structure — the cop may need
to handle this context to detect the pattern.

Message: Use rnext.blank?instead ofrnext.nil? or rnext.empty?.

Ready-made test snippet (add to offense.rb, adjust ^ count):

          break if rnext.nil? or rnext.empty? or rline.nil? or rline.empty?
^ Rails/Blank: Use `rnext.blank?` instead of `rnext.nil? or rnext.empty?`.

Full source context:

            words[-1] = first
            @split_words << SplitWord.new(word, first, rest)
          end
          rline = words.join(' ').strip
          rnext = "#{rest} #{rnext}".strip
          break
        else
          break if rnext.nil? or rnext.empty? or rline.nil? or rline.empty?
          words = rnext.split(/\s+/)
          word = words.shift
          size = width - rline.size - 1

          if (size <= 0)
            rnext = "#{word} #{words.join(' ')}".strip
            break

Omitted 6 additional diagnosed FN example(s) for brevity.

Current Rust Implementation

src/cop/rails/blank.rs

use crate::cop::{Cop, CopConfig};
use crate::diagnostic::{Diagnostic, Severity};
use crate::parse::codemap::CodeMap;
use crate::parse::source::SourceFile;
use ruby_prism::Visit;

/// Rails/Blank - flags code that can be simplified using `Object#blank?` from Active Support.
///
/// ## Investigation findings (2026-03-08)
///
/// **FN root cause (1095 FN):** The `UnlessPresent` pattern was not implemented at all.
/// The config option was read but the variable was prefixed with `_` and never used.
/// RuboCop's `on_if` handler flags `unless foo.present?` (both modifier and block forms)
/// suggesting `if foo.blank?` instead.
///
/// **Fix:** Implemented `UnlessPresent` check via a custom AST visitor in `check_source`.
/// The visitor handles both modifier (`something unless foo.present?`) and block
/// (`unless foo.present? ... end`) forms. Skips unless-with-else when Style/UnlessElse
/// would conflict (conservative: always skip when else clause is present).
///
/// **FP root cause (27 FP, first batch):** Missing `defining_blank?` check. RuboCop skips
/// `!present?` when it appears inside `def blank?` (defining blank? in terms of present?).
/// Nitrocop was incorrectly flagging these as offenses.
///
/// **Fix:** Added parent context tracking in the visitor. When inside a `def blank?` method,
/// `!present?` calls are suppressed.
///
/// **FP root cause (34 FP, second batch, 2026-03-10):** `nil_check_receiver` was matching
/// `!foo` (boolean negation, method name `!`) as a nil-check pattern. This caused
/// `!foo || foo.empty?` to be flagged as NilOrEmpty, but RuboCop's `nil_or_empty?`
/// NodePattern only matches explicit nil checks: `nil?`, `== nil`, `nil ==`. It does NOT
/// match `!foo`. The `!foo` branch was incorrectly added to `nil_check_receiver`.
///
/// **Fix:** Removed the `!` method name branch from `nil_check_receiver`. Only `nil?`,
/// `== nil`, and `nil == foo` are valid nil-check patterns for the NilOrEmpty check.
///
/// ## Investigation (2026-03-14)
///
/// **FP root cause (34 FP, third batch):** `present?` called WITH arguments was flagged.
/// RuboCop's NodePattern `(send (send $_ :present?) :!)` only matches when `present?` has
/// NO arguments. Calls like `!Helpers.present?(value)` or `unless Helpers.present?(value)`
/// use `present?` as a class method with an argument — RuboCop skips these.
///
/// Fix: Added argument count check in `check_not_present` and `check_unless_present`.
///
/// ## Investigation (2026-03-15)
///
/// **FP root cause (6 FP remaining):** Safe navigation calls were flagged.
/// - `unless response&.strip&.present?` → `check_unless_present` was matching `&.present?`
///   but RuboCop's `(send $_ :present?)` only matches `send` not `csend`.
/// - `foo.nil? || foo&.empty?` → `check_nil_or_empty` was matching `&.empty?` right side
///   but RuboCop's `(send $_ :empty?)` only matches `send` not `csend`.
///
/// Fix: Added `call_operator_loc() == &.` check to skip safe navigation calls in
/// `check_unless_present` and `check_nil_or_empty`.
///
/// ## Investigation (2026-03-15, second pass)
///
/// **FP root cause (2 FP remaining):** Pattern match guards `in "div" unless element.at("div").present?`
/// are parsed by Prism as `UnlessNode` inside `InNode`. RuboCop's `on_if` handler does not
/// visit these guard nodes. The cop was incorrectly visiting them.
///
/// Fix: Added `inside_in_node` context tracking. When inside an `InNode`, `check_unless_present`
/// is skipped.
///
/// ## Investigation (2026-03-15, round 3)
///
/// **FN root cause (2 FN):** `nil? || empty?` with implicit receiver (self)
/// was not matched. `nil_check_receiver` required an explicit receiver via
/// `receiver_source` which returns None for receiverless calls.
/// Fix: Changed `nil_check_receiver` return type to `Option<(Option<&[u8]>, &[u8])>` so
/// None receiver (implicit self) is preserved. Updated `check_nil_or_empty` to match
/// when both nil-check and empty? have None receivers (both implicit self).
///
/// **FN root cause (112 FN):** The `!foo || foo.empty?` pattern was not matched by
/// `nil_check_receiver`. RuboCop's `nil_or_empty?` NodePattern includes `(send $_ :!)` as one
/// of the left-side alternatives, meaning `!foo || foo.empty?` is a valid NilOrEmpty offense.
/// The previous fix (2026-03-10) incorrectly removed this pattern, because the old
/// implementation was treating `!foo` as the full left source text instead of extracting `foo`
/// (the receiver of `!`) as the variable to compare with `empty?`'s receiver.
///
/// Fix: Re-added `!` method name to `nil_check_receiver`. Now correctly extracts the receiver
/// of `!` (i.e., `foo` from `!foo`) as the variable, matching RuboCop's `(send $_ :!)` capture.
///
/// ## Investigation (2026-03-19)
///
/// **FP root cause (1 FP):** `!pkey_cols&.present?` uses safe navigation on `present?`.
/// RuboCop's `not_present?` NodePattern `(send (send $_ :present?) :!)` only matches `send`,
/// not `csend`. When `pkey_cols` is nil, `nil&.present?` returns `nil` and `!nil` is `true`,
/// but `pkey_cols&.blank?` would return `nil` (falsy). So the replacement changes semantics.
///
/// Fix: Added `call_operator_loc() == &.` check in `check_not_present` to skip safe navigation.
pub struct Blank;

/// Extract the receiver source text from a CallNode, returning None if absent.
fn receiver_source<'a>(call: &ruby_prism::CallNode<'a>) -> Option<&'a [u8]> {
    call.receiver().map(|r| r.location().as_slice())
}

/// Check if the left side of an OR node matches a nil-check-like pattern:
/// - `foo.nil?` or `nil?` (implicit self)
/// - `foo == nil`
/// - `nil == foo`
/// - `!foo` (boolean negation — RuboCop's `(send $_ :!)` pattern)
///
/// Returns (receiver source bytes option, left side source bytes) if matched.
/// The receiver is `None` when using implicit self (e.g., bare `nil?`).
/// RuboCop's `nil_or_empty?` NodePattern matches all four forms.
fn nil_check_receiver<'a>(node: &ruby_prism::Node<'a>) -> Option<(Option<&'a [u8]>, &'a [u8])> {
    let call = node.as_call_node()?;
    let method = call.name().as_slice();
    let left_src = node.location().as_slice();

    if method == b"nil?" {
        // foo.nil? or bare nil? (implicit self)
        return Some((receiver_source(&call), left_src));
    }

    if method == b"!" {
        // !foo — boolean negation. RuboCop's `(send $_ :!)` captures the receiver.
        return receiver_source(&call).map(|r| (Some(r), left_src));
    }

    if method == b"==" {
        // foo == nil  or  nil == foo
        let recv = call.receiver()?;
        let args = call.arguments()?;
        let arg_list: Vec<_> = args.arguments().iter().collect();
        if arg_list.len() != 1 {
            return None;
        }
        let arg = &arg_list[0];

        if arg.as_nil_node().is_some() {
            // foo == nil → receiver is foo
            return Some((Some(recv.location().as_slice()), left_src));
        }
        if recv.as_nil_node().is_some() {
            // nil == foo → receiver is arg
            return Some((Some(arg.location().as_slice()), left_src));
        }
    }

    None
}

impl Cop for Blank {
    fn name(&self) -> &'static str {
        "Rails/Blank"
    }

    fn default_severity(&self) -> Severity {
        Severity::Convention
    }

    fn check_source(
        &self,
        source: &SourceFile,
        parse_result: &ruby_prism::ParseResult<'_>,
        _code_map: &CodeMap,
        config: &CopConfig,
        diagnostics: &mut Vec<Diagnostic>,
        _corrections: Option<&mut Vec<crate::correction::Correction>>,
    ) {
        let nil_or_empty = config.get_bool("NilOrEmpty", true);
        let not_present = config.get_bool("NotPresent", true);
        let unless_present = config.get_bool("UnlessPresent", true);

        let mut visitor = BlankVisitor {
            cop: self,
            source,
            nil_or_empty,
            not_present,
            unless_present,
            inside_def_blank: false,
            inside_in_node: false,
            diagnostics: Vec::new(),
        };
        visitor.visit(&parse_result.node());
        diagnostics.extend(visitor.diagnostics);
    }
}

struct BlankVisitor<'a, 'src> {
    cop: &'a Blank,
    source: &'src SourceFile,
    nil_or_empty: bool,
    not_present: bool,
    unless_present: bool,
    inside_def_blank: bool,
    inside_in_node: bool,
    diagnostics: Vec<Diagnostic>,
}

impl<'pr> BlankVisitor<'_, '_> {
    /// Check NilOrEmpty: `foo.nil? || foo.empty?` or `nil? || empty?` (implicit self)
    fn check_nil_or_empty(&mut self, or_node: &ruby_prism::OrNode<'pr>) {
        let left = or_node.left();
        let right = or_node.right();

        if let Some((nil_recv, left_src)) = nil_check_receiver(&left) {
            // Right side must be `<same>.empty?` — NOT safe navigation (`&.empty?`)
            // RuboCop's NodePattern `(send $_ :empty?)` only matches send, not csend.
            if let Some(right_call) = right.as_call_node() {
                let is_safe_nav = right_call
                    .call_operator_loc()
                    .is_some_and(|loc| loc.as_slice() == b"&.");
                if right_call.name().as_slice() == b"empty?" && !is_safe_nav {
                    let empty_recv = receiver_source(&right_call);
                    // Both receivers must match: both Some with same source, or both None (implicit self)
                    if nil_recv == empty_recv {
                        let loc = or_node.location();
                        let (line, column) = self.source.offset_to_line_col(loc.start_offset());
                        let left_str = std::str::from_utf8(left_src).unwrap_or("nil?");
                        let right_str =
                            std::str::from_utf8(right.location().as_slice()).unwrap_or("empty?");
                        let message = match nil_recv {
                            Some(recv_bytes) => {
                                let recv_str = std::str::from_utf8(recv_bytes).unwrap_or("object");
                                format!(
                                    "Use `{recv_str}.blank?` instead of `{left_str} || {right_str}`."
                                )
                            }
                            None => {
                                format!("Use `blank?` instead of `{left_str} || {right_str}`.")
                            }
                        };
                        self.diagnostics.push(self.cop.diagnostic(
                            self.source,
                            line,
                            column,
                            message,
                        ));
                    }
                }
            }
        }
    }

    /// Check NotPresent: `!foo.present?`
    fn check_not_present(&mut self, call: &ruby_prism::CallNode<'pr>) {
        if call.name().as_slice() != b"!" {
            return;
        }

        let receiver = match call.receiver() {
            Some(r) => r,
            None => return,
        };

        let inner_call = match receiver.as_call_node() {
            Some(c) => c,
            None => return,
        };

        if inner_call.name().as_slice() != b"present?" {
            return;
        }

        // RuboCop's NodePattern `(send (send $_ :present?) :!)` only matches `send`, not `csend`.
        // `!pkey_cols&.present?` uses safe navigation — semantics differ from `!pkey_cols.blank?`
        // because `nil&.present?` returns `nil`, `!nil` is `true`, but `nil&.blank?` returns `nil`.
        if inner_call
            .call_operator_loc()
            .is_some_and(|loc| loc.as_slice() == b"&.")
        {
            return;
        }

        // RuboCop's NodePattern `(send (send $_ :present?) :!)` only matches present? with
        // NO arguments. `!Helpers.present?(value)` (class method style) must not be flagged.
        if inner_call
            .arguments()
            .is_some_and(|a| !a.arguments().is_empty())
        {
            return;
        }

        // Skip !present? inside def blank? (defining blank? in terms of present?)
        if self.inside_def_blank {
            return;
        }

        let loc = call.location();
        let (line, column) = self.source.offset_to_line_col(loc.start_offset());
        self.diagnostics.push(self.cop.diagnostic(
            self.source,
            line,
            column,
            "Use `blank?` instead of `!present?`.".to_string(),
        ));
    }

    /// Check UnlessPresent: `unless foo.present?` or `something unless foo.present?`
    fn check_unless_present(&mut self, unless_node: &ruby_prism::UnlessNode<'pr>) {
        // Skip pattern match guards: `in pattern unless condition`
        // In Prism, pattern match guards are represented as UnlessNodes inside InNodes.
        // RuboCop's `on_if` handler does not visit these guards.
        if self.inside_in_node {
            return;
        }

        // Skip unless-with-else (Style/UnlessElse interaction)
        // Conservative: always skip when else clause is present
        if unless_node.else_clause().is_some() {
            return;
        }

        let predicate = unless_node.predicate();
        let pred_call = match predicate.as_call_node() {
            Some(c) => c,
            None => return,
        };

        if pred_call.name().as_slice() != b"present?" {
            return;
        }

        // RuboCop's NodePattern `(send $_ :present?)` only matches send (not csend).
        // `unless obj&.present?` uses safe navigation and must NOT be flagged.
        if pred_call
            .call_operator_loc()
            .is_some_and(|loc| loc.as_slice() == b"&.")
        {
            return;
        }

        // RuboCop's NodePattern `(send $_ :present?)` only matches present? with NO arguments.
        if pred_call
            .arguments()
            .is_some_and(|a| !a.arguments().is_empty())
        {
            return;
        }

        // Build the receiver string for the message
        let recv_str = match pred_call.receiver() {
            Some(r) => {
                let src = r.location().as_slice();
                format!("{}.blank?", std::str::from_utf8(src).unwrap_or("object"))
            }
            None => "blank?".to_string(),
        };

        // Build the "current" string for the message
        let predicate_src =
            std::str::from_utf8(predicate.location().as_slice()).unwrap_or("present?");
        let current = format!("unless {predicate_src}");

        // Determine offense location based on modifier vs block form
        // For modifier form: `something unless foo.present?` → offense on `unless foo.present?`
        // For block form: `unless foo.present?\n...\nend` → offense on `unless foo.present?`
        let unless_loc = unless_node.location();
        let pred_loc = predicate.location();

        // The offense covers from the start of `unless` keyword to the end of the predicate
        // For modifier form, the keyword is in the middle; for block form, it's at the start
        let keyword_loc = unless_node.keyword_loc();
        let offense_start = keyword_loc.start_offset();
        let offense_end = pred_loc.end_offset();

        // Check if this is modifier form by comparing keyword start to node start
        let is_modifier = keyword_loc.start_offset() > unless_loc.start_offset();

        let (line, column) = if is_modifier {
            self.source.offset_to_line_col(offense_start)
        } else {
            // Block form: offense starts at the `unless` keyword (= node start)
            self.source.offset_to_line_col(offense_start)
        };

        // For the offense range length, count from keyword to end of predicate
        let _ = offense_end; // used implicitly via the annotation range

        self.diagnostics.push(self.cop.diagnostic(
            self.source,
            line,
            column,
            format!("Use `if {recv_str}` instead of `{current}`."),
        ));
    }
}

impl<'pr> Visit<'pr> for BlankVisitor<'_, '_> {
    fn visit_or_node(&mut self, node: &ruby_prism::OrNode<'pr>) {
        if self.nil_or_empty {
            self.check_nil_or_empty(node);
        }
        // Continue visiting children
        self.visit(&node.left());
        self.visit(&node.right());
    }

    fn visit_call_node(&mut self, node: &ruby_prism::CallNode<'pr>) {
        if self.not_present {
            self.check_not_present(node);
        }
        // Visit children (receiver, arguments, block)
        if let Some(recv) = node.receiver() {
            self.visit(&recv);
        }
        if let Some(args) = node.arguments() {
            for arg in args.arguments().iter() {
                self.visit(&arg);
            }
        }
        if let Some(block) = node.block() {
            self.visit(&block);
        }
    }

    fn visit_unless_node(&mut self, node: &ruby_prism::UnlessNode<'pr>) {
        if self.unless_present {
            self.check_unless_present(node);
        }
        // Visit children
        self.visit(&node.predicate());
        if let Some(stmts) = node.statements() {
            self.visit(&stmts.as_node());
        }
        if let Some(else_clause) = node.else_clause() {
            self.visit(&else_clause.as_node());
        }
    }

    fn visit_in_node(&mut self, node: &ruby_prism::InNode<'pr>) {
        let was_inside = self.inside_in_node;
        self.inside_in_node = true;
        ruby_prism::visit_in_node(self, node);
        self.inside_in_node = was_inside;
    }

    fn visit_def_node(&mut self, node: &ruby_prism::DefNode<'pr>) {
        let is_blank = node.name().as_slice() == b"blank?";
        let was_inside = self.inside_def_blank;
        if is_blank {
            self.inside_def_blank = true;
        }

        // Visit children: parameters and body
        if let Some(params) = node.parameters() {
            self.visit(&params.as_node());
        }
        if let Some(body) = node.body() {
            self.visit(&body);
        }

        self.inside_def_blank = was_inside;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    crate::cop_fixture_tests!(Blank, "cops/rails/blank");
}

RuboCop Ruby Implementation (ground truth)

vendor/rubocop-rails/lib/rubocop/cop/rails/blank.rb

# frozen_string_literal: true

module RuboCop
  module Cop
    module Rails
      # Checks for code that can be written with simpler conditionals
      # using `Object#blank?` defined by Active Support.
      #
      # Interaction with `Style/UnlessElse`:
      # The configuration of `NotPresent` will not produce an offense in the
      # context of `unless else` if `Style/UnlessElse` is enabled. This is
      # to prevent interference between the autocorrection of the two cops.
      #
      # @safety
      #   This cop is unsafe autocorrection, because `' '.empty?` returns false,
      #   but `' '.blank?` returns true. Therefore, autocorrection is not compatible
      #   if the receiver is a non-empty blank string, tab, or newline meta characters.
      #
      # @example NilOrEmpty: true (default)
      #   # Converts usages of `nil? || empty?` to `blank?`
      #
      #   # bad
      #   foo.nil? || foo.empty?
      #   foo == nil || foo.empty?
      #
      #   # good
      #   foo.blank?
      #
      # @example NotPresent: true (default)
      #   # Converts usages of `!present?` to `blank?`
      #
      #   # bad
      #   !foo.present?
      #
      #   # good
      #   foo.blank?
      #
      # @example UnlessPresent: true (default)
      #   # Converts usages of `unless present?` to `if blank?`
      #
      #   # bad
      #   something unless foo.present?
      #
      #   # good
      #   something if foo.blank?
      #
      #   # bad
      #   unless foo.present?
      #     something
      #   end
      #
      #   # good
      #   if foo.blank?
      #     something
      #   end
      #
      #   # good
      #   def blank?
      #     !present?
      #   end
      class Blank < Base
        extend AutoCorrector

        MSG_NIL_OR_EMPTY = 'Use `%<prefer>s` instead of `%<current>s`.'
        MSG_NOT_PRESENT = 'Use `%<prefer>s` instead of `%<current>s`.'
        MSG_UNLESS_PRESENT = 'Use `if %<prefer>s` instead of `%<current>s`.'
        RESTRICT_ON_SEND = %i[!].freeze

        # `(send nil $_)` is not actually a valid match for an offense. Nodes
        # that have a single method call on the left hand side
        # (`bar || foo.empty?`) will blow up when checking
        # `(send (:nil) :== $_)`.
        def_node_matcher :nil_or_empty?, <<~PATTERN
          (or
              {
                (send $_ :!)
                (send $_ :nil?)
                (send $_ :== nil)
                (send nil :== $_)
              }
              {
                (send $_ :empty?)
                (send (send (send $_ :empty?) :!) :!)
              }
          )
        PATTERN

        def_node_matcher :not_present?, '(send (send $_ :present?) :!)'

        def_node_matcher :defining_blank?, '(def :blank? (args) ...)'

        def_node_matcher :unless_present?, <<~PATTERN
          (:if $(send $_ :present?) {nil? (...)} ...)
        PATTERN

        def on_send(node)
          return unless cop_config['NotPresent']

          not_present?(node) do |receiver|
            # accepts !present? if its in the body of a `blank?` method
            next if defining_blank?(node.parent)

            message = format(MSG_NOT_PRESENT, prefer: replacement(receiver), current: node.source)
            add_offense(node, message: message) do |corrector|
              autocorrect(corrector, node)
            end
          end
        end

        def on_or(node)
          return unless cop_config['NilOrEmpty']

          nil_or_empty?(node) do |var1, var2|
            return unless var1 == var2

            message = format(MSG_NIL_OR_EMPTY, prefer: replacement(var1), current: node.source)
            add_offense(node, message: message) do |corrector|
              autocorrect(corrector, node)
            end
          end
        end

        def on_if(node)
          return unless cop_config['UnlessPresent']
          return unless node.unless?
          return if node.else? && config.cop_enabled?('Style/UnlessElse')

          unless_present?(node) do |method_call, receiver|
            range = unless_condition(node, method_call)

            message = format(MSG_UNLESS_PRESENT, prefer: replacement(receiver), current: range.source)
            add_offense(range, message: message) do |corrector|
              autocorrect(corrector, node)
            end
          end
        end

        private

        def autocorrect(corrector, node)
          method_call, variable1 = unless_present?(node)

          if method_call
            corrector.replace(node.loc.keyword, 'if')
            range = method_call.source_range
          else
            variable1, _variable2 = nil_or_empty?(node) || not_present?(node)
            range = node.source_range
          end

          corrector.replace(range, replacement(variable1))
        end

        def unless_condition(node, method_call)
          if node.modifier_form?
            node.loc.keyword.join(node.source_range.end)
          else
            node.source_range.begin.join(method_call.source_range)
          end
        end

        def replacement(node)
          node.respond_to?(:source) ? "#{node.source}.blank?" : 'blank?'
        end
      end
    end
  end
end

RuboCop Test Excerpts

vendor/rubocop-rails/spec/rubocop/cop/rails/blank_spec.rb

    it 'registers an offense and corrects' do

      expect_offense(<<~RUBY, source: source, message: message)
        #{source}
        ^{source} #{message}
      RUBY

  context 'NilOrEmpty set to true' do

    it 'accepts checking nil?' do

    it 'accepts checking empty?' do

    it 'accepts checking nil? || empty? on different objects' do

    it 'does not break when RHS of `or` is a naked falsiness check' do

    it 'does not break when LHS of `or` is a naked falsiness check' do

    it 'does not break when LHS of `or` is a send node with an argument' do

    context 'nil or empty' do

    context 'checking all variable types' do

  context 'NotPresent set to true' do

    it 'accepts !present? if its in the body of a `blank?` method' do

  context 'UnlessPresent set to true' do

    it 'accepts modifier if present?' do

    it 'accepts modifier unless blank?' do

    it 'accepts normal if present?' do

      expect_no_offenses(<<~RUBY)
        if foo.present?
          something
        end
      RUBY

    it 'accepts normal unless blank?' do

      expect_no_offenses(<<~RUBY)
        unless foo.blank?
          something
        end
      RUBY

Current Fixture: offense.rb

tests/fixtures/cops/rails/blank/offense.rb

!x.present?
^^^^^^^^^^^ Rails/Blank: Use `blank?` instead of `!present?`.

!name.present?
^^^^^^^^^^^^^^ Rails/Blank: Use `blank?` instead of `!present?`.

!user.email.present?
^^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `blank?` instead of `!present?`.

x.nil? || x.empty?
^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `x.blank?` instead of `x.nil? || x.empty?`.

name.nil? || name.empty?
^^^^^^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `name.blank?` instead of `name.nil? || name.empty?`.

foo == nil || foo.empty?
^^^^^^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `foo.blank?` instead of `foo == nil || foo.empty?`.

something unless foo.present?
          ^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `if foo.blank?` instead of `unless foo.present?`.

something unless present?
          ^^^^^^^^^^^^^^^ Rails/Blank: Use `if blank?` instead of `unless present?`.

unless foo.present?
^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `if foo.blank?` instead of `unless foo.present?`.
  something
end

!foo || foo.empty?
^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `foo.blank?` instead of `!foo || foo.empty?`.

!methods || methods.empty?
^^^^^^^^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `methods.blank?` instead of `!methods || methods.empty?`.

!url || url.empty?
^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `url.blank?` instead of `!url || url.empty?`.

return self if nil? || empty?
               ^^^^^^^^^^^^^^ Rails/Blank: Use `blank?` instead of `nil? || empty?`.
return [] if nil? || empty?
             ^^^^^^^^^^^^^^ Rails/Blank: Use `blank?` instead of `nil? || empty?`.

Current Fixture: no_offense.rb

tests/fixtures/cops/rails/blank/no_offense.rb

x.blank?
x.present?
!x.empty?
x.nil?
name.present? && name.length > 0
x.nil? || y.empty?
x.nil? && x.empty?
x.nil? || x.zero?
something if foo.present?
something unless foo.blank?
def blank?
  !present?
end
unless foo.present?
  something
else
  something_else
end

# present? called with argument (class method style) should NOT be flagged
# RuboCop's NodePattern `(send (send $_ :present?) :!)` requires present? with no arguments
!Helpers.present?(value)
!Vagrant::Util::Presence.present?(directory)
unless Helpers.present?(value)
  do_something
end

# safe navigation on present?/empty? — RuboCop's NodePattern matches send not csend
# so &.present? and &.empty? should NOT be flagged
return [] unless response&.strip&.present?
unless object&.present?
  do_something
end
foo.nil? || foo&.empty?

# pattern match guard: `in pattern unless condition` is not a regular unless
# RuboCop's on_if handler does not visit pattern match guards
case element.name
in "div" unless element.at("div").present?
  element.name = "p"
end

# safe navigation with !present? — semantics differ:
# !pkey_cols&.present? when pkey_cols is nil → !nil → true
# pkey_cols&.blank? when pkey_cols is nil → nil (falsy)
# RuboCop skips this pattern because `(send (send $_ :present?) :!)` doesn't
# match csend (safe navigation).
id_option = if pk_is_also_fk || !pkey_cols&.present?

@6 6 bot force-pushed the fix/rails-blank-23517202617 branch from d1c464f to c696956 Compare March 24, 2026 23:48
@6 6 bot marked this pull request as ready for review March 24, 2026 23:48
@6 6 bot enabled auto-merge (squash) March 24, 2026 23:48
@6
Copy link
Copy Markdown
Contributor Author

6 bot commented Mar 25, 2026

Auto-repair Started

  • Checks run: #23517853669
  • Route: hard
  • Backend: codex / hard
  • Model: gpt-5.4 (xhigh)
  • Reason: cop-check: Check cops against corpus baseline
  • Repair workflow: #23518456612
Task prompt (4636 tokens)

PR Repair Task: PR #186

Before making changes, read docs/agent-ci.md.

Repair the existing PR branch in place. Do not create a new branch or PR.
Preserve the intent of the current PR and make the smallest changes needed to fix the failing checks.
Do not repair this PR by reverting it back to origin/main, deleting the whole diff, or otherwise turning it into an empty/no-op PR.
If the only plausible fix is a full revert of the PR, stop and explain that clearly instead of performing the revert.
Do not edit unrelated files or do broad cleanup.

Context

  • PR: [bot] Fix Rails/Blank #186 [bot] Fix Rails/Blank
  • Branch: fix/rails-blank-23517202617
  • Checks run: #1030 (Checks)
  • Route: hard
  • Selected backend: codex / hard
  • Failure summary: cop-check: Check cops against corpus baseline

Required Outcome

Fix the currently failing checks shown below.
Before finishing, run the targeted validations needed to make the workflow's final verification likely to pass.

Required Local Verification

The workflow will rerun these commands before pushing. Your changes should make them pass:

python3 scripts/dispatch_cops.py changed --base origin/main --head HEAD > "$REPAIR_CHANGED_COPS_FILE"
failed=0
while IFS= read -r cop; do
  [ -z "$cop" ] && continue
  echo "=============================="
  echo "Checking: $cop (re-running against corpus)"
  echo "=============================="
  if ! python3 scripts/check_cop.py "$cop" --verbose --rerun --quick --clone; then
    echo "FAIL: $cop regression detected"
    failed=$((failed + 1))
  fi
done < "$REPAIR_CHANGED_COPS_FILE"
test "$failed" -eq 0

Current PR Diff Stat

src/cop/rails/blank.rs                     | 25 +++++++++++++++++--------
 tests/fixtures/cops/rails/blank/offense.rb | 18 ++++++++++++++++++
 2 files changed, 35 insertions(+), 8 deletions(-)

Current PR Diff Excerpt

diff --git a/src/cop/rails/blank.rs b/src/cop/rails/blank.rs
index b4f07dcd5..2b641eade 100644
--- a/src/cop/rails/blank.rs
+++ b/src/cop/rails/blank.rs
@@ -90,6 +90,18 @@ use ruby_prism::Visit;
 /// but `pkey_cols&.blank?` would return `nil` (falsy). So the replacement changes semantics.
 ///
 /// Fix: Added `call_operator_loc() == &.` check in `check_not_present` to skip safe navigation.
+///
+/// ## Investigation (2026-03-24)
+///
+/// **FN root cause (14 FN):** The `or` keyword in `foo.nil? or foo.empty?` was detected correctly
+/// (Prism parses both `||` and `or` as `OrNode`), but the diagnostic message hardcoded `||` as the
+/// separator between left and right operands. RuboCop uses `node.source` which preserves the actual
+/// operator (`or` or `||`). The message mismatch caused corpus FN counts because the expected
+/// message used `or` but nitrocop produced `||`.
+///
+/// Fix: Changed `check_nil_or_empty` to use `or_node.location().as_slice()` (the full OrNode
+/// source text) in the message instead of concatenating `left || right`. This preserves the actual
+/// operator in the diagnostic message, matching RuboCop's behavior.
 pub struct Blank;
 
 /// Extract the receiver source text from a CallNode, returning None if absent.
@@ -198,7 +210,7 @@ impl<'pr> BlankVisitor<'_, '_> {
         let left = or_node.left();
         let right = or_node.right();
 
-        if let Some((nil_recv, left_src)) = nil_check_receiver(&left) {
+        if let Some((nil_recv, _left_src)) = nil_check_receiver(&left) {
             // Right side must be `<same>.empty?` — NOT safe navigation (`&.empty?`)
             // RuboCop's NodePattern `(send $_ :empty?)` only matches send, not csend.
             if let Some(right_call) = right.as_call_node() {
@@ -211,18 +223,15 @@ impl<'pr> BlankVisitor<'_, '_> {
                     if nil_recv == empty_recv {
                         let loc = or_node.location();
                         let (line, column) = self.source.offset_to_line_col(loc.start_offset());
-                        let left_str = std::str::from_utf8(left_src).unwrap_or("nil?");
-                        let right_str =
-                            std::str::from_utf8(right.location().as_slice()).unwrap_or("empty?");
+                        let or_src =
+                            std::str::from_utf8(or_node.location().as_slice()).unwrap_or("...");
                         let message = match nil_recv {
                             Some(recv_bytes) => {
                                 let recv_str = std::str::from_utf8(recv_bytes).unwrap_or("object");
-                                format!(
-                                    "Use `{recv_str}.blank?` instead of `{left_str} || {right_str}`."
-                                )
+                                format!("Use `{recv_str}.blank?` instead of `{or_src}`.")
                             }
                             None => {
-                                format!("Use `blank?` instead of `{left_str} || {right_str}`.")
+                                format!("Use `blank?` instead of `{or_src}`.")
                             }
                         };
                         self.diagnostics.push(self.cop.diagnostic(
diff --git a/tests/fixtures/cops/rails/blank/offense.rb b/tests/fixtures/cops/rails/blank/offense.rb
index bf8858b14..04bd0e9f5 100644
--- a/tests/fixtures/cops/rails/blank/offense.rb
+++ b/tests/fixtures/cops/rails/blank/offense.rb
@@ -40,3 +40,21 @@
                ^^^^^^^^^^^^^^ Rails/Blank: Use `blank?` instead of `nil? || empty?`.
 return [] if nil? || empty?
              ^^^^^^^^^^^^^^ Rails/Blank: Use `blank?` instead of `nil? || empty?`.
+
+elements.nil? or elements.empty?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `elements.blank?` instead of `elements.nil? or elements.empty?`.
+
+return if name.nil? or name.empty?
+          ^^^^^^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `name.blank?` instead of `name.nil? or name.empty?`.
+
+words.shift if words[0].nil? or words[0].empty?
+               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `words[0].blank?` instead of `words[0].nil? or words[0].empty?`.
+
+foo unless bar.nil? || bar.empty?
+           ^^^^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `bar.blank?` instead of `bar.nil? || bar.empty?`.
+
+break if line.nil? or line.empty?
+         ^^^^^^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `line.blank?` instead of `line.nil? or line.empty?`.
+
+break if rnext.nil? or rnext.empty? or rline.nil? or rline.empty?
+         ^^^^^^^^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `rnext.blank?` instead of `rnext.nil? or rnext.empty?`.

Local Corpus Context

These corpus oracle artifacts are already downloaded locally by the workflow.
Prefer these local files over re-downloading the same corpus data from GitHub Actions.
If you still need GitHub metadata for debugging, a read-only token is available in GH_TOKEN.

  • Corpus JSON (matches the PR cop-check gate): /home/runner/work/_temp/agent-pr-repair/repair/corpus.json (corpus oracle run #23508978948)

Use these files directly with the repo scripts when you need corpus context.

python3 scripts/investigate_cop.py Department/CopName --input /home/runner/work/_temp/agent-pr-repair/repair/corpus.json --context
python3 scripts/check_cop.py Department/CopName --input /home/runner/work/_temp/agent-pr-repair/repair/corpus.json --verbose --rerun --quick --clone

Failed Checks Packet

cop-check

  • Route: hard
  • Failed steps: Check cops against corpus baseline
... (truncated, showing last 80 lines) ...
Cached synthetic-results.json from artifact
Downloaded corpus-report from run 23508978948 via gh
Cloning 1181 repos for Rails/Blank...
Cloned 1181/1181 repos
Running nitrocop per-repo...
--quick: running 1181/1181 repos (skipping 0 with zero baseline activity)
[50/1181] FontCustom__fontcustom__86d7f8d...
[100/1181] Shopify__graphql-batch__bcf1b9c...
[150/1181] afair__email_address__7312aeb...
[200/1181] avdi__quarto__5ebc3a0...
[250/1181] bskim45__fastlane-plugin-google_drive__b1ed775...
[300/1181] codemancers__invoker__b42edee...
[350/1181] deseretbook__classy_hash__1528b82...
[400/1181] emonti__rbkb__4645025...
[450/1181] fog__fog__f3e5cea...
[500/1181] hackedteam__rcs-db__6cff59d...
[550/1181] infinitered__bluepotion__293730b...
[600/1181] joncardasis__cocoapods-user-defined-build-types__e34a802...
[650/1181] leavez__cocoapods-binary__b22bd94...
[700/1181] metanorma__metanorma__c6f953a...
[750/1181] net-ssh__net-scp__8b3a4e9...
[800/1181] pat__thinking-sphinx__3ebecea...
[850/1181] publiclab__spectral-workbench__1f47fbe...
[900/1181] red-data-tools__enumerable-statistics__3042fe1...
[950/1181] ruby__debug__bad4d38...
[1000/1181] shakacode__shakapacker__fc0f9a2...
[1050/1181] stimulusreflex__futurism__17ff674...
[1100/1181] travis-ci__travis-api__d1cbb6d...
[1150/1181] weg-li__weg-li__14dfca6...
Debug: per-repo counts written to /home/runner/work/nitrocop/nitrocop/check-cop-debug.json
Per-repo discrepancy analysis:
check-cop total: 6494, oracle expected: 6486, diff: +8
Checking Rails/Blank against corpus
Gate: count-only cop-level regression check
Not a location-level conformance proof or a department completion gate
Baseline (from CI): 6,472 matches, 0 FP, 14 FN
Expected RuboCop offenses: 6,486
Repos with offenses (1181):
180  HubSpot__hubspot-api-ruby__75a9afa
164  inspec__inspec-aws__e226dc9
145  antiwork__gumroad__782a69f
121  workarea-commerce__workarea__2d01fcf
115  yohasebe__monadic-chat__79cf0fb
99  onetimesecret__onetimesecret__eb85a76
94  gisiahq__gisia__b7f70e0
91  archivesspace__archivesspace__cd3385c
87  owen2345__camaleon-cms__acc1dfa
85  danbooru__danbooru__fd45f0f
78  skyborn-industries__ffxiv-collect__b25e5ac
75  scarpe-team__scarpe__b3939e6
72  theforeman__foreman__7a2fc6c
70  hackclub__hcb__651ab54
64  decko-commons__decko__b3eb1fb
63  huginn__huginn__9328c1e
61  catarse__catarse__0d73a61
58  liaoziyang__stackneveroverflow__8f4dce2
57  inspec__inspec__965502e
52  avalonmediasystem__avalon__c6af9b3
49  OpenVoxProject__openvox__2665666
48  clacky-ai__openclacky__02e1cc9
46  noosfero__noosfero__299a0b9
46  sisimai__rb-sisimai__759ee37
43  parruda__swarm__5179529
43  puppetlabs__puppet__e227c27
42  discourse__discourse__9c8f125
41  librariesio__libraries.io__1e2d6bf
39  chef__chef__e58e1a1
38  activemerchant__active_merchant__ca45b1b
... and 1151 more
Results:
Expected (RuboCop):        6,486
Actual (nitrocop):         6,494
CI nitrocop baseline:      6,472
Excess (potential FP):         8
Missing (potential FN):        0
Gate type: count-only / cop-level regression
FAIL: FP increased from 0 to 18 (+18, threshold: 0)
FAIL: Rails/Blank regression detected
FAILED: 1 cop(s) regressed vs corpus baseline.
##[error]Process completed with exit code 1.

Constraints

  • Keep the fix scoped to the failing checks.
  • Reuse the existing PR branch and existing tests where possible.
  • Prefer the minimal patch that makes the deterministic verification pass.
  • A full revert to origin/main or an empty PR is treated as a failed repair, not a success.
  • If a fix is blocked by missing context, explain that clearly in the final message.

Available Local Helper Scripts

These helper scripts are available in this CI checkout. Prefer the stable top-level CLI paths shown below over ad hoc commands when they directly help with diagnosis or validation.

  • scripts/check_cop.py — aggregate corpus regression check for one cop
  • scripts/dispatch_cops.py — dispatch helpers for changed cops, task generation, ranking, and prior attempts
  • scripts/investigate_cop.py — inspect FP/FN examples from corpus oracle data
  • scripts/verify_cop_locations.py — verify exact known oracle FP/FN locations
  • scripts/corpus_smoke_test.py — smoke-test a few pinned repos

Typical usage when present:

python3 scripts/check_cop.py Department/CopName --verbose --rerun --quick --clone
python3 scripts/dispatch_cops.py changed --base origin/main --head HEAD
python3 scripts/investigate_cop.py Department/CopName --context
python3 scripts/verify_cop_locations.py Department/CopName
python3 scripts/corpus_smoke_test.py --binary target/release/nitrocop

Local Cop-Check Diagnosis

The workflow already reran the changed-cop corpus check locally before agent execution.
Use this packet as the starting point instead of rediscovering the same corpus regression.

Changed cops:

  • Rails/Blank

Rails/Blank

Start here:

  • Re-run after edits: python3 scripts/check_cop.py Rails/Blank --verbose --rerun --quick --clone
  • Baseline corpus context: python3 scripts/investigate_cop.py Rails/Blank --input /home/runner/work/_temp/agent-pr-repair/repair/corpus.json --repos-only
  • Inspect repo: /home/runner/work/nitrocop/nitrocop/vendor/corpus/HubSpot__hubspot-api-ruby__75a9afa
  • Inspect repo: /home/runner/work/nitrocop/nitrocop/vendor/corpus/inspec__inspec-aws__e226dc9
  • Inspect repo: /home/runner/work/nitrocop/nitrocop/vendor/corpus/antiwork__gumroad__782a69f
  • Inspect repo: /home/runner/work/nitrocop/nitrocop/vendor/corpus/workarea-commerce__workarea__2d01fcf
  • Inspect repo: /home/runner/work/nitrocop/nitrocop/vendor/corpus/yohasebe__monadic-chat__79cf0fb

Oracle context from CI corpus artifact:

  • Repos and exact examples: python3 scripts/investigate_cop.py Rails/Blank --input /home/runner/work/_temp/agent-pr-repair/repair/corpus.json --fn-only --context --limit 10
    Oracle FN hotspots:
  • databasically__lowdown__d593927 (5 FN)
  • cjstewart88__Tubalr__f6956c8 (4 FN)
  • pitluga__supply_drop__d64c50c (4 FN)

Representative oracle FN examples:

  • databasically__lowdown__d593927: vendor/rails/actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb:594 — Use words[0].blank? instead of words[0].nil? or words[0].empty?.
      587:   private
      588:     def __do_split_word(word, size) #:nodoc:
      589:       [word[0 .. (size - 1)], word[size .. -1]]
      590:     end
      591: 
      592:     def __format(to_wrap) #:nodoc:
      593:       words = to_wrap.split(/\s+/).compact
>>>   594:       words.shift if words[0].nil? or words[0].empty?
  • databasically__lowdown__d593927: vendor/rails/actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb:602 — Use line.blank? instead of line.nil? || line.empty?.
      595:       to_wrap = []
      596: 
      597:       abbrev = false
      598:       width = @columns - @first_indent - @left_margin - @right_margin
      599:       indent_str = ' ' * @first_indent
      600:       first_line = true
      601:       line = words.shift
>>>   602:       abbrev = __is_abbrev(line) unless line.nil? || line.empty?
  • databasically__lowdown__d593927: vendor/rails/actionmailer/lib/action_mailer/vendor/text-format-0.6.3/text/format.rb:630 — Use line.blank? instead of line.nil? or line.empty?.
      623:           line = w
      624:         end
      625: 
      626:         abbrev = __is_abbrev(w) unless w.nil?
      627:       end
      628: 
      629:       loop do
>>>   630:         break if line.nil? or line.empty?
/usr/bin/python3 scripts/check_cop.py Rails/Blank --verbose --rerun --quick --clone

Exit status: 1

Checking Rails/Blank against corpus
Gate: count-only cop-level regression check
Not a location-level conformance proof or a department completion gate
Baseline (from CI): 6,472 matches, 0 FP, 14 FN
Expected RuboCop offenses: 6,486

Repos with offenses (1181):
     180  HubSpot__hubspot-api-ruby__75a9afa
     164  inspec__inspec-aws__e226dc9
     145  antiwork__gumroad__782a69f
     121  workarea-commerce__workarea__2d01fcf
     115  yohasebe__monadic-chat__79cf0fb
      99  onetimesecret__onetimesecret__eb85a76
      94  gisiahq__gisia__b7f70e0
      91  archivesspace__archivesspace__cd3385c
      87  owen2345__camaleon-cms__acc1dfa
      85  danbooru__danbooru__fd45f0f
      78  skyborn-industries__ffxiv-collect__b25e5ac
      75  scarpe-team__scarpe__b3939e6
      72  theforeman__foreman__7a2fc6c
      70  hackclub__hcb__651ab54
      64  decko-commons__decko__b3eb1fb
      63  huginn__huginn__9328c1e
      61  catarse__catarse__0d73a61
      58  liaoziyang__stackneveroverflow__8f4dce2
      57  inspec__inspec__965502e
      52  avalonmediasystem__avalon__c6af9b3
      49  OpenVoxProject__openvox__2665666
      48  clacky-ai__openclacky__02e1cc9
      46  noosfero__noosfero__299a0b9
      46  sisimai__rb-sisimai__759ee37
      43  parruda__swarm__5179529
      43  puppetlabs__puppet__e227c27
      42  discourse__discourse__9c8f125
      41  librariesio__libraries.io__1e2d6bf
      39  chef__chef__e58e1a1
      38  activemerchant__active_merchant__ca45b1b
  ... and 1151 more

Results:
  Expected (RuboCop):        6,486
  Actual (nitrocop):         6,494
  CI nitrocop baseline:      6,472
  Excess (potential FP):         8
  Missing (potential FN):        0

  Gate type: count-only / cop-level regression

FAIL: FP increased from 0 to 18 (+18, threshold: 0)
gh CLI found but not authenticated, trying other methods...
Using cached corpus-results from run 23508978948
  Cloning 1181 repos for Rails/Blank...
  Cloned 1181/1181 repos
Running nitrocop per-repo...
  --quick: running 1181/1181 repos (skipping 0 with zero baseline activity)
  [50/1181] EugenMayer__docker-sync__4eab6de...
  [100/1181] Shopify__graphql-batch__bcf1b9c...
  [150/1181] afair__email_address__7312aeb...
  [200/1181] avdi__quarto__5ebc3a0...
  [250/1181] bskim45__fastlane-plugin-google_drive__b1ed775...
  [300/1181] codemancers__invoker__b42edee...
  [350/1181] derails__derails__7f68b11...
  [400/1181] emonti__rbkb__4645025...
  [450/1181] follmann__favicon_maker__264e483...
  [500/1181] hackedteam__rcs-db__6cff59d...
  [550/1181] infinitered__bluepotion__293730b...
  [600/1181] joncardasis__cocoapods-user-defined-build-types__e34a802...
  [650/1181] leavez__cocoapods-binary__b22bd94...
  [700/1181] metanorma__metanorma__c6f953a...
  [750/1181] net-ssh__net-scp__8b3a4e9...
  [800/1181] pat__thinking-sphinx__3ebecea...
  [850/1181] publiclab__spectral-workbench__1f47fbe...
  [900/1181] red-data-tools__enumerable-statistics__3042fe1...
  [950/1181] ruby__debug__bad4d38...
  [1000/1181] shakacode__shakapacker__fc0f9a2...
  [1050/1181] stimulusreflex__futurism__17ff674...
  [1100/1181] travis-ci__travis-api__d1cbb6d...
  [1150/1181] weg-li__weg-li__14dfca6...
Debug: per-repo counts written to /home/runner/work/nitrocop/nitrocop/check-cop-debug.json
Per-repo discrepancy analysis:
  check-cop total: 6494, oracle expected: 6486, diff: +8

@6
Copy link
Copy Markdown
Contributor Author

6 bot commented Mar 25, 2026

Auto-repair Started

  • Checks run: #23526441626
  • Route: hard
  • Backend: codex / hard
  • Model: gpt-5.4 (xhigh)
  • Reason: cop-check (5): Check cops against corpus baseline (shard 5/4); cop-check-gate: Run if [ true != true ]; then
  • Repair workflow: #23526845391
Task prompt (2960 tokens)

PR Repair Task: PR #186

Before making changes, read docs/agent-ci.md.

Repair the existing PR branch in place. Do not create a new branch or PR.
Preserve the intent of the current PR and make the smallest changes needed to fix the failing checks.
Do not repair this PR by reverting it back to origin/main, deleting the whole diff, or otherwise turning it into an empty/no-op PR.
If the only plausible fix is a full revert of the PR, stop and explain that clearly instead of performing the revert.
Do not edit unrelated files or do broad cleanup.

Context

  • PR: [bot] Fix Rails/Blank #186 [bot] Fix Rails/Blank
  • Branch: fix/rails-blank-23517202617
  • Checks run: #1090 (Checks)
  • Route: hard
  • Selected backend: codex / hard
  • Failure summary: cop-check (5): Check cops against corpus baseline (shard 5/4); cop-check-gate: Run if [ "true" != "true" ]; then

Required Outcome

Fix the currently failing checks shown below.
Before finishing, run the targeted validations needed to make the workflow's final verification likely to pass.

Required Local Verification

The workflow will rerun these commands before pushing. Your changes should make them pass:

Current PR Diff Stat

src/cop/rails/blank.rs                     | 25 +++++++++++++++++--------
 tests/fixtures/cops/rails/blank/offense.rb | 18 ++++++++++++++++++
 2 files changed, 35 insertions(+), 8 deletions(-)

Current PR Diff Excerpt

diff --git a/src/cop/rails/blank.rs b/src/cop/rails/blank.rs
index b4f07dcd5..2b641eade 100644
--- a/src/cop/rails/blank.rs
+++ b/src/cop/rails/blank.rs
@@ -90,6 +90,18 @@ use ruby_prism::Visit;
 /// but `pkey_cols&.blank?` would return `nil` (falsy). So the replacement changes semantics.
 ///
 /// Fix: Added `call_operator_loc() == &.` check in `check_not_present` to skip safe navigation.
+///
+/// ## Investigation (2026-03-24)
+///
+/// **FN root cause (14 FN):** The `or` keyword in `foo.nil? or foo.empty?` was detected correctly
+/// (Prism parses both `||` and `or` as `OrNode`), but the diagnostic message hardcoded `||` as the
+/// separator between left and right operands. RuboCop uses `node.source` which preserves the actual
+/// operator (`or` or `||`). The message mismatch caused corpus FN counts because the expected
+/// message used `or` but nitrocop produced `||`.
+///
+/// Fix: Changed `check_nil_or_empty` to use `or_node.location().as_slice()` (the full OrNode
+/// source text) in the message instead of concatenating `left || right`. This preserves the actual
+/// operator in the diagnostic message, matching RuboCop's behavior.
 pub struct Blank;
 
 /// Extract the receiver source text from a CallNode, returning None if absent.
@@ -198,7 +210,7 @@ impl<'pr> BlankVisitor<'_, '_> {
         let left = or_node.left();
         let right = or_node.right();
 
-        if let Some((nil_recv, left_src)) = nil_check_receiver(&left) {
+        if let Some((nil_recv, _left_src)) = nil_check_receiver(&left) {
             // Right side must be `<same>.empty?` — NOT safe navigation (`&.empty?`)
             // RuboCop's NodePattern `(send $_ :empty?)` only matches send, not csend.
             if let Some(right_call) = right.as_call_node() {
@@ -211,18 +223,15 @@ impl<'pr> BlankVisitor<'_, '_> {
                     if nil_recv == empty_recv {
                         let loc = or_node.location();
                         let (line, column) = self.source.offset_to_line_col(loc.start_offset());
-                        let left_str = std::str::from_utf8(left_src).unwrap_or("nil?");
-                        let right_str =
-                            std::str::from_utf8(right.location().as_slice()).unwrap_or("empty?");
+                        let or_src =
+                            std::str::from_utf8(or_node.location().as_slice()).unwrap_or("...");
                         let message = match nil_recv {
                             Some(recv_bytes) => {
                                 let recv_str = std::str::from_utf8(recv_bytes).unwrap_or("object");
-                                format!(
-                                    "Use `{recv_str}.blank?` instead of `{left_str} || {right_str}`."
-                                )
+                                format!("Use `{recv_str}.blank?` instead of `{or_src}`.")
                             }
                             None => {
-                                format!("Use `blank?` instead of `{left_str} || {right_str}`.")
+                                format!("Use `blank?` instead of `{or_src}`.")
                             }
                         };
                         self.diagnostics.push(self.cop.diagnostic(
diff --git a/tests/fixtures/cops/rails/blank/offense.rb b/tests/fixtures/cops/rails/blank/offense.rb
index bf8858b14..04bd0e9f5 100644
--- a/tests/fixtures/cops/rails/blank/offense.rb
+++ b/tests/fixtures/cops/rails/blank/offense.rb
@@ -40,3 +40,21 @@
                ^^^^^^^^^^^^^^ Rails/Blank: Use `blank?` instead of `nil? || empty?`.
 return [] if nil? || empty?
              ^^^^^^^^^^^^^^ Rails/Blank: Use `blank?` instead of `nil? || empty?`.
+
+elements.nil? or elements.empty?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `elements.blank?` instead of `elements.nil? or elements.empty?`.
+
+return if name.nil? or name.empty?
+          ^^^^^^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `name.blank?` instead of `name.nil? or name.empty?`.
+
+words.shift if words[0].nil? or words[0].empty?
+               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `words[0].blank?` instead of `words[0].nil? or words[0].empty?`.
+
+foo unless bar.nil? || bar.empty?
+           ^^^^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `bar.blank?` instead of `bar.nil? || bar.empty?`.
+
+break if line.nil? or line.empty?
+         ^^^^^^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `line.blank?` instead of `line.nil? or line.empty?`.
+
+break if rnext.nil? or rnext.empty? or rline.nil? or rline.empty?
+         ^^^^^^^^^^^^^^^^^^^^^^^^^^ Rails/Blank: Use `rnext.blank?` instead of `rnext.nil? or rnext.empty?`.

Local Corpus Context

These corpus oracle artifacts are already downloaded locally by the workflow.
Prefer these local files over re-downloading the same corpus data from GitHub Actions.
If you still need GitHub metadata for debugging, a read-only token is available in GH_TOKEN.

  • Corpus JSON (matches the PR cop-check gate): /home/runner/work/_temp/agent-pr-repair/repair/corpus.json (corpus oracle run #23519942949)

Use these files directly with the repo scripts when you need corpus context.

python3 scripts/investigate_cop.py Department/CopName --input /home/runner/work/_temp/agent-pr-repair/repair/corpus.json --context
python3 scripts/check_cop.py Department/CopName --input /home/runner/work/_temp/agent-pr-repair/repair/corpus.json --verbose --rerun --quick --clone

Failed Checks Packet

cop-check (5)

  • Route: hard
  • Failed steps: Check cops against corpus baseline (shard 5/4)
... (truncated, showing last 80 lines) ...
GH_TOKEN: ***
##[endgroup]
==============================
Checking: Rails/Blank (shard 5/4)
==============================
Cached synthetic-results.json from artifact
Downloaded corpus-report from run 23519942949 via gh
shard 5/8: 147/1181 repos to clone
Cloning 147 repos for Rails/Blank into /tmp/nitrocop_cop_check_7nd_b6qd...
Cloning 147 repos...
Cloned 147/147 repos
Running nitrocop per-repo...
[50/147] erichmenge__signed_form__2bbfe94...
[100/147] pat__gutentag__e1bd78e...
Debug: per-repo counts written to /home/runner/work/nitrocop/nitrocop/check-cop-debug.json
Per-repo discrepancy analysis:
check-cop total: 803, oracle expected: 6486, diff: -5683
Repos in oracle activity but 0 locally (1034):
0dayInc__pwn__c3d3e6d
24pullrequests__24pullrequests__381028d
AaronC81__parlour__ffc0e09
AaronLasseigne__active_interaction__97667c8
AgileVentures__WebsiteOne__15f7edb
Albacore__albacore__e316e20
AndyObtiva__glimmer-dsl-swt__449cf07
AndyObtiva__glimmer-dsl-web__fd77e68
Arachni__arachni__c1710d6
Arie__serveme__7f28d1b
Checking Rails/Blank against corpus
Gate: count-only cop-level regression check
Not a location-level conformance proof or a department completion gate
Baseline (from CI): 6,472 matches, 0 FP, 14 FN
Expected RuboCop offenses: 6,486
Repos with offenses (147):
121  workarea-commerce__workarea__2d01fcf
57  inspec__inspec__965502e
55  liaoziyang__stackneveroverflow__8f4dce2
46  noosfero__noosfero__299a0b9
32  opf__openproject__f5a451d
27  solutious__rudy__52627b6
26  Shopify__krane__278ce51
25  castwide__solargraph__676da4c
24  DataDog__dd-trace-rb__e3406de
15  edavis10__redmine__2d6f552
15  feedbin__feedbin__6396029
14  sudara__alonetone__ebf728b
13  redmine__redmine__a1a0959
12  Freika__dawarich__3d1ae95
12  html2rss__html2rss__7672db3
12  rubycocoa__rubycocoa__773ca0c
10  NoBrainerORM__nobrainer__be4dd19
10  railsadminteam__rails_admin__d8e0809
9  hashicorp__vagrant__92e8886
9  ttscoff__curlyq__899d60c
8  tobymao__18xx__8f5e103
7  activeagents__activeagent__63c19fd
6  gettalong__hexapdf__f1176c6
6  github__octocatalog-diff__59d46a6
6  rubocop__rubocop__b210a6e
6  testcontainers__testcontainers-ruby__602f58b
6  vasilakisfil__SimpleAMS__4af4a4d
5  Pistos__diakonos__e284f5d
5  autotelik__datashift__54dec8f
5  aws__aws-xray-sdk-ruby__b32f441
... and 117 more
Results:
Expected (RuboCop):        6,486
Actual (nitrocop):           803
CI nitrocop baseline:      6,472
Excess (potential FP):         0
Missing (potential FN):    5,683
Gate type: count-only / cop-level regression
Gate: per-repo FP + FN
New FP (local > oracle):      0
New FN (local < oracle):      2
FAIL: FN regression detected (+2)
+   2  liaoziyang__stackneveroverflow__8f4dce2  (local=55, oracle=57)
FAIL: Rails/Blank regression detected in shard 5
FAILED: 1 cop(s) regressed vs corpus baseline.
##[error]Process completed with exit code 1.

cop-check-gate

  • Route: hard
  • Failed steps: Run if [ "true" != "true" ]; then
cop-check-gate	Run if [ "true" != "true" ]; then	2026-03-25T05:34:49.6226967Z ##[group]Run if [ "true" != "true" ]; then
if [ "true" != "true" ]; then
echo "No cops changed — gate passes"
exit 0
fi
if [ "failure" = "success" ]; then
echo "All shards passed"
exit 0
fi
echo "cop-check failed: failure"
exit 1
shell: /usr/bin/bash -e {0}
##[endgroup]
cop-check failed: failure
##[error]Process completed with exit code 1.

Constraints

  • Keep the fix scoped to the failing checks.
  • Reuse the existing PR branch and existing tests where possible.
  • Prefer the minimal patch that makes the deterministic verification pass.
  • A full revert to origin/main or an empty PR is treated as a failed repair, not a success.
  • If a fix is blocked by missing context, explain that clearly in the final message.

Available Local Helper Scripts

These helper scripts are available in this CI checkout. Prefer the stable top-level CLI paths shown below over ad hoc commands when they directly help with diagnosis or validation.

  • scripts/check_cop.py — aggregate corpus regression check for one cop
  • scripts/dispatch_cops.py — dispatch helpers for changed cops, task generation, ranking, and prior attempts
  • scripts/investigate_cop.py — inspect FP/FN examples from corpus oracle data
  • scripts/verify_cop_locations.py — verify exact known oracle FP/FN locations
  • scripts/corpus_smoke_test.py — smoke-test a few pinned repos

Typical usage when present:

python3 scripts/check_cop.py Department/CopName --verbose --rerun --quick --clone
python3 scripts/dispatch_cops.py changed --base origin/main --head HEAD
python3 scripts/investigate_cop.py Department/CopName --context
python3 scripts/verify_cop_locations.py Department/CopName
python3 scripts/corpus_smoke_test.py --binary target/release/nitrocop

@6
Copy link
Copy Markdown
Contributor Author

6 bot commented Mar 25, 2026

Auto-repair Succeeded

  • Checks run: #23526441626
  • Backend: codex / hard
  • Model: gpt-5.4 (xhigh)
  • Repair workflow: #23526845391
  • Repair commit: dddbda9558b0a4039bda9af562cfda9dce8c0bdf

Auto-repair succeeded with backend codex / hard.

Validated locally before push using /home/runner/work/_temp/agent-pr-repair/repair/verify.sh.

6 bot added a commit that referenced this pull request Mar 25, 2026
@6 6 bot force-pushed the fix/rails-blank-23517202617 branch from 61c2e85 to dddbda9 Compare March 25, 2026 06:09
@6 6 closed this Mar 25, 2026
auto-merge was automatically disabled March 25, 2026 13:07

Pull request was closed

@6 6 deleted the fix/rails-blank-23517202617 branch March 25, 2026 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant