Skip to content

[bot] Fix Layout/ClosingHeredocIndentation#853

Merged
6[bot] merged 2 commits intomainfrom
fix/layout-closing_heredoc_indentation-23728141741
Mar 30, 2026
Merged

[bot] Fix Layout/ClosingHeredocIndentation#853
6[bot] merged 2 commits intomainfrom
fix/layout-closing_heredoc_indentation-23728141741

Conversation

@6
Copy link
Copy Markdown
Contributor

@6 6 bot commented Mar 30, 2026

Automated fix fix for Layout/ClosingHeredocIndentation corpus conformance.

Details

Cop Layout/ClosingHeredocIndentation
Backend codex / hard
Model gpt-5.4 (xhigh)
Mode fix
Turns 1
Run #405

Result

Updated [closing_heredoc_indentation.rs](/home/runner/work/nitrocop/nitrocop/src/cop/layout/closing_heredoc_indentation.rs) so outer call indentation is no longer inherited while visiting `AssocNode` values. That makes heredocs in hash-pair values like `cooked: <<~DOC` and `:template => <<-DOC` align only to their own opening line, which matches RuboCop. I also added a doc comment, two focused unit tests, and replaced the invalid placeholder tail in [offense.rb](/home/runner/work/nitrocop/nitrocop/tests/fixtures/cops/layout/closing_heredoc_indentation/offense.rb) with parseable repro cases.

Verification:
- `cargo test --lib -- cop::layout::closing_heredoc_indentation` passed.
- `python3 scripts/check_cop.py Layout/ClosingHeredocIndentation --rerun --clone --sample 15` passed with `0` new FP and `0` new FN regressions; the sampled oracle spot-check reported `27` FN resolved and `3` remaining.
- Commit: `9c844d8e` (`Fix ClosingHeredocIndentation hash pair values`).

Changes

  2 files changed
Agent conversation excerpt

I’m reading the CI guidance first, then I’ll inspect the current cop, its fixtures, and RuboCop’s reference implementation to pin down the false-negative pattern before editing anything.

I’m reading the CI guidance first, then I’ll inspect the current cop, its fixtures, and RuboCop’s reference implementation to pin down the false-negative pattern before editing anything.

exec_command: sed -n '1,240p' docs/agent-ci.md

exec_command: sed -n '1,240p' src/cop/layout/closing_heredoc_indentation.rs

exec_command: sed -n '1,240p' vendor/rubocop/lib/rubocop/cop/layout/closing_heredoc_indentation.rb

exec_command: sed -n '1,260p' vendor/rubocop/spec/rubocop/cop/layout/closing_heredoc_indentation_spec.rb

exec_command: sed -n '1,240p' tests/fixtures/cops/layout/closing_heredoc_indentation/offense.rb

exec_command: sed -n '1,240p' tests/fixtures/cops/layout/closing_heredoc_indentation/no_offense.rb

I’ve got the current implementation and reference behavior. The fixture already has some bare closing-token lines appended at the bottom, so I’m checking the worktree before I touch anything and then I’ll reproduce the failing behavior with the focused cop test.

I’ve got the current implementation and reference behavior. The fixture already has some bare closing-token lines appended at the bottom, so I’m checking the worktree before I touch anything and then I’ll reproduce the failing behavior with the focused cop test.

exec_command: git status --short

exec_command: sed -n '241,360p' src/cop/layout/closing_heredoc_indentation.rs

exec_command: python3 scripts/investigate_cop.py Layout/ClosingHeredocIndentation --context | sed -n '1,220p'

The worktree shows only the target fixture file as modified. I’m treating that as in-scope and checking whether the currently appended bare markers are enough to fail the focused test or if I need to replace them with parseable snippets.

The worktree shows only the target fixture file as modified. I’m treating that as in-scope and checking whether the currently appended bare markers are enough to fail the focused test or if I need to replace them with parseable snippets.

write_stdin: ``

exec_command: cargo test --lib -- cop::layout::closing_heredoc_indentation

exec_command: nl -ba tests/fixtures/cops/layout/closing_heredoc_indentation/offense.rb | sed -n '1,220p'

The current fixture is syntactically incomplete at the bottom, which is likely why it was left in this state. I’m letting the focused cop test finish so I can confirm the exact failure mode before I replace those placeholders with real parseable Ruby cases.

The current fixture is syntactically incomplete at the bottom, which is likely why it was left in this state. I’m letting the focused cop test finish so I can confirm the exact failure mode before I replace those placeholders with real parseable Ruby cases.

write_stdin: ``

write_stdin: ``

write_stdin: ``

write_stdin: ``

write_stdin: ``

I’ve confirmed the corpus FN cluster is all the same shape: heredocs used as hash values or keyword arguments inside call arguments. The current visitor only establishes “argument context” for CallNode arguments, so I’m now drilling into Prism’s node shape around hash pairs to see where that context gets dropped.

I’ve confirmed the corpus FN cluster is all the same shape: heredocs used as hash values or keyword arguments inside call arguments. The current visitor only establishes “argument context” for CallNode arguments, so I’m now drilling into Prism’s node shape around hash pairs to see where that context gets dropped.

exec_command: rg -n "argument\(|chained\(|HashNode|AssocNode|PairNode|visit_hash|visit_assoc|visit_keyword_hash" -S src vendor -g '!target'

exec_command: ruby -e 'require "prism"; src=%q{Fabricate.build(\n :chat_message,\n cooked: <<~COOKED,\n x\n COOKED\n)\n}; p Prism.parse(src).value'

write_stdin: ``

The quick Prism probe was malformed because of shell quoting, so I’m rerunning it with a real temp file. I also still have the focused test running in the background; once it returns I’ll replace the broken fixture tail with concrete reproductions and confirm the test fails for the right reason.

The quick Prism probe was malformed because of shell quoting, so I’m rerunning it with a real temp file. I also still have the focused test running in the background; once it returns I’ll replace the broken fixture tail with concrete reproductions and confirm the test fails for the right reason.

exec_command: cat > /tmp/closing_heredoc_probe.rb <<'RUBY' Fabricate.build( :chat_message, cooked: <<~COOKED, x COOKED ) RUBY ruby -e 'require "prism"; src = File.read("/tmp/closing_heredoc_probe.rb"); pp

exec_command: `cat > /tmp/closing_heredoc_probe2.rb <<'RUBY'
create_dynamic_portlet(:recently_updated_pages,
:template => <<-TEMPLATE

Recent Updates

TEMPLATE ) RUBY ruby -e 'req`

write_stdin: ``

The FN mechanism is clear now: the visitor is treating every heredoc anywhere inside a call’s argument list as eligible for “align to method definition,” but RuboCop only grants that exception to direct arguments or chained heredoc calls. Hash-pair values like cooked: <<~COOKED, should stay strict against their own opening line.

The FN mechanism is clear now: the visitor is treating every heredoc anywhere inside a call’s argument list as eligible for “align to method definition,” but RuboCop only grants that exception to direct arguments or chained heredoc calls. Hash-pair values like cooked: <<~COOKED, should stay strict against their own opening line.

exec_command: rg -n "\.parent\(\)" src -g '!target' | sed -n '1,220p'

exec_command: ruby -e 'require "prism"; src = File.read("/tmp/closing_heredoc_probe.rb"); result = Prism.parse(src); kw = result.value.statements.body[0].arguments.arguments[1]; assoc = kw.elements[0]; str = assoc.

exec_command: ruby -e 'require "prism"; src = File.read("/tmp/closing_heredoc_probe.rb"); result = Prism.parse(src); kw = result.value.statements.body[0].arguments.arguments[1]; assoc = kw.elements[0]; str = assoc.

update_plan

I have the fix shape. Next I’m editing the fixture first: removing the non-parseable placeholder lines and replacing them with minimal, real Ruby reproductions for hash-pair heredocs so the failing test is meaningful.

I have the fix shape. Next I’m editing the fixture first: removing the non-parseable placeholder lines and replacing them with minimal, real Ruby reproductions for hash-pair heredocs so the failing test is meaningful.

exec_command: `cat > /tmp/test.rb <<'RUBY'
create_dynamic_portlet(:recently_updated_pages,
:template => <<-TEMPLATE

Recent Updates

TEMPLATE ) RUBY rubocop --only Layout/ClosingHeredoc`

exec_command: cat > /tmp/test2.rb <<'RUBY' message = Fabricate.build( :chat_message, cooked: <<~COOKED, content COOKED ) RUBY rubocop --only Layout/ClosingHeredocIndentation /tmp/test2.rb

exec_command: cat > /tmp/test3.rb <<'RUBY' second_migration_field = Fabricate( :migration_theme_field, value: <<~JS, export default function migrate(settings) { settings.set("integer_setting", 3

write_stdin: ``

write_stdin: ``

write_stdin: ``

apply_patch

The fixture now contains real Ruby repros for the hash-pair cases. I’m rerunning the focused cop test to confirm those new cases fail against the current implementation before I change the Rust logic.

The fixture now contains real Ruby repros for the hash-pair cases. I’m rerunning the focused cop test to confirm those new cases fail against the current implementation before I change the Rust logic.

exec_command: cargo test --lib -- cop::layout::closing_heredoc_indentation

exec_command: rg -n "visit_assoc_node\(|node\.key\(|node\.value\(" src/cop -g '!target' | sed -n '1,220p'

exec_command: sed -n '1180,1255p' src/cop/style/method_call_with_args_parentheses.rs

write_stdin: ``

write_stdin: ``

write_stdin: ``

I’ve reproduced the meaningful failure. I’m patching the visitor now so argument_indent is cleared when traversing an AssocNode value, which keeps the “align to method definition” exception limited to direct heredoc arguments and chained heredoc calls, matching RuboCop.

I’ve reproduced the meaningful failure. I’m patching the visitor now so argument_indent is cleared when traversing an AssocNode value, which keeps the “align to method definition” exception limited to direct heredoc arguments and chained heredoc calls, matching RuboCop.

Task prompt (6989 tokens)

Fix Layout/ClosingHeredocIndentation — 0 FP, 30 FN

Instructions

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

Current state: 5,970 matches, 0 false positives, 30 false negatives.
Focus on: FN (RuboCop flags code nitrocop misses).

⚠ 5,970 existing matches must not regress. Validate with check_cop.py before committing.

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 Layout/ClosingHeredocIndentation /tmp/test.rb
    echo '<general pattern>' > /tmp/test.rb && rubocop --only Layout/ClosingHeredocIndentation /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/layout/closing_heredoc_indentation/offense.rb with ^ annotation
    • FP fix: add the false-positive pattern to tests/fixtures/cops/layout/closing_heredoc_indentation/no_offense.rb
  4. Verify test fails: cargo test --lib -- cop::layout::closing_heredoc_indentation
  5. Fix src/cop/layout/closing_heredoc_indentation.rs
  6. Verify test passes: cargo test --lib -- cop::layout::closing_heredoc_indentation
  7. Validate against corpus (REQUIRED before committing):
    python3 scripts/check_cop.py Layout/ClosingHeredocIndentation --rerun --clone --sample 15
    If this reports FP or FN regression, your fix is too broad — narrow it down.
  8. Add a /// doc comment on the cop struct documenting what you found and fixed
  9. Commit only your cop's files

Fixture Format

Mark offenses with ^ markers on the line AFTER the offending source line.
The ^ characters must align with the offending columns. The message format is Layout/ClosingHeredocIndentation: <message text>.
See the Current Fixture sections below for real examples from this cop.

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 Layout/ClosingHeredocIndentation /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/layout/closing_heredoc_indentation.rs and tests/fixtures/cops/layout/closing_heredoc_indentation/
  • Run cargo test --lib -- cop::layout::closing_heredoc_indentation to verify your fix (do NOT run the full test suite)
  • Run python3 scripts/check_cop.py Layout/ClosingHeredocIndentation --rerun --clone --sample 15 before committing to catch regressions
  • Do NOT touch unrelated files
  • Do NOT use git stash
  • Do NOT push — you do not have push permission; the workflow handles pushing after you exit

Current Fixture: offense.rb

tests/fixtures/cops/layout/closing_heredoc_indentation/offense.rb

class Foo
  def bar
    <<~SQL
      'Hi'
  SQL
  ^^^ Layout/ClosingHeredocIndentation: `SQL` is not aligned with `<<~SQL`.
  end
end

class Baz
  def qux
    <<~RUBY
      something
        RUBY
        ^^^^ Layout/ClosingHeredocIndentation: `RUBY` is not aligned with `<<~RUBY`.
  end
end

def example
  <<-TEXT
    hello
      TEXT
      ^^^^ Layout/ClosingHeredocIndentation: `TEXT` is not aligned with `<<-TEXT`.
end

# Heredoc in block body should still flag offense
get '/foo' do
    <<-EOHTML
    <html></html>
EOHTML
^^^^^^ Layout/ClosingHeredocIndentation: `EOHTML` is not aligned with `<<-EOHTML`.
end

# Heredoc as argument with wrong closing alignment (matches neither opening nor call)
include_examples :offense,
                 <<-HEREDOC
  bar
    HEREDOC
    ^^^^^^^ Layout/ClosingHeredocIndentation: `HEREDOC` is not aligned with `<<-HEREDOC` or beginning of method definition.

Current Fixture: no_offense.rb

tests/fixtures/cops/layout/closing_heredoc_indentation/no_offense.rb

class Foo
  def bar
    <<~SQL
      'Hi'
    SQL
  end
end

class Baz
  def qux
    <<~RUBY
      something
    RUBY
  end
end

def example
  <<-TEXT
    hello
  TEXT
end

x = <<SIMPLE
no indent required
SIMPLE

# Heredoc argument aligned to outermost call
include_examples :offense,
                 <<-HEREDOC
  bar
HEREDOC

# Heredoc argument with strip_indent aligned to outermost call
include_examples :offense,
                 <<-HEREDOC.strip_indent
  bar
HEREDOC

# Heredoc in block body, properly aligned
get '/foo' do
  <<-EOHTML
  <html></html>
  EOHTML
end

# Chained call with heredoc argument aligned to outermost call
expect($stdout.string)
  .to eq(<<~RESULT)
    content here
RESULT

# Empty heredoc content, aligned
let(:source) { <<~HEREDOC }
HEREDOC

Key Source Files

  • Rust implementation: src/cop/layout/closing_heredoc_indentation.rs
  • RuboCop Ruby source (ground truth): vendor/rubocop/lib/rubocop/cop/layout/closing_heredoc_indentation.rb
  • RuboCop test excerpts: vendor/rubocop/spec/rubocop/cop/layout/closing_heredoc_indentation_spec.rb

Read these files before making changes.

Start Here

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

Helpful local commands:

  • python3 scripts/investigate_cop.py Layout/ClosingHeredocIndentation --repos-only
  • python3 scripts/investigate_cop.py Layout/ClosingHeredocIndentation --context
  • python3 scripts/verify_cop_locations.py Layout/ClosingHeredocIndentation

Top FN repos:

  • browsermedia__browsercms__0a7fb92 (5 FN) — example lib/generators/browser_cms/demo_site/templates/demo.seeds.rb:95
  • discourse__discourse__9c8f125 (4 FN) — example plugins/chat/spec/models/chat/message_spec.rb:503
  • github__octocatalog-diff__59d46a6 (3 FN) — example spec/octocatalog-diff/fixtures/repos/default/modules/stdlib/lib/puppet/parser/functions/defined_with_params.rb:19

Representative FN examples:

  • browsermedia__browsercms__0a7fb92: lib/generators/browser_cms/demo_site/templates/demo.seeds.rb:95TEMPLATE is not aligned with :template => <<-TEMPLATE.
  • browsermedia__browsercms__0a7fb92: lib/generators/browser_cms/demo_site/templates/demo.seeds.rb:157HTML is not aligned with :body => <<-HTML.
  • browsermedia__browsercms__0a7fb92: lib/generators/browser_cms/demo_site/templates/demo.seeds.rb:200HTML is not aligned with :body => <<-HTML.

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: 15 code bug(s)

FN #1: browsermedia__browsercms__0a7fb92: lib/generators/browser_cms/demo_site/templates/demo.seeds.rb:95

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

Message: ``TEMPLATEis not aligned with:template => <<-TEMPLATE`.`

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

TEMPLATE
^ Layout/ClosingHeredocIndentation: `TEMPLATE` is not aligned with `:template => <<-TEMPLATE`.

Full file context (30 lines before offense):

       65:             <p>Enter the following:</p><ul>
       66:             <li>Username: cmsadmin</li><li>Password: cmsadmin</li></ul>"
       67: create_html_block(:login, :name => "Login", :content => login, :connect_to_page_id => home_page.id, :connect_to_container => "side_bar_1")
       68: 
       69: features = "<p>Here is a list of some of the features that BrowserCMS sports.</p>
       70:                 <ul>
       71:                   <li>100% Web Based Interface</li>
       72:                   <li>Group Based Permissions</li>
       73:                   <li>Rich Text Editing</li>
       74:                   <li>Flexible Design Templates</li>
       75:                 </ul>
       76:                 "
       77: create_html_block(:features, :name => "Features", :content => features, :connect_to_page_id => home_page.id, :connect_to_container => "side_bar_2")
       78: 
       79: 
       80: create_html_block(:sidebar, :name => "Sidebar", :content => "<ul><li><a href=\"/\">Home</a></li><li><a href=\"/about\">About Us</a></li></ul>", :publish_on_save => true)
       81: create_html_block(:about_us, :name => "About Us", :content => "We are super fantastic", :publish_on_save => true)
       82: 
       83: pages(:about).create_connector(html_blocks(:about_us), "main")
       84: 
       85: create_dynamic_portlet(:recently_updated_pages,
       86:                        :name => 'Recently Updated Pages',
       87:                        :code => "@pages = Cms::Page.all(:order => 'updated_at desc', :limit => 3)",
       88:                        :template => <<-TEMPLATE
       89: <h2>Recent Updates</h2>
       90: <ul>
       91:   <% @pages.each do |page| %><li>
       92:     <%= page.name %>
       93:   </li><% end %>
       94: </ul>
>>>    95: TEMPLATE
       96: )
       97: 
       98: # Publish all Pages
       99: Cms::Page.all.each { |p| p.publish! }
      100: Cms::Link.all.each { |l| l.publish! }
      101: 
      102: 

FN #2: browsermedia__browsercms__0a7fb92: lib/generators/browser_cms/demo_site/templates/demo.seeds.rb:157

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

Message: ``HTMLis not aligned with:body => <<-HTML`.`

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

HTML
^ Layout/ClosingHeredocIndentation: `HTML` is not aligned with `:body => <<-HTML`.

Full file context (30 lines before offense):

      127:     }
      128:   </style>
      129:   </head>
      130:   <body>
      131:     <%= render :partial => "partials/header"%>
      132:       <div id="main">
      133:   <div id="broad" class="wrapper">
      134:     <%= image_tag "/themes/blue_steel/images/splash.jpg", :alt=>"Splash!", :id=>"splash"%>
      135:     <div class="main_wrapper">
      136:       <h2>Welcome to BrowserCMS 3.0</h2>
      137:       <%= container :main %>
      138:     </div>
      139:   </div>
      140:   <div id="narrow" class="wrapper">
      141:     <div class="page_highlight main_wrapper">
      142:       <h3>Login</h3>
      143:       <%= container :side_bar_1 %>
      144:     </div>
      145:     <div class="page_highlight main_wrapper">
      146:       <h3>Features</h3>
      147:       <%= container :side_bar_2 %>
      148:     </div>
      149:   </div>
      150:       </div>
      151:       <div style="clear: both;"></div>
      152:       <%= render :partial => "partials/footer" %>
      153:     </div>
      154:   </body>
      155: </html>
      156: 
>>>   157: HTML
      158: )
      159: 
      160: create_page_template(:sub_page,
      161:                      :name => "sub_page", :format => "html", :handler => "erb",
      162:                      :body => <<-HTML
      163:     <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
      164: <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">

FN #3: browsermedia__browsercms__0a7fb92: lib/generators/browser_cms/demo_site/templates/demo.seeds.rb:200

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

Message: ``HTMLis not aligned with:body => <<-HTML`.`

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

HTML
^ Layout/ClosingHeredocIndentation: `HTML` is not aligned with `:body => <<-HTML`.

Full file context (30 lines before offense):

      170:     <%= yield :html_head %>
      171:     <%= stylesheet_link_tag "/themes/blue_steel/stylesheets/style" %>
      172:   </head>
      173:   <body>
      174:       <%= render :partial => "partials/header"%>
      175:       <div id="main">
      176:         <div id="sub_nav" class="nav wrapper">
      177:           <%# Sub Menus: Developers can dynamically render menus for subsections.
      178:             # Here we show all pages/sections starting one level down from the top of the site.
      179:             # All children of the top level section will be shown here in a list. %>
      180:           <%= render_menu :from_top => 1, :depth => 2 %>
      181:         </div>
      182:         <div id="broad" class="wrapper">
      183:           <%# This demonstrates how breadcrumbs can be automatically generated by using helpers.
      184:             # The style of the breadcrumbs is controlled via CSS, which turns the ul/li into horizontal breadcrumbs with separaters. %>
      185:           <!-- Commented out as we don't have styles/design for this yet.
      186:             <%= render_breadcrumbs %>
      187:           -->
      188:           <div class="main_wrapper">
      189:             <h2><%= page_title %></h2>
      190:             <%= container :main %>
      191:           </div>
      192:         </div>
      193:       </div>
      194:       <div style="clear: both;"></div>
      195:       <%= render :partial => "partials/footer" %>
      196:     </div>
      197:   </body>
      198: </html>
      199: 
>>>   200: HTML
      201: )
      202: 
      203: 
      204: # Create partials
      205: 
      206: create_page_partial(:_footer,
      207:                     :name => "_footer", :format => "html", :handler => "erb",

FN #4: browsermedia__browsercms__0a7fb92: lib/generators/browser_cms/demo_site/templates/demo.seeds.rb:215

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

Message: ``HTMLis not aligned with:body => <<-HTML`.`

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

HTML
^ Layout/ClosingHeredocIndentation: `HTML` is not aligned with `:body => <<-HTML`.

Full file context (30 lines before offense):

      185:           <!-- Commented out as we don't have styles/design for this yet.
      186:             <%= render_breadcrumbs %>
      187:           -->
      188:           <div class="main_wrapper">
      189:             <h2><%= page_title %></h2>
      190:             <%= container :main %>
      191:           </div>
      192:         </div>
      193:       </div>
      194:       <div style="clear: both;"></div>
      195:       <%= render :partial => "partials/footer" %>
      196:     </div>
      197:   </body>
      198: </html>
      199: 
      200: HTML
      201: )
      202: 
      203: 
      204: # Create partials
      205: 
      206: create_page_partial(:_footer,
      207:                     :name => "_footer", :format => "html", :handler => "erb",
      208:                     :body => <<-HTML
      209:     <div id="footer" class="nav wrapper">
      210:   <div>
      211:     <p>&copy; 1998-2009 BrowserMedia, LLC. All Rights Reserved</p>
      212:     <a href="/footer_nav/site_map">Site Map</a><span>|</span><a href="/footer_nav/privacy_policy">Privacy Policy</a><span>|</span><a href="">Contact Us</a><span>|</span><a href="/footer_nav/our_office">Our Office</a>
      213:   </div>
      214: </div>
>>>   215: HTML
      216: )
      217: 
      218: create_page_partial(:_header,
      219:                     :name => "_header", :format => "html", :handler => "erb",
      220:                     :body => <<-HTML
      221:     <div id="content">
      222:   <div id="upper_nav" class="nav">

FN #5: browsermedia__browsercms__0a7fb92: lib/generators/browser_cms/demo_site/templates/demo.seeds.rb:246

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

Message: ``HTMLis not aligned with:body => <<-HTML`.`

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

HTML
^ Layout/ClosingHeredocIndentation: `HTML` is not aligned with `:body => <<-HTML`.

Full file context (30 lines before offense):

      216: )
      217: 
      218: create_page_partial(:_header,
      219:                     :name => "_header", :format => "html", :handler => "erb",
      220:                     :body => <<-HTML
      221:     <div id="content">
      222:   <div id="upper_nav" class="nav">
      223:     <div>
      224:       <a href="/">Home</a><span>|</span>
      225:       <a href="/util_nav/demo">Demo</a><span>|</span>
      226:       <a href="/util_nav/contact_us">Contact Us</a><span>|</span>
      227:       <a href="/util_nav/about_us">About Us</a>
      228:     </div>
      229:   </div>
      230:   <div style="clear: both;"></div>
      231:   <div id="header" class="wrapper">
      232:     <a href="/"><%= image_tag "/themes/blue_steel/images/logo.jpg", :alt=>"logo"%></a>
      233:     <div id="header_text">
      234:       <h1>BrowserCMS 3.0</h1>
      235:       <p>
      236:         The newly redesigned BrowserCMS 3.0 is an easy, affordable<br />
      237:         way to control, edit, and organize website content.
      238:       </p>
      239:     </div>
      240:   </div>
      241:   <div style="clear: both;"></div>
      242:   <div id="mid_nav" class="nav wrapper">
      243:     <%= render_menu :from_top => 0, :depth => 1%>
      244:   </div>
      245: 
>>>   246: HTML
      247: )
      248: 
      249: 

FN #6: discourse__discourse__9c8f125: plugins/chat/spec/models/chat/message_spec.rb:503

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

Full-file enclosing chain: enclosing line 497: message = > block (do..end) (line 488: it "includes links in pretty text excerpt if the raw message is a single link and the PrettyText excerpt is blank" do) > block (do..end) (line 170: describe ".cook" do) > block (do..end) (line 3: describe Chat::Message do)

Message: ``COOKEDis not aligned withcooked: <<~COOKED,`.`

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

        COOKED
^ Layout/ClosingHeredocIndentation: `COOKED` is not aligned with `cooked: <<~COOKED,`.

Full file context (30 lines before offense):

      473:           "data-id": category.id,
      474:         },
      475:       ) do
      476:         with_tag("span", with: { class: "hashtag-icon-placeholder" })
      477:       end
      478:     end
      479: 
      480:     it "supports censored plugin" do
      481:       watched_word = Fabricate(:watched_word, action: WatchedWord.actions[:censor])
      482: 
      483:       cooked = described_class.cook(watched_word.word)
      484: 
      485:       expect(cooked).to eq("<p>■■■■■</p>")
      486:     end
      487: 
      488:     it "includes links in pretty text excerpt if the raw message is a single link and the PrettyText excerpt is blank" do
      489:       message =
      490:         Fabricate.build(
      491:           :chat_message,
      492:           message: "https://twitter.com/EffinBirds/status/1518743508378697729",
      493:         )
      494:       expect(message.build_excerpt).to eq(
      495:         "https://twitter.com/EffinBirds/status/1518743508378697729",
      496:       )
      497:       message =
      498:         Fabricate.build(
      499:           :chat_message,
      500:           message: "https://twitter.com/EffinBirds/status/1518743508378697729",
      501:           cooked: <<~COOKED,
      502:           <aside class=\"onebox twitterstatus\" data-onebox-src=\"https://twitter.com/EffinBirds/status/1518743508378697729\">\n  <header class=\"source\">\n\n      <a href=\"https://twitter.com/EffinBirds/status/1518743508378697729\" target=\"_blank\" rel=\"nofollow ugc noopener\">twitter.com</a>\n  </header>\n\n  <article class=\"onebox-body\">\n    \n<h4><a href=\"https://twitter.com/EffinBirds/status/1518743508378697729\" target=\"_blank\" rel=\"nofollow ugc noopener\">Effin' Birds</a></h4>\n<div class=\"twitter-screen-name\"><a href=\"https://twitter.com/EffinBirds/status/1518743508378697729\" target=\"_blank\" rel=\"nofollow ugc noopener\">@EffinBirds</a></div>\n\n<div class=\"tweet\">\n  <span class=\"tweet-description\">https://t.co/LjlqMm9lck</span>\n</div>\n\n<div class=\"date\">\n  <a href=\"https://twitter.com/EffinBirds/status/1518743508378697729\" class=\"timestamp\" target=\"_blank\" rel=\"nofollow ugc noopener\">5:07 PM - 25 Apr 2022</a>\n\n    <span class=\"like\">\n      <svg viewbox=\"0 0 512 512\" width=\"14px\" height=\"16px\" aria-hidden=\"true\">\n        <path d=\"M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z\"></path>\n      </svg>\n      2.5K\n    </span>\n\n    <span class=\"retweet\">\n      <svg viewbox=\"0 0 640 512\" width=\"14px\" height=\"16px\" aria-hidden=\"true\">\n        <path d=\"M629.657 343.598L528.971 444.284c-9.373 9.372-24.568 9.372-33.941 0L394.343 343.598c-9.373-9.373-9.373-24.569 0-33.941l10.823-10.823c9.562-9.562 25.133-9.34 34.419.492L480 342.118V160H292.451a24.005 24.005 0 0 1-16.971-7.029l-16-16C244.361 121.851 255.069 96 276.451 96H520c13.255 0 24 10.745 24 24v222.118l40.416-42.792c9.285-9.831 24.856-10.054 34.419-.492l10.823 10.823c9.372 9.372 9.372 24.569-.001 33.941zm-265.138 15.431A23.999 23.999 0 0 0 347.548 352H160V169.881l40.416 42.792c9.286 9.831 24.856 10.054 34.419.491l10.822-10.822c9.373-9.373 9.373-24.569 0-33.941L144.971 67.716c-9.373-9.373-24.569-9.373-33.941 0L10.343 168.402c-9.373 9.373-9.373 24.569 0 33.941l10.822 10.822c9.562 9.562 25.133 9.34 34.419-.491L96 169.881V392c0 13.255 10.745 24 24 24h243.549c21.382 0 32.09-25.851 16.971-40.971l-16.001-16z\"></path>\n      </svg>\n      499\n    </span>\n</div>\n\n  </article>\n\n  <div class=\"onebox-metadata\">\n    \n    \n  </div>\n\n  <div style=\"clear: both\"></div>\n</aside>\n
>>>   503:         COOKED
      504:         )
      505:       expect(message.build_excerpt).to eq(
      506:         "https://twitter.com/EffinBirds/status/1518743508378697729",
      507:       )
      508:     end
      509: 
      510:     it "excerpts upload file name if message is empty" do

FN #7: discourse__discourse__9c8f125: spec/integration/activerecord_preventing_writes_spec.rb:15

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

Full-file enclosing chain: enclosing line 5: theme_field = > block (do..end) (line 4: it "should not result in an error response when there is a theme field that needs to be baked" do) > block (do..end) (line 3: RSpec.describe "When ActiveRecord is preventing writes" do)

Message: ``HTMLis not aligned withvalue: <<~HTML,`.`

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

      HTML
^ Layout/ClosingHeredocIndentation: `HTML` is not aligned with `value: <<~HTML,`.

Full file context (30 lines before offense):

        1: # frozen_string_literal: true
        2: 
        3: RSpec.describe "When ActiveRecord is preventing writes" do
        4:   it "should not result in an error response when there is a theme field that needs to be baked" do
        5:     theme_field =
        6:       Fabricate(
        7:         :theme_field,
        8:         type_id: ThemeField.types[:html],
        9:         target_id: Theme.targets[:common],
       10:         name: "head_tag",
       11:         value: <<~HTML,
       12:         <script type="text/discourse-plugin" version="0.1">
       13:           console.log(settings.uploads.imajee);
       14:         </script>
>>>    15:       HTML
       16:       )
       17: 
       18:     SiteSetting.default_theme_id = theme_field.theme_id
       19: 
       20:     ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role, prevent_writes: true) do
       21:       get "/latest"
       22: 

FN #8: discourse__discourse__9c8f125: spec/models/theme_spec.rb:1362

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

Full-file enclosing chain: enclosing line 1352: second_migration_field = > block (do..end) (line 1351: it "does not raise an out of sequence error and does not create ThemeSettingsMigrationrecord for out of sequence migration whenallow_out_of_sequence_migration kwarg is set to true" do) > block (do..end) (line 1091: describe "#migrate_settings" do) > enclosing line 249: HTML

Message: ``JSis not aligned withvalue: <<~JS,`.`

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

        JS
^ Layout/ClosingHeredocIndentation: `JS` is not aligned with `value: <<~JS,`.

Full file context (30 lines before offense):

     1332:           settings.delete("setting_that_will_be_removed");
     1333:           return settings;
     1334:         }
     1335:       JS
     1336:       theme.reload
     1337:       theme.migrate_settings
     1338:       theme.reload
     1339: 
     1340:       expect(theme.theme_settings.count).to eq(0)
     1341: 
     1342:       records = theme.theme_settings_migrations
     1343:       expect(records.size).to eq(1)
     1344: 
     1345:       expect(records[0].diff).to eq(
     1346:         "additions" => [],
     1347:         "deletions" => [{ "key" => "setting_that_will_be_removed", "val" => 1023 }],
     1348:       )
     1349:     end
     1350: 
     1351:     it "does not raise an out of sequence error and does not create `ThemeSettingsMigration` record for out of sequence migration when `allow_out_of_sequence_migration` kwarg is set to true" do
     1352:       second_migration_field =
     1353:         Fabricate(
     1354:           :migration_theme_field,
     1355:           name: "0001-some-other-migration-name",
     1356:           theme: theme,
     1357:           value: <<~JS,
     1358:           export default function migrate(settings) {
     1359:             settings.set("integer_setting", 3);
     1360:             return settings;
     1361:           }
>>>  1362:         JS
     1363:           version: 1,
     1364:         )
     1365: 
     1366:       expect do theme.migrate_settings end.to raise_error(
     1367:         Theme::SettingsMigrationError,
     1368:         /'0001-some-other-migration-name' is out of sequence/,
     1369:       )

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

@6 6 bot force-pushed the fix/layout-closing_heredoc_indentation-23728141741 branch from 9c844d8 to d90dd3a Compare March 30, 2026 05:00
@6 6 bot marked this pull request as ready for review March 30, 2026 05:00
@6 6 bot enabled auto-merge (squash) March 30, 2026 05:00
@github-actions
Copy link
Copy Markdown
Contributor

Cop-check results (8 shards)

Cop Baseline FP Baseline FN Local FP Local FN FP Δ FN Δ Result
Layout/ClosingHeredocIndentation 0 4 0 0 0 -4
Layout/ClosingHeredocIndentation 0 0 0 0 0 0
Layout/ClosingHeredocIndentation 0 0 0 0 0 0
Layout/ClosingHeredocIndentation 0 3 0 0 0 -3
Layout/ClosingHeredocIndentation 0 13 0 1 0 -12
Layout/ClosingHeredocIndentation 0 0 0 0 0 0
Layout/ClosingHeredocIndentation 0 6 0 0 0 -6
Layout/ClosingHeredocIndentation 0 4 0 2 0 -2

All shards passed — ready to merge.

Copy link
Copy Markdown
Owner

@6 6 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed: correctly clears argument_indent context for AssocNode values, preventing heredoc indentation exceptions from leaking into hash pair values. Resolves 27 FNs.

@6 6 bot merged commit 7e19d4f into main Mar 30, 2026
14 checks passed
@6 6 bot deleted the fix/layout-closing_heredoc_indentation-23728141741 branch March 30, 2026 05:22
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