fix(gate-16): a prettier reformat is not a set of changed methods either - #441
Merged
Conversation
`.github#395` taught gate-16 that layout is not a change, in PHP. The fleet is now adopting `@nextcloud/prettier-config`, which reformats `.js`/`.ts`/`.vue`/ `.css`/`.scss`, and the same defect is back on the frontend half: pipelinq#820 reports 468 changed methods against a `development` that reports none. #395 deliberately excluded JS trailing commas and JS re-wraps, for three real reasons — array elision, automatic semicolon insertion, and a re-wrap across a `//`. Those are the specification for the JS rules, not a reason to have none. Each is refused explicitly, with its own control. Measured: pipelinq#820 468 -> 11, shillinq#545 197 -> 1, scholiq#329 125 -> 2, openregister#2466 29 -> 1. PHP byte-identical over 3,054 real file pairs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Extends
.github#395's cosmetic-reformat normalisation from PHP to JS / TS / Vue.The defect
The fleet is adopting
@nextcloud/prettier-config(useTabs: true,semi: false,trailingComma: "all"). On pipelinq#820, with the same gate package on both sides:468 false findings on one app, blocking a 17-app rollout. The "fix" a hurried author reaches for is annotating 468 untouched methods with
@spec, which bakes the defect into the codebase for good.#395 excluded JS trailing commas and JS re-wraps for three reasons that are real: array elision, ASI, and a re-wrap across a
//. Those are the specification for how to handle JS safely, not a reason to leave it unhandled.Measured, on the live prettier PRs
Base = each app's merge-base with
development. Before =check_spec_coverage.pyatc26f9a3; after = this branch.developmentis# count=0on all four with both versions, so every number in the "before" column is attributable to the reformat and nothing else.What is normalised
The line-level key already removed whitespace outside literals and unified quote style for JS. Added:
the opening brace is kept in JS. PHP strips a trailing
{because php-cs-fixer moves it; prettier never does, and stripping it actively corrupts the comparison — a mustache split as<span>{{+x+}}</span>loses one{, a multi-lineimport {loses the brace that opens the specifier list. A}-only line was already kept, so this makes the two halves of a pair symmetric rather than adding a rule;a trailing comma, line-level and region-level, refused on any line or region carrying an elision marker;
a re-wrap — the same characters across different line breaks — with the four refusals below;
a parenthesis prettier re-printed. Unavoidable: prettier does not edit text, it re-prints the tree, so every redundant parenthesis the author wrote is gone and every one its own line-breaking needs has appeared. Both directions occur in one diff:
A pair is dropped only when the expression inside binds strictly tighter than both neighbours. Strictly, so associativity never enters the argument —
a - (b - c)and(a - b) - care both refused rather than one of them proved. The one exception is||/&&/??at equal power, where re-grouping provably preserves the value;+is deliberately not in that set, because1 + (2 + '3')is'123'and1 + 2 + '3'is'33'.The scan runs on a space-collapsed variant of each line, not the space-stripped one used to match lines. That is load-bearing:
'value' in ctxstrips to'value'inctx, whereinis indistinguishable from the tail of an identifier and its binding power of 10 would silently read as a primary's 21.What is refused, and why
Each of these costs precision and each is deliberate. When the analysis could not show the two forms were one program, it says the method changed.
[a, , b]has three entries and[a, b]has two.,,or[,anywhere in either region refuses the whole region, and the line-level rule drops at most one trailing comma, soa,,can never reduce toa;return/throw/break/continue/yield/async, and a break before++/--/=>;//anywhere in either region, including one inside a string such as a URL. Deliberately blunt: there is no parser here, and the cost of the bluntness is precision while the cost of being wrong is a changed method nobody hears about. Covers both directions — a break after a//uncomments what followed, joining onto one comments out what follows;;→ newline (semi: falseapplied to a line carrying several statements). Equating those needs the same ASI argument the guard above exists to refuse. Cost: 3 findings;as/satisfies—a || b as stringasserts onlyb, so(a || b) as stringis a different expression;axios.delete(url)is a call. Reading itsdeleteas the unary operator hands the parentheses a binding power of 14 and weldsaxios.deleteurl. Found on pipelinq'sforecastApi.jsby the residue analysis, not by a test — it is now both;a++ + banda + ++bare the same characters once the spaces are gone. Literal-aware: a CSS class calledbadge--offused to refuse every expression that mentioned it.The 15 that remain
Not zero, and I would rather say so than reach further. They are:
(await someCall({ … 17 lines … })), where the added(and its)are several opcodes apart;semi: falseon multi-statement lines (pipelinqQuestionEditor.vue);Every one is a false positive: an author is asked about a method that only moved. None is a false negative.
The anti-widening half
A gate that stops reporting is worse than one that over-reports. Every rule ships with a control proving a real change still travels through it.
JsNormalisationTest, 29 tests (unittestsubTests expand to 55 assertions), each an A-arm paired with its B-arm:[1, 2]→[1, 2,][x, y]→[x, , y], and an elision through a re-wrapreturn (…)axios.delete(…)reflowedaxios.delete(url)≠axios.deleteurl++· a re-wrap that uncomments · a re-wrap that comments out ·;→ newline · an added method entirely in scopeThe 20-pair table is the control for the whole paren canonicaliser. Each pair is the same characters apart from one parenthesis and each pair means two different things —
(a || b) && c,(a + b) * c,a - (b - c),f((a, b)),(a ? b : c) ? d : e,!(a && b),(a || b).c,(await x) ** 2,new (a.b)(),('k' in ctx) + 1,/(a)/.test(s),({ a: 1 }), and eight more. If any ever equate, gate-16 has stopped reporting a real operator-precedence bug.Shell arm 5, through
bin/hydra-gateson a real git repo, with two pre-conditions so a silent PASS cannot be mistaken for a correct one:carry + w→carry - w): gate-16 FAILS, namestotalWeights, and does not namebuildLabel,mayEditorpersistRowbeside it.ReflowedView.vue.prettierand.prettier-changeddiffer in exactly one character (cmp -l= 1 line).Mutation-checked against the pre-#435 checker, both suites:
On real data, twice — the arm that separates "468 → 11" from "the frontend half is off". One genuine one-line change committed on top of each reformat:
ProjectDetail.vue::statusLabel'Paused'→'Suspended'AdministratieSwitcher.vue::activeLabelformatLabel(active)→formatLabel(active, true)ProjectDetail.vuecarries 43 untagged in-scope methods, all reformatted. Exactly one is reported: the one that changed. (App-wide, report mode counts 1,480 untagged methods; the gate reports 12.)And at population scale, on ordinary work. Two mutated methods prove the rules do not blind those two; this asks whether the frontend half still reports what it used to across pipelinq's history. 25 commit ranges chosen for producing a non-empty frontend finding set — 756 findings in total — old checker vs new: identical on all 25. Ranges that reported nothing on both sides were skipped rather than counted; 36 of the first 40 sampled were empty-vs-empty, and counting those as agreement would have been a comparison of two blank strings.
PHP is unchanged
_is_pure_rewrapkept its PHP body byte-for-byte and JS got its own arm rather than a branch inside it. Verified two ways:(base, head)PHP file pairs from openregister's history through_substantively_changed_lines(…, is_php=True), old vs new: 0 differ;NormalisationTest's 17 PHP assertions are unchanged and pass. Two JS assertions changed, both deliberately, both documented in place:test_trailing_comma_is_not_normalised_in_javascript→..._is_normalised_in_javascript_too. The old assertion was semantically wrong:[1, 2]and[1, 2,]are both two-element arrays in every engine since ES5. The hazard gate-16 counts a brace-style reformat as a modified method: 1 finding becomes 185 on a formatting-only PR (sibling of #388/#391) #395 named is a hole, and that now has its own two controls;test_rewrap_is_not_applied_to_javascript→test_rewrap_across_an_asi_boundary_is_still_a_change_in_javascript. Same code, same assertion, renamed: it is now the control for the ASI guard rather than for the absence of the rule.Also fixed, found on the way
The line-level key stripped whitespace inside template literals —
`not installed. `read as`not installed.`. Pre-existing (_NORM_STRING_REknows'and"only) and strictly a blindness, so it is closed here rather than filed: a line carrying a backtick is now masked with the scanner, which marks quasi text as literal and${…}as the code it is.Cost
One extra scan per JS line carrying a backtick, plus the paren canonicalisation per non-matching opcode. pipelinq#820: 7.4s → 12.2s. openregister#2466: 9.5s → 17.5s. The window that reaches for the rest of a construct is bounded at 4 opcodes and 120 lines either side; raising it to 16 opcodes / 400 lines was measured to close zero further findings.
Unchanged
--mode report, the empty-scope contract (#361), theCOVERAGE:line,spec_tags_removed, and the intersection property —_drop_cosmetic_onlystill only ever removes lines from git's own answer, so nothing here can widen the scope.🤖 Generated with Claude Code