Skip to content

fix(gate-14): brace style is not supposed to be an input to route reachability - #391

Merged
rubenvdlinde merged 1 commit into
mainfrom
fix/gate-14-knr-brace-signature
Aug 12, 2026
Merged

fix(gate-14): brace style is not supposed to be an input to route reachability#391
rubenvdlinde merged 1 commit into
mainfrom
fix/gate-14-knr-brace-signature

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

What

gate-14 invariant 1 buffers a method signature plus up to twelve following lines and greps the buffer for : ...Response. Both loop stop conditions are tested against the lines it reads ahead and never against the signature line itself.

Under Allman that was harmless — the next line is a lone {. Under K&R the signature line already opens the body, so nothing stops the buffer: it runs through the body, the closing brace and the next method's docblock until it hits that method's signature, and the first method inherits the second one's return type.

Measured

openconnector#1229, mid-migration to Nextcloud's coding standard:

lib/Controller/UiController.php method=__construct
  expected_route='ui#__construct' rule=missing-route

A constructor reported as an unrouted endpoint. The TemplateResponse it was credited with belongs to makeSpaResponse() eleven lines below. The identical awk over the identical file in Allman braces prints nothing — the finding was a property of brace style, and the reformat looked like it had broken routing.

Nextcloud's standard is K&R and the fleet is adopting it app by app, so every app with a __construct followed within twelve lines by a Response-returning action gets this phantom on its first reformat — roughly two dozen PRs each re-diagnosing the same thing.

The fix

Read nothing ahead once the signature line ends in {. Correct rather than merely quieter: if the body opens on the signature line then the whole signature — parameters, return type and all — is on that line. A K&R method that genuinely returns a Response still matches, on $0.

Both arms, because "stop reading ahead" could have been "skip K&R files"

New fixture route-registration/knr-braces/:

UiController K&R, __construct then a routed dashboard(): TemplateResponseui#__construct MUST NOT be reported
GadgetController K&R, run(): JSONResponse, routed nowhere → gadget#run MUST still be reported

The suite asserts exactly one finding, so the fix cannot pass by going silent.

Mutation check (HYDRA_GATES_RUNNER_UNDER_TEST)

pre-fix runner   45 passed, 2 FAILED — 2 findings, ui#__construct among them
fixed runner     47 passed, 0 failed — 1 finding, gadget#run

shellcheck clean on both changed files.

…chability

Invariant 1 asks "does this method return a Response?" by buffering the
signature line plus up to twelve following lines and grepping the buffer for
`: ...Response`. Both of the loop's stop conditions are tested against `nxt` —
the lines it reads AHEAD — and neither is ever tested against the signature
line itself.

Under Allman braces that was harmless: the very next line is a lone `{`, so
the buffer stopped after one line. Under K&R the signature line already opens
the body, nothing stops the buffer, and it runs on through the body, the
closing brace and the next method's docblock until it reaches that method's
signature. The first method then inherits the SECOND method's return type.

MEASURED on openconnector#1229, mid-migration to Nextcloud's coding standard:

  lib/Controller/UiController.php method=__construct
    expected_route='ui#__construct' rule=missing-route

A constructor, reported as an unrouted endpoint. The `TemplateResponse` it was
credited with belongs to `makeSpaResponse()` eleven lines below. Running the
identical awk over the identical file in Allman braces prints nothing — so the
finding was a property of the brace style, and the reformat looked like it had
broken routing.

That matters beyond one PR. Nextcloud's coding standard IS K&R and the fleet is
adopting it app by app, so every app with a `__construct` followed within
twelve lines by a Response-returning action receives this phantom on its first
reformat — roughly two dozen PRs, each of which has to re-diagnose it.

THE FIX reads nothing ahead once the signature line ends in `{`. That is
correct rather than merely quieter: if the body opens on the signature line
then the whole signature — parameters, return type and all — is on that line,
so there is nothing further to find. A K&R method that genuinely returns a
Response still matches, on `$0`.

THE FIXTURE CARRIES BOTH ARMS, because "stop reading ahead" could have been
written as "skip K&R files", which would retire the gate for the entire fleet:

  UiController      K&R, `__construct` then a routed `dashboard(): TemplateResponse`
                    -> ui#__construct MUST NOT be reported
  GadgetController  K&R, `run(): JSONResponse`, routed nowhere
                    -> gadget#run MUST still be reported

and the suite asserts EXACTLY ONE finding, so the fix cannot pass by going
quiet.

MUTATION-CHECKED both ways, via HYDRA_GATES_RUNNER_UNDER_TEST:

  pre-fix runner   45 passed, 2 FAILED — 2 findings, ui#__construct among them
  fixed runner     47 passed, 0 failed — 1 finding, gadget#run

shellcheck is clean on both changed files.
@rubenvdlinde
rubenvdlinde merged commit fa047f3 into main Aug 12, 2026
34 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/gate-14-knr-brace-signature branch August 12, 2026 12:46
rubenvdlinde added a commit that referenced this pull request Aug 12, 2026
…ods (#395) (#399)

* fix(gate-16): a coding-standard reformat is not a set of changed methods (#395)

The changed-method set came from a plain `git diff -U0`, so when
`nextcloud/coding-standard` moved every `{` onto its signature line, EVERY
method in a migrated app read as modified and gate-16 demanded an `@spec`
anchor on all of them. Third of this class today after gate-14 (#391) and
gate-48 (#388); this one is the delta gate, so it is the one a formatting-only
PR can genuinely regress.

`git diff -w` does not help. The brace is a TOKEN THAT MOVED LINES, not
whitespace whose width changed, so `foo()` and `foo() {` differ under `-w`.

MEASURED, seven live `chore/nextcloud-coding-standard` PRs, base = each app's
merge-base with development:

  procest#819        185 -> 0      openregister#2445  183 -> 0
  pipelinq#809       374 -> 0      shillinq#532       274 -> 0
  openconnector#1229  30 -> 0      openbuild#189       19 -> 0
  scholiq#322          6 -> 0                    total 1071 -> 0

THE NORMALISATION, applied to BOTH sides of a line-by-line comparison against
the file's own base version, and INTERSECTED with git's answer so it can only
ever remove lines from the scope:

  * brace placement, K&R vs Allman;
  * indentation and intra-line spacing, INCLUDING cast and operator spacing;
  * a trailing comma at the end of a PHP line;
  * quote style, only where the two spellings are the same string;
  * a PHP statement re-wrapped across lines — same characters, new line breaks.

DELIBERATELY NOT NORMALISED: anything inside a string literal; interpolating or
escaped literals (`"$x"` is not `'$x'`, `"\n"` is not `'\n'`); trailing commas
in JS (elision) and re-wraps in JS (ASI); a re-wrap across a `//` comment,
because inserting a break after one UNCOMMENTS what followed; and type
respellings such as `string|null` -> `?string`, which are equivalent but are on
a SIGNATURE line and would keep the list of rules growing.

EVERY RULE HAS A POSITIVE CONTROL, because a normalisation that swallowed a
real edit would be worse than the over-reporting it replaces: 17 assertions in
`NormalisationTest` pair each rule with a change that must still be seen
through it (a value, a new parameter, a string's CONTENT, an operand inside a
re-wrap), plus two end-to-end arms and a wrapper-level arm 4 whose `.knr` and
`.knr-changed` fixtures differ in exactly one character.

MUTATION-CHECKED both ways against the pre-fix checker:

  python suite   36 -> 2 failures + 17 errors   fixed: 37 passed
  shell suite    21 passed / 3 FAILED           fixed: 24 passed / 0

INDEPENDENT LAYER — PHP's own tokeniser, not this normaliser: of the 399 files
the 1071 suppressed findings named, 396 are token-identical to their base once
whitespace, comments, trailing commas and quote style are set aside and
`else if`/`elseif` plus import order are accounted for. All three exceptions are
outside the suppressed methods: openconnector's `const` -> `public const` is
class-level, and openregister's two `string|null` -> `?string` signatures are
still IN scope after normalisation — they report nothing because both methods
already carry an `@spec` tag, which is the gate working, not the gate blind.

Cost: openregister's 2616-file diff goes from 24s to 54s, one `git show` per
in-scope changed file. `--mode report`, the empty-scope contract (#361) and the
COVERAGE line are untouched.

* docs(hydra-gates): state what gate-16 counts as a change

The delta-gate section said which gates need a base and what happens without
one, but not what a delta gate treats as a change — which is the input #395
turned out to hinge on.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
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