@blumintinc/eslint-plugin-blumint 1.20.196 → 1.20.198
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +1 -1
- package/lib/rules/no-entire-object-hook-deps.js +127 -19
- package/lib/utils/fixtureTypeProgram.d.ts +37 -2
- package/lib/utils/fixtureTypeProgram.js +278 -21
- package/package.json +1 -1
- package/release-manifest.json +28 -0
package/lib/index.js
CHANGED
|
@@ -109,6 +109,51 @@ function readExhaustiveDepsDisable(comment) {
|
|
|
109
109
|
}
|
|
110
110
|
return comment.type === utils_1.AST_TOKEN_TYPES.Block ? 'file' : null;
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Whether a dependency's type is a primitive, so that narrowing it into member
|
|
114
|
+
* paths is meaningless and an unread entry is a deliberate recompute trigger
|
|
115
|
+
* rather than a stale object.
|
|
116
|
+
*
|
|
117
|
+
* why the `*Like` flags, never the bare ones: each `*Like` is the union of a
|
|
118
|
+
* primitive and its LITERAL form, and a literal type is what this repo's own
|
|
119
|
+
* fixers produce. `global-const-style` appends `as const`, which turns `0` into
|
|
120
|
+
* the numeric literal type `0`. The list used to name `StringLiteral`
|
|
121
|
+
* explicitly but `Number` and `Boolean` bare, so a string literal was primitive
|
|
122
|
+
* while `0 as const` and `true as const` were not — the same value lost its
|
|
123
|
+
* exemption the moment a sibling fixer froze it, which made this an exemption
|
|
124
|
+
* destroyed by COMPOSITION rather than a latent gap (#2238).
|
|
125
|
+
*
|
|
126
|
+
* why unions are walked: a union carries `TypeFlags.Union` and none of the
|
|
127
|
+
* flags above, so a bitmask test on the union ITSELF answers no for
|
|
128
|
+
* `'compact' | 'full'` — a string at runtime, and the shape
|
|
129
|
+
* `prefer-union-from-const-array` leaves behind. A union is primitive exactly
|
|
130
|
+
* when every constituent is, which also keeps `string | { a: number }` an
|
|
131
|
+
* object.
|
|
132
|
+
*/
|
|
133
|
+
function isPrimitiveType(type) {
|
|
134
|
+
// why the mask is built HERE and not hoisted to module scope: this module is
|
|
135
|
+
// imported by `src/index.ts`, so a module-scope `TypeFlags.X` is read the
|
|
136
|
+
// moment the plugin loads. Against a compiler that does not root-export the
|
|
137
|
+
// API, `TypeFlags` is `undefined` and the read throws — taking down every
|
|
138
|
+
// rule in the plugin, not just this one. `plugin-load-without-compiler-api`
|
|
139
|
+
// is the guard that says so.
|
|
140
|
+
const primitiveFlags = typescript_1.TypeFlags.StringLike |
|
|
141
|
+
typescript_1.TypeFlags.NumberLike |
|
|
142
|
+
typescript_1.TypeFlags.BooleanLike |
|
|
143
|
+
typescript_1.TypeFlags.BigIntLike |
|
|
144
|
+
typescript_1.TypeFlags.ESSymbolLike |
|
|
145
|
+
typescript_1.TypeFlags.Null |
|
|
146
|
+
typescript_1.TypeFlags.Undefined |
|
|
147
|
+
typescript_1.TypeFlags.Void |
|
|
148
|
+
typescript_1.TypeFlags.Never;
|
|
149
|
+
if (type.flags & primitiveFlags)
|
|
150
|
+
return true;
|
|
151
|
+
if (type.flags & typescript_1.TypeFlags.Union) {
|
|
152
|
+
const constituents = type.types;
|
|
153
|
+
return constituents.length > 0 && constituents.every(isPrimitiveType);
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
112
157
|
/** `channelGroupActive` -> `setChannelGroupActive`, `a` -> `setA`. */
|
|
113
158
|
function toSetterName(dependencyName) {
|
|
114
159
|
return `set${dependencyName.charAt(0).toUpperCase()}${dependencyName.slice(1)}`;
|
|
@@ -119,19 +164,16 @@ function isArrayOrPrimitive(checker, esTreeNode, nodeMap) {
|
|
|
119
164
|
if (!tsNode)
|
|
120
165
|
return false;
|
|
121
166
|
const type = checker.getTypeAtLocation(tsNode);
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
typescript_1.TypeFlags.Never |
|
|
133
|
-
typescript_1.TypeFlags.BigInt |
|
|
134
|
-
typescript_1.TypeFlags.ESSymbol)) {
|
|
167
|
+
// why: the `*Like` flags, never the bare ones. Each `*Like` is the union of
|
|
168
|
+
// a primitive and its LITERAL form, and a literal type is what this repo's
|
|
169
|
+
// own `global-const-style` produces — it appends `as const`, which turns
|
|
170
|
+
// `0` into the numeric literal type `0`. The list used to name
|
|
171
|
+
// `StringLiteral` explicitly but `Number` and `Boolean` bare, so a string
|
|
172
|
+
// literal was primitive while `0 as const` and `true as const` were not:
|
|
173
|
+
// the same value lost the exemption the moment a sibling fixer froze it
|
|
174
|
+
// (#2238). Enum members come in through `NumberLike`/`StringLike` for the
|
|
175
|
+
// same reason — an enum member is a primitive at runtime.
|
|
176
|
+
if (isPrimitiveType(type)) {
|
|
135
177
|
return true;
|
|
136
178
|
}
|
|
137
179
|
// Check if it's an array type
|
|
@@ -1310,6 +1352,46 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
1310
1352
|
dependencyEntryIdentifiers = entries;
|
|
1311
1353
|
return entries;
|
|
1312
1354
|
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Whether a parameter's binding comes out of a DESTRUCTURING pattern - a
|
|
1357
|
+
* destructured prop rather than a positional parameter.
|
|
1358
|
+
*
|
|
1359
|
+
* why: this is the line `noUnusedParameters` draws, measured against the
|
|
1360
|
+
* consumer's own compiler. It reports a destructured property wherever it
|
|
1361
|
+
* sits, including a rest sibling and including an `_`-prefixed one; a
|
|
1362
|
+
* positional parameter it reports only when the name does not start with
|
|
1363
|
+
* `_`. Keying on the pattern rather than on the name is therefore the
|
|
1364
|
+
* accurate test for the destructured case, and `_`-prefixing must NOT be
|
|
1365
|
+
* read as an opt-out there - tsc ignores the name inside a pattern, so
|
|
1366
|
+
* honouring it would readmit the strand on a binding that merely looks
|
|
1367
|
+
* deliberate.
|
|
1368
|
+
*
|
|
1369
|
+
* The AssignmentPattern step is carried but not reachable from the report
|
|
1370
|
+
* path: measured, the rule emits nothing at all for a DEFAULTED
|
|
1371
|
+
* destructured prop (`({ label, revision = 0 })`), so the fixer never gets
|
|
1372
|
+
* to judge one. It stays because dropping it would classify such a prop as
|
|
1373
|
+
* positional the moment that reporting gap is closed, which is the strand
|
|
1374
|
+
* this function exists to prevent - not because a fixture exercises it.
|
|
1375
|
+
*/
|
|
1376
|
+
function isDestructuredParameter(name) {
|
|
1377
|
+
if (name.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
1378
|
+
return false;
|
|
1379
|
+
let node = name.parent;
|
|
1380
|
+
while (node) {
|
|
1381
|
+
if (node.type === utils_1.AST_NODE_TYPES.ObjectPattern ||
|
|
1382
|
+
node.type === utils_1.AST_NODE_TYPES.ArrayPattern) {
|
|
1383
|
+
return true;
|
|
1384
|
+
}
|
|
1385
|
+
if (node.type === utils_1.AST_NODE_TYPES.Property ||
|
|
1386
|
+
node.type === utils_1.AST_NODE_TYPES.RestElement ||
|
|
1387
|
+
node.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
|
|
1388
|
+
node = node.parent;
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
return false;
|
|
1392
|
+
}
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1313
1395
|
/**
|
|
1314
1396
|
* Whether removing `element`'s binding from every dependency array that
|
|
1315
1397
|
* lists it would leave the binding with no reader in the file.
|
|
@@ -1336,11 +1418,35 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
1336
1418
|
* inference is safe in the conservative direction — if a sibling report is
|
|
1337
1419
|
* suppressed the cost is a withheld fix, never a dangling reference.
|
|
1338
1420
|
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
1342
|
-
*
|
|
1343
|
-
*
|
|
1421
|
+
* A DESTRUCTURED parameter is not exempt; a positional one still is.
|
|
1422
|
+
*
|
|
1423
|
+
* why: the instrument that covers a parameter is `noUnusedParameters`, not
|
|
1424
|
+
* `noUnusedLocals`, and the consumer sets `noUnusedParameters: true` while
|
|
1425
|
+
* setting `noUnusedLocals: false` — so a blanket parameter exemption reads
|
|
1426
|
+
* the one flag the consumer has turned OFF and misses the one it has turned
|
|
1427
|
+
* ON. It also cites `no-unused-vars` with `args: 'none'`, which is not the
|
|
1428
|
+
* consumer's setting either. Stranding a destructured prop is therefore a
|
|
1429
|
+
* red build there, from a `tsc --noEmit` gate, and `no-unused-props` cannot
|
|
1430
|
+
* clean up after this fixer because that rule is report-only.
|
|
1431
|
+
*
|
|
1432
|
+
* Nearly every dependency entry in a React component is a destructured
|
|
1433
|
+
* prop, so this is the common case rather than an edge: 21 composed
|
|
1434
|
+
* findings over 13 distinct fixture shapes, every one of them a
|
|
1435
|
+
* destructured prop this fixer stranded (#2236).
|
|
1436
|
+
*
|
|
1437
|
+
* The POSITIONAL parameter stays exempt, and deliberately so. tsc reports
|
|
1438
|
+
* one too, so this IS a residue — but the composed sweep over 23,785
|
|
1439
|
+
* fixtures reached zero of them on its own, and declining there would
|
|
1440
|
+
* settle the reporting question #1621 defers on unmeasured ground while
|
|
1441
|
+
* withholding fixes the corpus shows to be safe, including the #2208
|
|
1442
|
+
* margin-comment arm whose subject is a positional parameter. The residue
|
|
1443
|
+
* is carried deliberately and it is WITNESSED: the control fixture added
|
|
1444
|
+
* with this fix is now the sweep's only surviving stranded parameter, so
|
|
1445
|
+
* the cost of the exemption is visible in that guard's dump rather than
|
|
1446
|
+
* asserted here and forgotten.
|
|
1447
|
+
*
|
|
1448
|
+
* The report stands either way. Only the rewrite is withheld, which is the
|
|
1449
|
+
* conservative direction the rest of this function already takes.
|
|
1344
1450
|
*/
|
|
1345
1451
|
function wouldStrandBinding(element) {
|
|
1346
1452
|
const identifier = unwrapExpression(element);
|
|
@@ -1349,7 +1455,9 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
1349
1455
|
const variable = resolveBinding(identifier);
|
|
1350
1456
|
if (!variable || variable.defs.length === 0)
|
|
1351
1457
|
return false;
|
|
1352
|
-
|
|
1458
|
+
const parameterDefs = variable.defs.filter((def) => def.type === 'Parameter');
|
|
1459
|
+
if (parameterDefs.length === variable.defs.length &&
|
|
1460
|
+
!parameterDefs.some((def) => isDestructuredParameter(def.name))) {
|
|
1353
1461
|
return false;
|
|
1354
1462
|
}
|
|
1355
1463
|
const entries = collectDependencyEntryIdentifiers();
|
|
@@ -256,8 +256,39 @@ export type DiagnosticsFn = (before: string[], after: string[]) => string[];
|
|
|
256
256
|
* they run now match `tsconfig.json`.
|
|
257
257
|
*/
|
|
258
258
|
export declare const introducedDiagnosticsIgnoringUnused: DiagnosticsFn;
|
|
259
|
+
export declare const multisetIntersect: (lists: string[][]) => string[];
|
|
260
|
+
/** The `TS####` prefix a corpus diagnostic is built with in `compileCorpus`. */
|
|
261
|
+
export declare const codeOf: (diagnostic: string) => string;
|
|
262
|
+
export declare const canonicalizeDiagnostic: (diagnostic: string) => string;
|
|
263
|
+
export type DiagnosticIntersection = {
|
|
264
|
+
/** The shared multiset, carrying the FIRST list's original message strings. */
|
|
265
|
+
common: string[];
|
|
266
|
+
/**
|
|
267
|
+
* Everything ANY mode saw that the intersection did not keep - what the
|
|
268
|
+
* oracle silenced. Taken over every list, not just the first: the artifact
|
|
269
|
+
* class this discount exists for is the STRICT-only diagnostic, which never
|
|
270
|
+
* appears in the default mode's list and so is invisible to a counter read
|
|
271
|
+
* off `lists[0]` alone.
|
|
272
|
+
*/
|
|
273
|
+
dropped: string[];
|
|
274
|
+
/**
|
|
275
|
+
* The subset of `dropped` that a code-only intersection would have KEPT: the
|
|
276
|
+
* TS code is present in every list with the multiplicity to match, and only
|
|
277
|
+
* the message text diverged. A genuinely mode-specific diagnostic is not in
|
|
278
|
+
* here, so this counter isolates exactly the silent-divergence failure and a
|
|
279
|
+
* guard can assert it to zero.
|
|
280
|
+
*
|
|
281
|
+
* Measured zero across all three consuming guards. If one ever appears, the
|
|
282
|
+
* remedy is to extend `canonicalizeDiagnostic` when it is another print-order
|
|
283
|
+
* divergence, or to record that one shape by name in the guard's own baseline
|
|
284
|
+
* when the two modes genuinely produce different diagnostics under the same
|
|
285
|
+
* TS code. Widening the comparison back toward the code alone is not a
|
|
286
|
+
* remedy: it resumes silencing, which is the defect.
|
|
287
|
+
*/
|
|
288
|
+
codeMatchedDrops: string[];
|
|
289
|
+
};
|
|
259
290
|
/**
|
|
260
|
-
* The multiset every list shares.
|
|
291
|
+
* The multiset every list shares, with an account of what it discarded.
|
|
261
292
|
*
|
|
262
293
|
* This is the mode discount, and it is the one place the cross-corpus oracles
|
|
263
294
|
* deliberately differ from `fixer-type-safety`'s. That guard UNIONS the
|
|
@@ -286,8 +317,12 @@ export declare const introducedDiagnosticsIgnoringUnused: DiagnosticsFn;
|
|
|
286
317
|
* The intersection only bites where both modes could judge. A pair whose input
|
|
287
318
|
* compiles under one mode only has a single-element intersection, so for it
|
|
288
319
|
* this is identical to the union.
|
|
320
|
+
*
|
|
321
|
+
* Because dropping is how this oracle produces a clean, every drop is counted
|
|
322
|
+
* rather than discarded in silence, and `codeMatchedDrops` separates "the modes
|
|
323
|
+
* disagree" from "the modes agree and the message merely printed differently".
|
|
289
324
|
*/
|
|
290
|
-
export declare const
|
|
325
|
+
export declare const intersectDiagnostics: (lists: string[][]) => DiagnosticIntersection;
|
|
291
326
|
/**
|
|
292
327
|
* `ts.createProgram` SILENTLY drops a root file whose name it does not
|
|
293
328
|
* recognize as TypeScript: `corpus.ts-7` is filtered out with no diagnostic at
|
|
@@ -26,7 +26,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
26
26
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
27
|
};
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.DECLARES_INTO_SHARED_SCOPE = exports.withSuffix = exports.multisetIntersect = exports.introducedDiagnosticsIgnoringUnused = exports.introducedDiagnostics = exports.isFragmentArtifact = exports.isUnusedDeclaration = exports.missingNameOf = exports.UNRESOLVED_NAME = exports.multisetDiff = exports.compileCorpus = exports.MODES = exports.STUBS = exports.SUBSTITUTION_PARTNER_STUBS = exports.FIRESTORE_CLIENT_STUB = exports.FIRESTORE_ADMIN_STUB = exports.FIRESTORE_COMMON = exports.TIMESTAMP_ADMIN = exports.REACT_STUB = void 0;
|
|
29
|
+
exports.DECLARES_INTO_SHARED_SCOPE = exports.withSuffix = exports.intersectDiagnostics = exports.canonicalizeDiagnostic = exports.codeOf = exports.multisetIntersect = exports.introducedDiagnosticsIgnoringUnused = exports.introducedDiagnostics = exports.isFragmentArtifact = exports.isUnusedDeclaration = exports.missingNameOf = exports.UNRESOLVED_NAME = exports.multisetDiff = exports.compileCorpus = exports.MODES = exports.STUBS = exports.SUBSTITUTION_PARTNER_STUBS = exports.FIRESTORE_CLIENT_STUB = exports.FIRESTORE_ADMIN_STUB = exports.FIRESTORE_COMMON = exports.TIMESTAMP_ADMIN = exports.REACT_STUB = void 0;
|
|
30
30
|
const path_1 = __importDefault(require("path"));
|
|
31
31
|
const ts = __importStar(require("typescript"));
|
|
32
32
|
/**
|
|
@@ -725,8 +725,266 @@ exports.introducedDiagnostics = introducedDiagnostics;
|
|
|
725
725
|
*/
|
|
726
726
|
const introducedDiagnosticsIgnoringUnused = (before, after) => (0, exports.introducedDiagnostics)(before, after).filter((d) => !(0, exports.isUnusedDeclaration)(d));
|
|
727
727
|
exports.introducedDiagnosticsIgnoringUnused = introducedDiagnosticsIgnoringUnused;
|
|
728
|
+
const multisetIntersect = (lists) => intersectBy(lists, exports.canonicalizeDiagnostic);
|
|
729
|
+
exports.multisetIntersect = multisetIntersect;
|
|
730
|
+
/** The `TS####` prefix a corpus diagnostic is built with in `compileCorpus`. */
|
|
731
|
+
const codeOf = (diagnostic) => {
|
|
732
|
+
const colon = diagnostic.indexOf(':');
|
|
733
|
+
return colon < 0 ? diagnostic : diagnostic.slice(0, colon);
|
|
734
|
+
};
|
|
735
|
+
exports.codeOf = codeOf;
|
|
736
|
+
const OPENERS = '<([{';
|
|
737
|
+
const CLOSERS = '>)]}';
|
|
738
|
+
const isOpener = (char) => OPENERS.includes(char);
|
|
739
|
+
const isCloser = (char) => CLOSERS.includes(char);
|
|
740
|
+
/**
|
|
741
|
+
* Every scan below treats `=>` as ONE token. Its `>` is not a closing bracket,
|
|
742
|
+
* and letting it decrement the depth drives a function-typed member negative
|
|
743
|
+
* and splits the rest of the string in the wrong places.
|
|
744
|
+
*/
|
|
745
|
+
const skipsArrow = (text, index) => text[index] === '=' && text[index + 1] === '>';
|
|
746
|
+
/** Splits on `separator` where it sits at bracket depth zero. */
|
|
747
|
+
const splitTopLevel = (text, separator) => {
|
|
748
|
+
const parts = [];
|
|
749
|
+
let depth = 0;
|
|
750
|
+
let start = 0;
|
|
751
|
+
for (let index = 0; index < text.length; index++) {
|
|
752
|
+
const char = text[index];
|
|
753
|
+
if (skipsArrow(text, index)) {
|
|
754
|
+
index++;
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (isOpener(char))
|
|
758
|
+
depth++;
|
|
759
|
+
else if (isCloser(char))
|
|
760
|
+
depth--;
|
|
761
|
+
else if (char === separator && depth === 0) {
|
|
762
|
+
parts.push(text.slice(start, index));
|
|
763
|
+
start = index + 1;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
parts.push(text.slice(start));
|
|
767
|
+
return parts;
|
|
768
|
+
};
|
|
769
|
+
/**
|
|
770
|
+
* A bracketed group's contents are an element LIST - tuple elements, type
|
|
771
|
+
* arguments, parameters, object members - separated by `,` or `;`. Splitting
|
|
772
|
+
* one as if it were a union is how `{ a: A | B; }` canonicalizes to
|
|
773
|
+
* `{B; | a: A}`: the `|` belongs to the member's type, not to the body.
|
|
774
|
+
*/
|
|
775
|
+
const splitListElements = (text) => {
|
|
776
|
+
const parts = [];
|
|
777
|
+
const separators = [];
|
|
778
|
+
let depth = 0;
|
|
779
|
+
let start = 0;
|
|
780
|
+
for (let index = 0; index < text.length; index++) {
|
|
781
|
+
const char = text[index];
|
|
782
|
+
if (skipsArrow(text, index)) {
|
|
783
|
+
index++;
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
if (isOpener(char))
|
|
787
|
+
depth++;
|
|
788
|
+
else if (isCloser(char))
|
|
789
|
+
depth--;
|
|
790
|
+
else if ((char === ',' || char === ';') && depth === 0) {
|
|
791
|
+
parts.push(text.slice(start, index));
|
|
792
|
+
separators.push(char);
|
|
793
|
+
start = index + 1;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
parts.push(text.slice(start));
|
|
797
|
+
return { parts, separators };
|
|
798
|
+
};
|
|
799
|
+
/** Splits on a depth-zero `=>`, so `(x: A) => B | C` unions only `B | C`. */
|
|
800
|
+
const splitTopLevelArrow = (text) => {
|
|
801
|
+
const parts = [];
|
|
802
|
+
let depth = 0;
|
|
803
|
+
let start = 0;
|
|
804
|
+
for (let index = 0; index < text.length; index++) {
|
|
805
|
+
const char = text[index];
|
|
806
|
+
if (skipsArrow(text, index)) {
|
|
807
|
+
if (depth === 0) {
|
|
808
|
+
parts.push(text.slice(start, index));
|
|
809
|
+
start = index + 2;
|
|
810
|
+
}
|
|
811
|
+
index++;
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
if (isOpener(char))
|
|
815
|
+
depth++;
|
|
816
|
+
else if (isCloser(char))
|
|
817
|
+
depth--;
|
|
818
|
+
}
|
|
819
|
+
parts.push(text.slice(start));
|
|
820
|
+
return parts;
|
|
821
|
+
};
|
|
822
|
+
/** The depth-zero `:` separating a member's name from its type, or -1. */
|
|
823
|
+
const labelEnd = (text) => {
|
|
824
|
+
let depth = 0;
|
|
825
|
+
for (let index = 0; index < text.length; index++) {
|
|
826
|
+
const char = text[index];
|
|
827
|
+
if (skipsArrow(text, index)) {
|
|
828
|
+
index++;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (isOpener(char))
|
|
832
|
+
depth++;
|
|
833
|
+
else if (isCloser(char))
|
|
834
|
+
depth--;
|
|
835
|
+
else if (char === ':' && depth === 0)
|
|
836
|
+
return index;
|
|
837
|
+
}
|
|
838
|
+
return -1;
|
|
839
|
+
};
|
|
840
|
+
/** The index of the bracket closing the one at `open`, or -1 if unbalanced. */
|
|
841
|
+
const matchingBracket = (text, open) => {
|
|
842
|
+
let depth = 0;
|
|
843
|
+
for (let index = open; index < text.length; index++) {
|
|
844
|
+
const char = text[index];
|
|
845
|
+
if (skipsArrow(text, index)) {
|
|
846
|
+
index++;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (isOpener(char))
|
|
850
|
+
depth++;
|
|
851
|
+
else if (isCloser(char)) {
|
|
852
|
+
depth--;
|
|
853
|
+
if (depth === 0)
|
|
854
|
+
return index;
|
|
855
|
+
if (depth < 0)
|
|
856
|
+
return -1;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return -1;
|
|
860
|
+
};
|
|
861
|
+
/** Rewrites the contents of every bracketed group as an element list. */
|
|
862
|
+
const sortUnionsInside = (text) => {
|
|
863
|
+
let out = '';
|
|
864
|
+
let index = 0;
|
|
865
|
+
while (index < text.length) {
|
|
866
|
+
const char = text[index];
|
|
867
|
+
if (isOpener(char)) {
|
|
868
|
+
const close = matchingBracket(text, index);
|
|
869
|
+
if (close < 0) {
|
|
870
|
+
out += char;
|
|
871
|
+
index++;
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
out += char + sortUnionsList(text.slice(index + 1, close)) + text[close];
|
|
875
|
+
index = close + 1;
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
out += char;
|
|
879
|
+
index++;
|
|
880
|
+
}
|
|
881
|
+
return out;
|
|
882
|
+
};
|
|
883
|
+
const sortUnionsList = (text) => {
|
|
884
|
+
const { parts, separators } = splitListElements(text);
|
|
885
|
+
const sorted = parts.map((part) => sortUnionsMember(part.trim()));
|
|
886
|
+
return sorted.reduce((out, part, index) => index ? `${out}${separators[index - 1]} ${part}` : part, '');
|
|
887
|
+
};
|
|
888
|
+
/** `name: T` unions only `T`; the name is not a union member. */
|
|
889
|
+
const sortUnionsMember = (text) => {
|
|
890
|
+
const colon = labelEnd(text);
|
|
891
|
+
if (colon < 0)
|
|
892
|
+
return sortUnions(text);
|
|
893
|
+
return `${text.slice(0, colon)}: ${sortUnions(text.slice(colon + 1).trim())}`;
|
|
894
|
+
};
|
|
895
|
+
/**
|
|
896
|
+
* Only UNION members are reordered. Tuple elements, type arguments, parameters
|
|
897
|
+
* and object members print in DECLARATION order, which is a property of the
|
|
898
|
+
* source and stable across programs, so sorting those would erase a real
|
|
899
|
+
* difference rather than a spurious one - they are rebuilt in place.
|
|
900
|
+
*/
|
|
901
|
+
const sortUnions = (text) => {
|
|
902
|
+
const arrowParts = splitTopLevelArrow(text);
|
|
903
|
+
if (arrowParts.length > 1) {
|
|
904
|
+
return arrowParts
|
|
905
|
+
.map((part, index) => index === arrowParts.length - 1
|
|
906
|
+
? sortUnions(part.trim())
|
|
907
|
+
: sortUnionsInside(part.trim()))
|
|
908
|
+
.join(' => ');
|
|
909
|
+
}
|
|
910
|
+
if (splitListElements(text).separators.length)
|
|
911
|
+
return sortUnionsList(text);
|
|
912
|
+
const members = splitTopLevel(text, '|').map((member) => sortUnionsInside(member.trim()));
|
|
913
|
+
if (members.length === 1)
|
|
914
|
+
return members[0];
|
|
915
|
+
return [...members].sort().join(' | ');
|
|
916
|
+
};
|
|
728
917
|
/**
|
|
729
|
-
*
|
|
918
|
+
* A diagnostic message with every printed union in a canonical member order.
|
|
919
|
+
*
|
|
920
|
+
* TypeScript orders a union's members by type ID - the order the checker
|
|
921
|
+
* happened to CREATE those types in - not by anything in the source, and a
|
|
922
|
+
* type ID is per-program. Two programs over the same files can therefore print
|
|
923
|
+
* one union two ways:
|
|
924
|
+
*
|
|
925
|
+
* TS2345: ... parameter of type 'Record<string, unknown> | unknown[]'.
|
|
926
|
+
* TS2345: ... parameter of type 'unknown[] | Record<string, unknown>'.
|
|
927
|
+
*
|
|
928
|
+
* That matters because `intersectDiagnostics` is a SILENCING oracle: what it
|
|
929
|
+
* drops becomes a clean. Comparing raw messages discarded a diagnostic present
|
|
930
|
+
* in BOTH modes as strict-only, which is why `cross-fixture-fixer-type-safety`
|
|
931
|
+
* read 0 findings while `fixer-type-safety` - which unions instead of
|
|
932
|
+
* intersecting - baselines the same 4 `enforce-microdiff` TS2345 pairs (#2235).
|
|
933
|
+
*
|
|
934
|
+
* Rewriting is confined to single-quoted spans because that is where and only
|
|
935
|
+
* where TypeScript prints a type; a string-literal type nested in one is
|
|
936
|
+
* printed with double quotes, so the spans do not nest. This is a COMPARISON
|
|
937
|
+
* KEY - every diagnostic reported to a maintainer is the original string.
|
|
938
|
+
*/
|
|
939
|
+
const canonicalCache = new Map();
|
|
940
|
+
const canonicalizeDiagnostic = (diagnostic) => {
|
|
941
|
+
const cached = canonicalCache.get(diagnostic);
|
|
942
|
+
if (cached !== undefined)
|
|
943
|
+
return cached;
|
|
944
|
+
const canonical = diagnostic.replace(/'([^']*)'/g, (_match, inner) => `'${sortUnions(inner)}'`);
|
|
945
|
+
canonicalCache.set(diagnostic, canonical);
|
|
946
|
+
return canonical;
|
|
947
|
+
};
|
|
948
|
+
exports.canonicalizeDiagnostic = canonicalizeDiagnostic;
|
|
949
|
+
/** The entries of `list` that `kept` does not cover, compared by `keyOf`. */
|
|
950
|
+
const subtractBy = (kept, list, keyOf) => {
|
|
951
|
+
const counts = new Map();
|
|
952
|
+
for (const diagnostic of kept) {
|
|
953
|
+
const key = keyOf(diagnostic);
|
|
954
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
955
|
+
}
|
|
956
|
+
return list.filter((diagnostic) => {
|
|
957
|
+
const key = keyOf(diagnostic);
|
|
958
|
+
const remaining = counts.get(key) || 0;
|
|
959
|
+
if (remaining <= 0)
|
|
960
|
+
return true;
|
|
961
|
+
counts.set(key, remaining - 1);
|
|
962
|
+
return false;
|
|
963
|
+
});
|
|
964
|
+
};
|
|
965
|
+
const intersectBy = (lists, keyOf) => {
|
|
966
|
+
if (!lists.length)
|
|
967
|
+
return [];
|
|
968
|
+
let common = [...lists[0]];
|
|
969
|
+
for (const list of lists.slice(1)) {
|
|
970
|
+
const counts = new Map();
|
|
971
|
+
for (const diagnostic of list) {
|
|
972
|
+
const key = keyOf(diagnostic);
|
|
973
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
974
|
+
}
|
|
975
|
+
common = common.filter((diagnostic) => {
|
|
976
|
+
const key = keyOf(diagnostic);
|
|
977
|
+
const remaining = counts.get(key) || 0;
|
|
978
|
+
if (remaining <= 0)
|
|
979
|
+
return false;
|
|
980
|
+
counts.set(key, remaining - 1);
|
|
981
|
+
return true;
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
return common;
|
|
985
|
+
};
|
|
986
|
+
/**
|
|
987
|
+
* The multiset every list shares, with an account of what it discarded.
|
|
730
988
|
*
|
|
731
989
|
* This is the mode discount, and it is the one place the cross-corpus oracles
|
|
732
990
|
* deliberately differ from `fixer-type-safety`'s. That guard UNIONS the
|
|
@@ -755,27 +1013,26 @@ exports.introducedDiagnosticsIgnoringUnused = introducedDiagnosticsIgnoringUnuse
|
|
|
755
1013
|
* The intersection only bites where both modes could judge. A pair whose input
|
|
756
1014
|
* compiles under one mode only has a single-element intersection, so for it
|
|
757
1015
|
* this is identical to the union.
|
|
1016
|
+
*
|
|
1017
|
+
* Because dropping is how this oracle produces a clean, every drop is counted
|
|
1018
|
+
* rather than discarded in silence, and `codeMatchedDrops` separates "the modes
|
|
1019
|
+
* disagree" from "the modes agree and the message merely printed differently".
|
|
758
1020
|
*/
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
counts.set(diagnostic, remaining - 1);
|
|
773
|
-
return true;
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
return common;
|
|
1021
|
+
const intersectDiagnostics = (lists) => {
|
|
1022
|
+
const common = intersectBy(lists, exports.canonicalizeDiagnostic);
|
|
1023
|
+
const byCode = intersectBy(lists, exports.codeOf);
|
|
1024
|
+
return {
|
|
1025
|
+
common,
|
|
1026
|
+
// Compared by the CANONICAL key, like `common` itself: subtracting by raw
|
|
1027
|
+
// string would report the other mode's spelling of a KEPT diagnostic as a
|
|
1028
|
+
// drop, which is the #2235 confusion inverted.
|
|
1029
|
+
dropped: lists.flatMap((list) => subtractBy(common, list, exports.canonicalizeDiagnostic)),
|
|
1030
|
+
// Canonical-key equality implies code equality, so `byCode` contains
|
|
1031
|
+
// `common` as a multiset and the difference is exactly the divergence.
|
|
1032
|
+
codeMatchedDrops: subtractBy(common, byCode, exports.canonicalizeDiagnostic),
|
|
1033
|
+
};
|
|
777
1034
|
};
|
|
778
|
-
exports.
|
|
1035
|
+
exports.intersectDiagnostics = intersectDiagnostics;
|
|
779
1036
|
/**
|
|
780
1037
|
* `ts.createProgram` SILENTLY drops a root file whose name it does not
|
|
781
1038
|
* recognize as TypeScript: `corpus.ts-7` is filtered out with no diagnostic at
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.198",
|
|
4
|
+
"date": "2026-08-31T11:06:34.784Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "no-entire-object-hook-deps",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2238
|
|
11
|
+
],
|
|
12
|
+
"summary": "treat a literal type as the primitive it is (closes #2238)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.197",
|
|
18
|
+
"date": "2026-08-31T08:50:13.198Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "no-entire-object-hook-deps",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
2236
|
|
25
|
+
],
|
|
26
|
+
"summary": "decline a removal that strands a destructured prop (closes #2236)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.20.196",
|
|
4
32
|
"date": "2026-08-31T07:14:16.368Z",
|