Skip to content

feat: lay out long attributes as PER-CS 12.3 describes - #2518

Open
mreiden wants to merge 1 commit into
prettier:mainfrom
mreiden:feat/per-cs-attribute-layout
Open

mreiden wants to merge 1 commit into
prettier:mainfrom
mreiden:feat/per-cs-attribute-layout

Conversation

@mreiden

@mreiden mreiden commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Problem

A long attribute is printed like an array, breaking the #[...] block before the attribute name and indenting the whole attribute:

#[
    AsCommand(
        name: 'app:insertions:autorenew',
        description: 'Automatically applies a new IO for carriers with an IO marked auto-renew',
    ),
]
class ApplyAutoRenewInsertionCommand extends Command

PER-CS 12.3 says:

If an attribute's argument list is split into multiple lines for any reason, then: the attribute MUST be the only one in its attribute block. The attribute arguments MUST follow the same rules as defined for multiline function calls.

with the example in 12.4:

#[Complex(
    prop: 'val',
    other: 5,
)]

This is also the form Symfony, Doctrine and PhpStorm produce, so the current output fights every other tool in a project.

Change

printAttrGroup():

  • A block with one attribute is ["#[", attr, "]"], so only the argument list can break — #[AsCommand()].

  • A block with several attributes stays on one line while it fits (#[Other, Stuff]); otherwise it becomes one block per attribute (#[A] #[B] is the same declaration as #[A, B]), which is what 12.3 requires and gives the familiar one-per-line Doctrine layout:

    #[ORM\Id]
    #[ORM\GeneratedValue(strategy: "CUSTOM")]
    #[ORM\CustomIdGenerator(class: SomeVeryLongGeneratorClassName::class)]
  • Inline attributes (parameters, closures, anonymous classes) keep the existing newline after ] when they break, which puts a parameter's attribute on its own line before the parameter as 12.2 requires.

clean() flattens a node's attribute groups so AST_COMPARE=1 treats the split blocks as equivalent.

This is a formatting change: two existing snapshots (attributes, attributes-trail-comma) change from the array-like layout to the PER-CS one. The trailing comma from #1885 now lives inside the argument list, where printArgumentsList already puts it.

Tests

New tests/attributes/attributes-per-cs.php covers a long single attribute on a class, property, method, enum case and constant; a multi-attribute block that fits and one that splits; attributes on parameters in a multiline list, on closures and on an anonymous class. Full suite passes with AST_COMPARE=1; the output is idempotent and passes php -l.

🤖 Generated with Claude Code

A long attribute was printed like an array, breaking the `#[...]` block
before the attribute name:

    #[
        AsCommand(
            name: 'app:insertions:autorenew',
            description: '...',
        ),
    ]

PER-CS 12.3 says that when an attribute's argument list is split over
several lines the attribute must be the only one in its block and the
arguments follow the multiline function call rules:

    #[AsCommand(
        name: 'app:insertions:autorenew',
        description: '...',
    )]

A block with one attribute now only ever breaks inside the argument
list. A block with several attributes stays on one line while it fits;
otherwise each attribute gets a block of its own (`#[A] #[B]` is the
same declaration as `#[A, B]`), which also gives the one-attribute-per-
line layout for Doctrine-style stacks. Inline attributes (parameters,
closures, anonymous classes) keep the newline after `]` when they
break, which puts a parameter's attribute on its own line as 12.2
requires.

`clean()` flattens a node's attribute groups so the AST comparison
treats the split blocks as equivalent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant