Skip to content

[bot] Fix Style/MixinUsage#151

Merged
6 merged 9 commits intomainfrom
fix/style-mixin_usage-23473250532
Mar 24, 2026
Merged

[bot] Fix Style/MixinUsage#151
6 merged 9 commits intomainfrom
fix/style-mixin_usage-23473250532

Conversation

@6
Copy link
Copy Markdown
Contributor

@6 6 bot commented Mar 24, 2026

Automated fix fix for Style/MixinUsage corpus conformance.

Details

Cop Style/MixinUsage
Backend claude-oauth / normal
Model Claude Opus 4.6 (OAuth, medium)
Mode fix
Turns 15
Run #71

Result

Fix complete. The issue was that `BEGIN {}` blocks (Prism's `PreExecutionNode`) weren't being treated as opaque scopes. RuboCop's `in_top_level_scope?` only considers `{kwbegin begin if def}` as transparent wrappers — `BEGIN {}` isn't in that list, so mixin calls inside it shouldn't be flagged. Added `visit_pre_execution_node` to mark it as opaque, fixing all 3 FPs.

Changes

  2 files changed
Agent conversation excerpt

All 3 FPs are include inside BEGIN {} blocks (Prism: PreExecutionNode). This isn't in RuboCop's transparent wrapper list, so it should be an opaque scope. Let me fix this.

ToolSearch

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

Now let me check if Prism has a PreExecutionNode visitor method.

Bash: grep -r "pre_execution" src/ --include="*.rs" -l 2>/dev/null; grep -r "PreExecution" src/ --include="*.rs" | head -20

Read: /home/runner/work/nitrocop/nitrocop/src/cop/style/begin_block.rs

Good. The fix is clear: add visit_pre_execution_node as an opaque scope in the MixinUsage visitor. Let me add the test case first, then fix the code.

Edit: /home/runner/work/nitrocop/nitrocop/tests/fixtures/cops/style/mixin_usage/no_offense.rb

Read: /home/runner/work/nitrocop/nitrocop/tests/fixtures/cops/style/mixin_usage/no_offense.rb

Read: /home/runner/work/nitrocop/nitrocop/src/cop/style/mixin_usage.rs

Now let me add the fixture and fix the code.

Edit: /home/runner/work/nitrocop/nitrocop/tests/fixtures/cops/style/mixin_usage/no_offense.rb

Now verify the test fails before the fix:

Bash: cargo test --lib -- cop::style::mixin_usage 2>&1 | tail -20

Test fails as expected. Now add visit_pre_execution_node as an opaque scope:

Edit: /home/runner/work/nitrocop/nitrocop/src/cop/style/mixin_usage.rs

Now add the doc comment for this round:

Edit: /home/runner/work/nitrocop/nitrocop/src/cop/style/mixin_usage.rs

Bash: cargo test --lib -- cop::style::mixin_usage 2>&1 | tail -10

Both tests pass. Now let me format the changed file and commit.

Bash: cargo fmt -- src/cop/style/mixin_usage.rs

Bash: `git add src/cop/style/mixin_usage.rs tests/fixtures/cops/style/mixin_usage/no_offense.rb && git commit -m "$(cat <<'EOF'
Fix Style/MixinUsage: treat BEGIN {} blocks as opaque scope

BEGIN {} (Prism:`

Fix complete. The issue was that BEGIN {} blocks (Prism's PreExecutionNode) weren't being treated as opaque scopes. RuboCop's in_top_level_scope? only considers {kwbegin begin if def} as transparent wrappers — BEGIN {} isn't in that list, so mixin calls inside it shouldn't be flagged. Added visit_pre_execution_node to mark it as opaque, fixing all 3 FPs.

Task prompt (5947 tokens)

Fix Style/MixinUsage — 3 FP, 0 FN

Cross-Corpus Risk

  • Standard corpus: 540 matches, 0 FP, 0 FN
  • Extended corpus: 2,834 matches, 3 FP, 0 FN
  • Extended divergence currently touches 1 repo

Risk class: extended-only edge case against a standard-perfect baseline.

Treat the extended examples as a narrow edge case, not proof that the broad pattern is safe.
Any carve-out that reduces the extended FP/FN count but regresses the standard corpus is a bad fix.
Prefer the smallest context-specific change you can justify from RuboCop behavior and the vendor spec.
This is concentrated in only a few repos, which increases the risk of overfitting to one local shape.

Instructions

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

Current state: 2,834 matches, 3 false positives, 0 false negatives.
Focus on: FP (nitrocop flags code RuboCop does not).

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 Style/MixinUsage /tmp/test.rb
    echo '<general pattern>' > /tmp/test.rb && rubocop --only Style/MixinUsage /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/style/mixin_usage/offense.rb with ^ annotation
    • FP fix: add the false-positive pattern to tests/fixtures/cops/style/mixin_usage/no_offense.rb
  4. Verify test fails: cargo test --lib -- cop::style::mixin_usage
  5. Fix src/cop/style/mixin_usage.rs
  6. Verify test passes: cargo test --lib -- cop::style::mixin_usage
  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
     ^^ Style/MixinUsage: Trailing whitespace detected.

The ^ characters must align with the offending columns. The message format is Style/MixinUsage: <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 Style/MixinUsage /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/style/mixin_usage.rs and tests/fixtures/cops/style/mixin_usage/
  • Run cargo test --lib -- cop::style::mixin_usage to verify your fix (do NOT run the full test suite)
  • Do NOT touch unrelated files
  • Do NOT use git stash

Prism Notes

  • const splits into ConstantReadNode (simple Foo) and ConstantPathNode (qualified Foo::Bar). If you handle one, check if you need the other.

Start Here

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

Helpful local commands:

  • python3 scripts/investigate-cop.py Style/MixinUsage --repos-only
  • python3 scripts/investigate-cop.py Style/MixinUsage --context
  • python3 scripts/verify-cop-locations.py Style/MixinUsage

Top FP repos:

  • ged__linguistics__b0b119c (3 FP) — example experiments/conjunct-with-block.rb:13

Representative FP examples:

  • ged__linguistics__b0b119c: experiments/conjunct-with-block.rb:13include is used at the top level. Use inside class or module.
  • ged__linguistics__b0b119c: experiments/lafcadio_plural.rb:13include is used at the top level. Use inside class or module.
  • ged__linguistics__b0b119c: experiments/lprintf.rb:17include is used at the top level. Use inside class or module.

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.

  • FP: 3 confirmed code bug(s), 0 context-dependent

FP #1: ged__linguistics__b0b119c: experiments/conjunct-with-block.rb:13

CONFIRMED false positive — CODE BUG
nitrocop incorrectly flags this pattern in isolation.
Fix the detection logic to not flag this.

Enclosing structure: BEGIN {} block (Prism: PreExecutionNode) (line: BEGIN {)
The offense is inside this structure — this is likely WHY
RuboCop does not flag it. Your fix should detect this context.

Full source context (add relevant parts to no_offense.rb):

#

BEGIN {
	base = File::dirname( File::dirname(File::expand_path(__FILE__)) )
	$LOAD_PATH.unshift "#{base}/lib"

	require "#{base}/utils.rb"
	include UtilityFunctions
}

require 'linguistics'

Linguistics::use( :en, :installProxy => true )
array = %w{sheep shrew goose bear penguin barnacle sheep goose goose}

Message: ``includeis used at the top level. Use insideclass` or `module`.`

FP #2: ged__linguistics__b0b119c: experiments/lafcadio_plural.rb:13

CONFIRMED false positive — CODE BUG
nitrocop incorrectly flags this pattern in isolation.
Fix the detection logic to not flag this.

Enclosing structure: BEGIN {} block (Prism: PreExecutionNode) (line: BEGIN {)
The offense is inside this structure — this is likely WHY
RuboCop does not flag it. Your fix should detect this context.

Full source context (add relevant parts to no_offense.rb):

#

BEGIN {
	base = File::dirname( File::dirname(File::expand_path(__FILE__)) )
	$LOAD_PATH.unshift "#{base}/lib"

	require "#{base}/utils.rb"
	include UtilityFunctions

	require 'linguistics'
}

$yaml = false
Linguistics::use( :en )

Message: ``includeis used at the top level. Use insideclass` or `module`.`

FP #3: ged__linguistics__b0b119c: experiments/lprintf.rb:17

CONFIRMED false positive — CODE BUG
nitrocop incorrectly flags this pattern in isolation.
Fix the detection logic to not flag this.

Enclosing structure: BEGIN {} block (Prism: PreExecutionNode) (line: BEGIN {)
The offense is inside this structure — this is likely WHY
RuboCop does not flag it. Your fix should detect this context.

Full source context (add relevant parts to no_offense.rb):

#

BEGIN {
	base = File::dirname( File::dirname(File::expand_path(__FILE__)) )
	$LOAD_PATH.unshift "#{base}/lib"

	require "#{base}/utils.rb"
	include UtilityFunctions
}

require 'linguistics'

Linguistics::use( :en, :classes => [String,Array] )

module Linguistics::EN

Message: ``includeis used at the top level. Use insideclass` or `module`.`

Current Rust Implementation

src/cop/style/mixin_usage.rs

use ruby_prism::Visit;

use crate::cop::{Cop, CopConfig};
use crate::diagnostic::Diagnostic;
use crate::parse::source::SourceFile;

/// Corpus investigation (round 1): 14+ FPs from `include T('default/layout/html')` in YARD
/// templates. Root cause: we checked `node.arguments().is_some()` which matches any argument
/// including method calls. RuboCop's node pattern requires arguments to be `const` nodes.
/// Fixed by verifying all arguments are ConstantReadNode or ConstantPathNode before flagging.
///
/// Corpus investigation (round 2): 6 FPs from `include M` inside `while`, `until`, `for`,
/// `case`, and lambda/proc blocks at the top level. Root cause: nitrocop only tracked
/// `in_class_or_module` and `in_block` (BlockNode) as scope barriers, but missed other
/// constructs. RuboCop's `in_top_level_scope?` pattern only considers `begin`, `kwbegin`,
/// `if`, and `def` as transparent wrappers — everything else (while, until, for, case,
/// lambda, etc.) creates an opaque scope. Fixed by replacing the opt-out approach with an
/// opt-in approach: only transparent nodes (if, def, begin) pass through the top-level flag.
///
/// Corpus investigation (round 3): 6 FPs from `include`/`extend`/`prepend` inside
/// `begin...rescue` or `begin...ensure` blocks at the top level. In RuboCop's Parser AST,
/// `begin...rescue...end` wraps the body in a `rescue` node, making it opaque (not in the
/// transparent `{kwbegin begin if def}` list). In Prism, statements are direct children
/// of `BeginNode`. Fixed by overriding `visit_begin_node` to mark the scope as opaque when
/// `rescue_clause` or `ensure_clause` is present. Plain `begin...end` remains transparent.
///
/// Corpus investigation (round 4): 2 FPs from `include GravatarHelper, GravatarHelper::PublicMethods, ERB::Util`
/// in redmine forks. Root cause: RuboCop's node pattern `(send nil? ${:include :extend :prepend} const)`
/// matches exactly ONE `const` argument. Multi-argument mixin calls like `include A, B, C`
/// don't match the pattern and are not flagged. nitrocop was incorrectly accepting any number
/// of const arguments. Fixed by requiring exactly one argument in the const check.
pub struct MixinUsage;

const MIXIN_METHODS: &[&[u8]] = &[b"include", b"extend", b"prepend"];

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

    fn check_source(
        &self,
        source: &SourceFile,
        parse_result: &ruby_prism::ParseResult<'_>,
        _code_map: &crate::parse::codemap::CodeMap,
        _config: &CopConfig,
        diagnostics: &mut Vec<Diagnostic>,
        _corrections: Option<&mut Vec<crate::correction::Correction>>,
    ) {
        let mut visitor = MixinUsageVisitor {
            cop: self,
            source,
            diagnostics: Vec::new(),
            in_opaque_scope: false,
        };
        visitor.visit(&parse_result.node());
        diagnostics.extend(visitor.diagnostics);
    }
}

struct MixinUsageVisitor<'a> {
    cop: &'a MixinUsage,
    source: &'a SourceFile,
    diagnostics: Vec<Diagnostic>,
    /// True when we're inside a scope that is NOT considered "top level" by RuboCop.
    /// RuboCop's `in_top_level_scope?` only treats `begin`, `kwbegin`, `if`, and `def`
    /// as transparent wrappers. Everything else (class, module, block, while, until,
    /// for, case, lambda, etc.) creates an opaque scope where mixin calls are allowed.
    in_opaque_scope: bool,
}

impl<'pr> Visit<'pr> for MixinUsageVisitor<'_> {
    fn visit_call_node(&mut self, node: &ruby_prism::CallNode<'pr>) {
        let method_bytes = node.name().as_slice();

        if MIXIN_METHODS.contains(&method_bytes)
            && node.receiver().is_none()
            && !self.in_opaque_scope
        {
            // RuboCop's node pattern `(send nil? ${:include :extend :prepend} const)`
            // matches exactly ONE `const` argument. Multi-argument calls like
            // `include A, B, C` don't match, nor do method call arguments like
            // `include T('...')`.
            let is_single_const_mixin = node.arguments().is_some_and(|args| {
                let arguments: Vec<_> = args.arguments().iter().collect();
                arguments.len() == 1
                    && (arguments[0].as_constant_read_node().is_some()
                        || arguments[0].as_constant_path_node().is_some())
            });

            if is_single_const_mixin {
                let method_str = std::str::from_utf8(method_bytes).unwrap_or("include");
                let loc = node.location();
                let (line, column) = self.source.offset_to_line_col(loc.start_offset());
                self.diagnostics.push(self.cop.diagnostic(
                    self.source,
                    line,
                    column,
                    format!(
                        "`{method_str}` is used at the top level. Use inside `class` or `module`."
                    ),
                ));
            }
        }

        // Visit children
        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);
        }
    }

    // === Transparent wrappers (RuboCop considers these still "top level") ===
    // `begin`/`kwbegin`, `if`, and `def` are transparent.
    // No need to override visit_if_node or visit_def_node —
    // the default traversal descends into children without changing in_opaque_scope.
    //
    // However, `begin...rescue...end` and `begin...ensure...end` are special:
    // In RuboCop's Parser AST, the `rescue`/`ensure` node becomes the parent of
    // the body statements, and `rescue`/`ensure` is NOT in the transparent list.
    // So we must treat BeginNode with rescue/ensure as opaque.
    fn visit_begin_node(&mut self, node: &ruby_prism::BeginNode<'pr>) {
        let has_rescue_or_ensure = node.rescue_clause().is_some() || node.ensure_clause().is_some();
        if has_rescue_or_ensure {
            let prev = self.in_opaque_scope;
            self.in_opaque_scope = true;
            ruby_prism::visit_begin_node(self, node);
            self.in_opaque_scope = prev;
        } else {
            // Plain `begin...end` without rescue/ensure is transparent
            ruby_prism::visit_begin_node(self, node);
        }
    }

    // === Opaque scopes (mixin calls inside these are NOT top-level) ===

    fn visit_class_node(&mut self, node: &ruby_prism::ClassNode<'pr>) {
        let prev = self.in_opaque_scope;
        self.in_opaque_scope = true;
        if let Some(body) = node.body() {
            self.visit(&body);
        }
        self.in_opaque_scope = prev;
    }

    fn visit_module_node(&mut self, node: &ruby_prism::ModuleNode<'pr>) {
        let prev = self.in_opaque_scope;
        self.in_opaque_scope = true;
        if let Some(body) = node.body() {
            self.visit(&body);
        }
        self.in_opaque_scope = prev;
    }

    fn visit_singleton_class_node(&mut self, node: &ruby_prism::SingletonClassNode<'pr>) {
        let prev = self.in_opaque_scope;
        self.in_opaque_scope = true;
        if let Some(body) = node.body() {
            self.visit(&body);
        }
        self.in_opaque_scope = prev;
    }

    fn visit_block_node(&mut self, node: &ruby_prism::BlockNode<'pr>) {
        let prev = self.in_opaque_scope;
        self.in_opaque_scope = true;
        if let Some(body) = node.body() {
            self.visit(&body);
        }
        self.in_opaque_scope = prev;
    }

    fn visit_lambda_node(&mut self, node: &ruby_prism::LambdaNode<'pr>) {
        let prev = self.in_opaque_scope;
        self.in_opaque_scope = true;
        if let Some(body) = node.body() {
            self.visit(&body);
        }
        self.in_opaque_scope = prev;
    }

    fn visit_while_node(&mut self, node: &ruby_prism::WhileNode<'pr>) {
        let prev = self.in_opaque_scope;
        self.in_opaque_scope = true;
        ruby_prism::visit_while_node(self, node);
        self.in_opaque_scope = prev;
    }

    fn visit_until_node(&mut self, node: &ruby_prism::UntilNode<'pr>) {
        let prev = self.in_opaque_scope;
        self.in_opaque_scope = true;
        ruby_prism::visit_until_node(self, node);
        self.in_opaque_scope = prev;
    }

    fn visit_for_node(&mut self, node: &ruby_prism::ForNode<'pr>) {
        let prev = self.in_opaque_scope;
        self.in_opaque_scope = true;
        ruby_prism::visit_for_node(self, node);
        self.in_opaque_scope = prev;
    }

    fn visit_case_node(&mut self, node: &ruby_prism::CaseNode<'pr>) {
        let prev = self.in_opaque_scope;
        self.in_opaque_scope = true;
        ruby_prism::visit_case_node(self, node);
        self.in_opaque_scope = prev;
    }

    fn visit_case_match_node(&mut self, node: &ruby_prism::CaseMatchNode<'pr>) {
        let prev = self.in_opaque_scope;
        self.in_opaque_scope = true;
        ruby_prism::visit_case_match_node(self, node);
        self.in_opaque_scope = prev;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    crate::cop_fixture_tests!(MixinUsage, "cops/style/mixin_usage");
}

RuboCop Ruby Implementation (ground truth)

vendor/rubocop/lib/rubocop/cop/style/mixin_usage.rb

# frozen_string_literal: true

module RuboCop
  module Cop
    module Style
      # Checks that `include`, `extend` and `prepend` statements appear
      # inside classes and modules, not at the top level, so as to not affect
      # the behavior of `Object`.
      #
      # @example
      #   # bad
      #   include M
      #
      #   class C
      #   end
      #
      #   # bad
      #   extend M
      #
      #   class C
      #   end
      #
      #   # bad
      #   prepend M
      #
      #   class C
      #   end
      #
      #   # good
      #   class C
      #     include M
      #   end
      #
      #   # good
      #   class C
      #     extend M
      #   end
      #
      #   # good
      #   class C
      #     prepend M
      #   end
      class MixinUsage < Base
        MSG = '`%<statement>s` is used at the top level. Use inside `class` or `module`.'
        RESTRICT_ON_SEND = %i[include extend prepend].freeze

        # @!method include_statement(node)
        def_node_matcher :include_statement, <<~PATTERN
          (send nil? ${:include :extend :prepend}
            const)
        PATTERN

        # @!method in_top_level_scope?(node)
        def_node_matcher :in_top_level_scope?, <<~PATTERN
          {
            root?                        # either at the top level
            ^[  {kwbegin begin if def}   # or wrapped within one of these
                #in_top_level_scope? ]   # that is in top level scope
          }
        PATTERN

        def on_send(node)
          include_statement(node) do |statement|
            return unless in_top_level_scope?(node)

            add_offense(node, message: format(MSG, statement: statement))
          end
        end
      end
    end
  end
end

RuboCop Test Excerpts

vendor/rubocop/spec/rubocop/cop/style/mixin_usage_spec.rb

  context 'include' do

    it 'registers an offense when using outside class (used above)' do

      expect_offense(<<~RUBY)
        include M
        ^^^^^^^^^ `include` is used at the top level. Use inside `class` or `module`.
        class C
        end
      RUBY

    it 'registers an offense when using outside class (used below)' do

      expect_offense(<<~RUBY)
        class C
        end
        include M
        ^^^^^^^^^ `include` is used at the top level. Use inside `class` or `module`.
      RUBY

    it 'registers an offense when using only `include` statement' do

      expect_offense(<<~RUBY)
        include M
        ^^^^^^^^^ `include` is used at the top level. Use inside `class` or `module`.
      RUBY

    it 'registers an offense when using `include` in method definition outside class or module' do

      expect_offense(<<~RUBY)
        def foo
          include M
          ^^^^^^^^^ `include` is used at the top level. Use inside `class` or `module`.
        end
      RUBY

    it 'does not register an offense when using outside class' do

      expect_no_offenses(<<~RUBY)
        Foo.include M
        class C; end
      RUBY

    it 'does not register an offense when using inside class' do

      expect_no_offenses(<<~RUBY)
        class C
          include M
        end
      RUBY

    it 'does not register an offense when using inside block' do

      expect_no_offenses(<<~RUBY)
        Class.new do
          include M
        end
      RUBY

    it 'does not register an offense when using inside block ' \

      expect_no_offenses(<<~RUBY)
        klass.class_eval do
          include M1
          include M2 if defined?(M)
        end
      RUBY

    it "doesn't register an offense when `include` call is a method argument" do

      expect_no_offenses(<<~RUBY)
        do_something(include(M))
      RUBY

    it 'does not register an offense when using `include` in method definition inside class' do

Current Fixture: offense.rb

tests/fixtures/cops/style/mixin_usage/offense.rb

include M
^^^^^^^^^ Style/MixinUsage: `include` is used at the top level. Use inside `class` or `module`.

extend N
^^^^^^^^ Style/MixinUsage: `extend` is used at the top level. Use inside `class` or `module`.

prepend O
^^^^^^^^^ Style/MixinUsage: `prepend` is used at the top level. Use inside `class` or `module`.

# Transparent wrappers: include inside def/if/begin at top level is still flagged
def foo
  include M
  ^^^^^^^^^ Style/MixinUsage: `include` is used at the top level. Use inside `class` or `module`.
end

if condition
  extend N
  ^^^^^^^^ Style/MixinUsage: `extend` is used at the top level. Use inside `class` or `module`.
end

begin
  prepend O
  ^^^^^^^^^ Style/MixinUsage: `prepend` is used at the top level. Use inside `class` or `module`.
end

include M1::M2::M3
^^^^^^^^^^^^^^^^^^ Style/MixinUsage: `include` is used at the top level. Use inside `class` or `module`.

Current Fixture: no_offense.rb

tests/fixtures/cops/style/mixin_usage/no_offense.rb

class C
  include M
end
module N
  extend O
end
Foo.include M
Class.new do
  include M
end
obj.include(M)
# Method call arguments should not be flagged (only constants)
include T('default/layout/html')
extend some_method
prepend build_module(:foo)
# include inside while/until/for/case/lambda at top level is NOT flagged by RuboCop
while condition
  include M
end
until done
  extend N
end
for x in items
  prepend O
end
case foo
when :bar
  include M
end
-> { include M }
proc { include M }
# include inside begin/rescue at top level is NOT flagged by RuboCop
begin
  include M
rescue LoadError
  nil
end
begin
  require 'something'
  include M
rescue LoadError => e
  puts e
end
# include inside if inside begin/ensure at top level
begin
  if condition
    include M
  end
ensure
  cleanup
end
# Multiple constant arguments: RuboCop's pattern matches only a single const
include GravatarHelper, GravatarHelper::PublicMethods, ERB::Util
extend A, B
prepend X, Y, Z

@6 6 bot force-pushed the fix/style-mixin_usage-23473250532 branch from c6138e0 to 4b0d0ab Compare March 24, 2026 04:44
@6 6 bot marked this pull request as ready for review March 24, 2026 04:44
@6 6 bot enabled auto-merge (squash) March 24, 2026 04:44
@6 6 bot added the needs-human label Mar 24, 2026
@6
Copy link
Copy Markdown
Contributor Author

6 bot commented Mar 24, 2026

Auto-repair Agent Failed

The repair agent step failed before local verification ran.

@6 6 closed this Mar 24, 2026
auto-merge was automatically disabled March 24, 2026 05:03

Pull request was closed

@6 6 deleted the fix/style-mixin_usage-23473250532 branch March 24, 2026 05:03
@6
Copy link
Copy Markdown
Contributor Author

6 bot commented Mar 24, 2026

Auto-repair Started

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

PR Repair Task: PR #151

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 Style/MixinUsage #151 [bot] Fix Style/MixinUsage
  • Branch: fix/style-mixin_usage-23473250532
  • Checks run: #929 (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/style/mixin_usage.rs                        | 12 ++++++++++++
 tests/fixtures/cops/style/mixin_usage/no_offense.rb |  4 ++++
 2 files changed, 16 insertions(+)

Current PR Diff Excerpt

diff --git a/src/cop/style/mixin_usage.rs b/src/cop/style/mixin_usage.rs
index f297299bd..568a2a9e7 100644
--- a/src/cop/style/mixin_usage.rs
+++ b/src/cop/style/mixin_usage.rs
@@ -29,6 +29,11 @@ use crate::parse::source::SourceFile;
 /// matches exactly ONE `const` argument. Multi-argument mixin calls like `include A, B, C`
 /// don't match the pattern and are not flagged. nitrocop was incorrectly accepting any number
 /// of const arguments. Fixed by requiring exactly one argument in the const check.
+///
+/// Corpus investigation (round 5): 3 FPs from `include` inside `BEGIN {}` blocks
+/// (Prism: PreExecutionNode). `BEGIN {}` is not in RuboCop's transparent wrapper list
+/// (`{kwbegin begin if def}`), so it creates an opaque scope. Fixed by adding
+/// `visit_pre_execution_node` as an opaque scope handler.
 pub struct MixinUsage;
 
 const MIXIN_METHODS: &[&[u8]] = &[b"include", b"extend", b"prepend"];
@@ -220,6 +225,13 @@ impl<'pr> Visit<'pr> for MixinUsageVisitor<'_> {
         ruby_prism::visit_case_match_node(self, node);
         self.in_opaque_scope = prev;
     }
+
+    fn visit_pre_execution_node(&mut self, node: &ruby_prism::PreExecutionNode<'pr>) {
+        let prev = self.in_opaque_scope;
+        self.in_opaque_scope = true;
+        ruby_prism::visit_pre_execution_node(self, node);
+        self.in_opaque_scope = prev;
+    }
 }
 
 #[cfg(test)]
diff --git a/tests/fixtures/cops/style/mixin_usage/no_offense.rb b/tests/fixtures/cops/style/mixin_usage/no_offense.rb
index 173609fe3..9682f6a0c 100644
--- a/tests/fixtures/cops/style/mixin_usage/no_offense.rb
+++ b/tests/fixtures/cops/style/mixin_usage/no_offense.rb
@@ -53,3 +53,7 @@ module N
 include GravatarHelper, GravatarHelper::PublicMethods, ERB::Util
 extend A, B
 prepend X, Y, Z
+# include inside BEGIN {} block is NOT flagged by RuboCop (PreExecutionNode is opaque)
+BEGIN {
+  include UtilityFunctions
+}

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.

  • Standard corpus JSON (matches the PR cop-check gate): /home/runner/work/_temp/agent-pr-repair/repair/corpus-standard.json (corpus oracle run #23471072736)
  • Extended corpus JSON (broader diagnosis): /home/runner/work/_temp/agent-pr-repair/repair/corpus-extended.json (corpus oracle run #23471571040)

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-standard.json --context
python3 scripts/check-cop.py Department/CopName --input /home/runner/work/_temp/agent-pr-repair/repair/corpus-standard.json --verbose --rerun --quick --clone

Failed Checks Packet

cop-check

  • Route: hard
  • Failed steps: Check cops against corpus baseline
... (truncated, showing last 80 lines) ...
echo ""
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 < /tmp/changed_cops.txt
if [ "$failed" -gt 0 ]; then
echo ""
echo "FAILED: $failed cop(s) regressed vs corpus baseline."
exit 1
fi
shell: /usr/bin/bash -e {0}
env:
CARGO_HOME: /home/runner/.cargo
CARGO_INCREMENTAL: 0
CARGO_TERM_COLOR: always
CACHE_ON_FAILURE: false
NITROCOP_BIN: /home/runner/work/nitrocop/nitrocop/target/release/nitrocop
GH_TOKEN: ***
##[endgroup]
==============================
Checking: Style/MixinUsage (re-running against corpus)
==============================
Cached synthetic-results.json from artifact
Downloaded standard corpus-report from run 23471072736 via gh
Cloning 186 repos for Style/MixinUsage...
Cloned 186/186 repos
Running nitrocop per-repo...
(used batch --corpus-check mode)
Checking Style/MixinUsage against corpus
Gate: count-only cop-level regression check
Not a location-level conformance proof or a department completion gate
Baseline (from CI): 540 matches, 0 FP, 0 FN
Expected RuboCop offenses: 540
Repos with offenses (186):
35  travis-ci__dpl__8c6eabc
29  puppetlabs__puppet__e227c27
21  seyhunak__twitter-bootstrap-rails__de5f917
20  DataDog__dd-trace-rb__e3406de
19  lsegal__yard__eddd10c
16  autolab__Autolab__674efe9
12  claudiob__bh__34e6f00
12  helpyio__helpy__26aff0e
12  scinote-eln__scinote-web__8435988
8  ifmeorg__ifme__4145f80
8  kschiess__parslet__f61daf0
8  newrelic__newrelic-ruby-agent__e5684e4
8  publiclab__plots2__5e1d8c2
7  Arachni__arachni__c1710d6
7  jashkenas__ruby-processing__2d83318
7  lynndylanhurley__devise_token_auth__bcdc3a5
7  ruby-concurrency__concurrent-ruby__30dc89e
6  antiwork__gumroad__782a69f
6  gettalong__hexapdf__f1176c6
6  github__entitlements-app__287ad6b
6  kanwei__algorithms__feaa5d5
6  owen2345__camaleon-cms__acc1dfa
5  arches__table_print__591affe
5  guard__listen__2fa1a74
5  kmuto__review__031f3a6
5  nesquena__rabl__50ebc12
5  otwcode__otwarchive__412f04f
5  peritor__webistrano__a98ba6b
5  rgeo__activerecord-postgis-adapter__ed2e3d5
5  rubyworks__facets__12326d4
... and 156 more
Results:
Expected (RuboCop):          540
Actual (nitrocop):           574
CI nitrocop baseline:        540
Excess (potential FP):        34
Missing (potential FN):        0
Gate type: count-only / cop-level regression
FAIL: FP increased from 0 to 34 (+34, threshold: 0)
FAIL: Style/MixinUsage 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:

  • Style/MixinUsage

Style/MixinUsage

Start here:

  • Re-run after edits: python3 scripts/check-cop.py Style/MixinUsage --verbose --rerun --quick --clone
  • Baseline corpus context: python3 scripts/investigate-cop.py Style/MixinUsage --input /home/runner/work/_temp/agent-pr-repair/repair/corpus-standard.json --repos-only
  • Batch sanity check if counts look suspicious: python3 scripts/check-cop.py Style/MixinUsage --verbose --rerun --quick --clone --no-batch
  • This local packet used batch --corpus-check; compare 1-2 top repos in per-repo mode before inventing a full manual sweep.
  • Inspect repo: /home/runner/work/nitrocop/nitrocop/vendor/corpus/travis-ci__dpl__8c6eabc
  • Inspect repo: /home/runner/work/nitrocop/nitrocop/vendor/corpus/puppetlabs__puppet__e227c27
  • Inspect repo: /home/runner/work/nitrocop/nitrocop/vendor/corpus/seyhunak__twitter-bootstrap-rails__de5f917
  • Inspect repo: /home/runner/work/nitrocop/nitrocop/vendor/corpus/DataDog__dd-trace-rb__e3406de
  • Inspect repo: /home/runner/work/nitrocop/nitrocop/vendor/corpus/lsegal__yard__eddd10c

Oracle context from CI corpus artifact:

  • Repos and exact examples: python3 scripts/investigate-cop.py Style/MixinUsage --input /home/runner/work/_temp/agent-pr-repair/repair/corpus-standard.json --fn-only --context --limit 10
/usr/bin/python3 scripts/check-cop.py Style/MixinUsage --verbose --rerun --quick --clone

Exit status: 1

Checking Style/MixinUsage against corpus
Gate: count-only cop-level regression check
Not a location-level conformance proof or a department completion gate
Baseline (from CI): 540 matches, 0 FP, 0 FN
Expected RuboCop offenses: 540

Repos with offenses (186):
      35  travis-ci__dpl__8c6eabc
      29  puppetlabs__puppet__e227c27
      21  seyhunak__twitter-bootstrap-rails__de5f917
      20  DataDog__dd-trace-rb__e3406de
      19  lsegal__yard__eddd10c
      16  autolab__Autolab__674efe9
      12  claudiob__bh__34e6f00
      12  helpyio__helpy__26aff0e
      12  scinote-eln__scinote-web__8435988
       8  ifmeorg__ifme__4145f80
       8  kschiess__parslet__f61daf0
       8  newrelic__newrelic-ruby-agent__e5684e4
       8  publiclab__plots2__5e1d8c2
       7  Arachni__arachni__c1710d6
       7  jashkenas__ruby-processing__2d83318
       7  lynndylanhurley__devise_token_auth__bcdc3a5
       7  ruby-concurrency__concurrent-ruby__30dc89e
       6  antiwork__gumroad__782a69f
       6  gettalong__hexapdf__f1176c6
       6  github__entitlements-app__287ad6b
       6  kanwei__algorithms__feaa5d5
       6  owen2345__camaleon-cms__acc1dfa
       5  arches__table_print__591affe
       5  guard__listen__2fa1a74
       5  kmuto__review__031f3a6
       5  nesquena__rabl__50ebc12
       5  otwcode__otwarchive__412f04f
       5  peritor__webistrano__a98ba6b
       5  rgeo__activerecord-postgis-adapter__ed2e3d5
       5  rubyworks__facets__12326d4
  ... and 156 more

Results:
  Expected (RuboCop):          540
  Actual (nitrocop):           574
  CI nitrocop baseline:        540
  Excess (potential FP):        34
  Missing (potential FN):        0

  Gate type: count-only / cop-level regression

FAIL: FP increased from 0 to 34 (+34, threshold: 0)
gh CLI found but not authenticated, trying other methods...
Found corpus oracle run but need a token to download artifacts.
Set GH_TOKEN, GH_TOKEN_FOR_ACTIONS_READ, or GITHUB_TOKEN env var, or run: gh auth login
Using docs/corpus.md as fallback (summary data only, dated 2026-03-24)
  No baseline activity or divergence for Style/MixinUsage
Running nitrocop per-repo...
  (used batch --corpus-check mode)

@6
Copy link
Copy Markdown
Contributor Author

6 bot commented Mar 24, 2026

Auto-repair Agent Failed

The repair agent step failed before local verification ran.

@6 6 restored the fix/style-mixin_usage-23473250532 branch March 24, 2026 05:19
@6 6 reopened this Mar 24, 2026
@6 6 removed the needs-human label Mar 24, 2026
@6 6 enabled auto-merge (squash) March 24, 2026 05:25
@6 6 merged commit 660d57d into main Mar 24, 2026
4 checks passed
@6 6 deleted the fix/style-mixin_usage-23473250532 branch March 24, 2026 13:44
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