Skip to content

Conversation

@gakonst
Copy link
Member

@gakonst gakonst commented Jan 21, 2026

Motivation

Closes #12399

When accessing a struct field after a function call with named arguments (e.g., _lzSend({...}).guid), the formatter was incorrectly placing the .field on a new line with weird indentation.

Before:

bytes32 guid =
    _lzSend({
    _dstEid: dstEid,
    ...
})
.guid;  // <-- unwanted line break

After:

bytes32 guid =
    _lzSend({
    _dstEid: dstEid,
    ...
}).guid;  // field stays on same line

Solution

Added detection for calls with named arguments in the member expression handling. When the preceding expression is a call using named arguments (the {key: value} syntax), we skip the zerobreak() that was causing the unwanted line break.

This specifically targets the pattern from the issue report while preserving existing behavior for:

  • Regular method chains (obj.method().anotherMethod())
  • Calls with positional arguments

PR Checklist

  • Added Tests (testdata/StructFieldAccess/)
  • Added Documentation (inline comments)
  • Breaking changes (none)

Fixes #12399

When accessing a struct field after a function call with named arguments
(e.g., `_lzSend({...}).guid`), the formatter was incorrectly placing
the `.field` on a new line.

The fix detects calls with named arguments and skips the zerobreak that
was causing the unwanted line break. This keeps the field access on the
same line as the closing `})`.

Example - before:
```solidity
bytes32 guid = _lzSend({
    _dstEid: dstEid,
    ...
})
    .guid;  // <-- unwanted line break
```

Example - after:
```solidity
bytes32 guid = _lzSend({
    _dstEid: dstEid,
    ...
}).guid;  // field stays on same line
```

Amp-Thread-ID: https://ampcode.com/threads/T-019bdc92-a774-749b-a831-7d9de5021e45
Co-authored-by: Amp <amp@ampcode.com>
@gakonst gakonst force-pushed the fix/fmt-struct-field-access branch from ff972ff to 2677aa9 Compare January 21, 2026 06:49
@grandizzy grandizzy marked this pull request as ready for review January 21, 2026 06:56
grandizzy
grandizzy previously approved these changes Jan 21, 2026
Copy link
Collaborator

@grandizzy grandizzy left a comment

Choose a reason for hiding this comment

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

looks good, @0xrusowsky mind to have a quick look before merge? thanks!

@gakonst
Copy link
Member Author

gakonst commented Jan 21, 2026

Hey @gakonst 👋

Could you add an extra test case for chained calls with named args to ensure the second chained call and final .guid are properly formatted?

Something like this in original.sol:

// Chained calls with named args
function e() external {
    bytes32 guid = firstCall({
        param1: value1,
        param2: value2
    }).secondCall({
        arg1: val1,
        arg2: val2
    }).guid;
}

And the corresponding expected output in fmt.sol:

// Chained calls with named args
function e() external {
    bytes32 guid = firstCall({
        param1: value1,
        param2: value2
    }).secondCall({
        arg1: val1,
        arg2: val2
    }).guid;
}

This would verify that when you have multiple chained calls each using named args, the fix works for all of them (not just the first call), and that the final .guid field access stays on the same line as the closing }).

0xrusowsky and others added 6 commits January 21, 2026 13:31
When a chain expression (like _lzSend({...}).guid) doesn't fit on one
line and requires a line break continuation, the chain's ibox may or
may not add indentation depending on whether the callee fits.

Previously, has_chain() would return true for any chain in the stack,
causing without_ind(true) to skip commasep indentation even when the
chain didn't add its own indentation. This resulted in under-indented
named args.

The fix tracks whether each chain context actually added its own
indentation via a new has_indent flag on CallContext. The new
has_chain_with_indent() method only returns true when a chain in
the stack has its own indentation, ensuring we only skip commasep
indentation when it would cause double-indentation.

Also updated test case to use a more realistic chained call pattern
that actually requires line breaks.
@grandizzy grandizzy added this pull request to the merge queue Jan 22, 2026
Merged via the queue into master with commit b940844 Jan 22, 2026
16 checks passed
@grandizzy grandizzy deleted the fix/fmt-struct-field-access branch January 22, 2026 13:00
@github-project-automation github-project-automation bot moved this to Done in Foundry Jan 22, 2026
gakonst added a commit that referenced this pull request Jan 22, 2026
…#13166)

* fix(fmt): keep struct field access on same line after named args call

Fixes #12399

When accessing a struct field after a function call with named arguments
(e.g., `_lzSend({...}).guid`), the formatter was incorrectly placing
the `.field` on a new line.

The fix detects calls with named arguments and skips the zerobreak that
was causing the unwanted line break. This keeps the field access on the
same line as the closing `})`.

Example - before:
```solidity
bytes32 guid = _lzSend({
    _dstEid: dstEid,
    ...
})
    .guid;  // <-- unwanted line break
```

Example - after:
```solidity
bytes32 guid = _lzSend({
    _dstEid: dstEid,
    ...
}).guid;  // field stays on same line
```

Amp-Thread-ID: https://ampcode.com/threads/T-019bdc92-a774-749b-a831-7d9de5021e45
Co-authored-by: Amp <amp@ampcode.com>

* test(fmt): add chained calls with named args test case

* fix(fmt): correct expected indentation in test file

* Revert "fix(fmt): correct expected indentation in test file"

This reverts commit bb80b1d.

* fix(fmt): track chain indentation to fix named args formatting

When a chain expression (like _lzSend({...}).guid) doesn't fit on one
line and requires a line break continuation, the chain's ibox may or
may not add indentation depending on whether the callee fits.

Previously, has_chain() would return true for any chain in the stack,
causing without_ind(true) to skip commasep indentation even when the
chain didn't add its own indentation. This resulted in under-indented
named args.

The fix tracks whether each chain context actually added its own
indentation via a new has_indent flag on CallContext. The new
has_chain_with_indent() method only returns true when a chain in
the stack has its own indentation, ensuring we only skip commasep
indentation when it would cause double-indentation.

Also updated test case to use a more realistic chained call pattern
that actually requires line breaks.

* chore: rustfmt

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com>
github-merge-queue bot pushed a commit that referenced this pull request Jan 22, 2026
* fix(anvil): clear stale block hashes from db cache during reorg

After anvil_reorg, the BLOCKHASH opcode was returning stale/incorrect
hashes because the block hashes stored in the database cache (used by
the EVM for the BLOCKHASH opcode) were not being cleared when blocks
were unwound during a reorg.

The fix adds a remove_block_hash method to the Db trait and calls it
during rollback to remove the stale block hashes for unwound blocks.
When new blocks are mined after the reorg, do_mine_block correctly
inserts the new block hashes.

Fixes #13165

Amp-Thread-ID: https://ampcode.com/threads/T-019be58a-cfdc-770c-a9bf-5a96b31339e9
Co-authored-by: Amp <amp@ampcode.com>

* fix: correct return type access and formatting for test

* style: apply cargo +nightly fmt

* fix: remove incorrect field access on FixedBytes return type

* fix: restore preserved block hashes after clearing db cache during rollback

The previous fix only removed stale hashes but clear() wipes ALL block hashes
including valid ones. Now we collect the hashes for blocks that should be
preserved (0 to common_block.number) before clearing, then restore them after.

* fix: reorder rollback to unwind first, then restore block hashes from sparse storage

Instead of iterating 0..N blocks, we now:
1. Unwind storage first (removes unwound block hashes)
2. Clear db cache and restore accounts
3. Restore block hashes by iterating storage.hashes (sparse, only existing blocks)

* fix: collect block hashes before await to avoid holding lock across await

* chore(deps): bump to foundry-compilers v0.19.4 (#13178)

bump to v0.19.4

* fix(fmt): keep struct field access on same line after named args call (#13166)

* fix(fmt): keep struct field access on same line after named args call

Fixes #12399

When accessing a struct field after a function call with named arguments
(e.g., `_lzSend({...}).guid`), the formatter was incorrectly placing
the `.field` on a new line.

The fix detects calls with named arguments and skips the zerobreak that
was causing the unwanted line break. This keeps the field access on the
same line as the closing `})`.

Example - before:
```solidity
bytes32 guid = _lzSend({
    _dstEid: dstEid,
    ...
})
    .guid;  // <-- unwanted line break
```

Example - after:
```solidity
bytes32 guid = _lzSend({
    _dstEid: dstEid,
    ...
}).guid;  // field stays on same line
```

Amp-Thread-ID: https://ampcode.com/threads/T-019bdc92-a774-749b-a831-7d9de5021e45
Co-authored-by: Amp <amp@ampcode.com>

* test(fmt): add chained calls with named args test case

* fix(fmt): correct expected indentation in test file

* Revert "fix(fmt): correct expected indentation in test file"

This reverts commit bb80b1d.

* fix(fmt): track chain indentation to fix named args formatting

When a chain expression (like _lzSend({...}).guid) doesn't fit on one
line and requires a line break continuation, the chain's ibox may or
may not add indentation depending on whether the callee fits.

Previously, has_chain() would return true for any chain in the stack,
causing without_ind(true) to skip commasep indentation even when the
chain didn't add its own indentation. This resulted in under-indented
named args.

The fix tracks whether each chain context actually added its own
indentation via a new has_indent flag on CallContext. The new
has_chain_with_indent() method only returns true when a chain in
the stack has its own indentation, ensuring we only skip commasep
indentation when it would cause double-indentation.

Also updated test case to use a more realistic chained call pattern
that actually requires line breaks.

* chore: rustfmt

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com>

* refactor: improve rollback block hash restoration

- Remove unused remove_block_hash trait method
- Acquire db lock once during restore to reduce lock churn
- Insert account info before storage to prevent fork-mode RPC fetches
- Bound block hash restoration to last 256 blocks (EVM BLOCKHASH limit)
- Increase test reorg depth from 1 to 5 for better coverage

* test: add deep reorg test for 256-block BLOCKHASH limit

Tests a 50-block reorg on a 300+ block chain to verify:
- Block hashes within 256 window remain consistent after reorg
- BLOCKHASH returns 0 for blocks outside the 256 window

* fmt

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
Co-authored-by: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com>
Co-authored-by: zerosnacks <zerosnacks@protonmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

bug(fmt): unexpected line breaks when accessing struct fields

4 participants