@cplieger/deadset-ts 7.0.0-dev.2 → 7.0.0-dev.3
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/dist/src/analysis.js +89 -44
- package/dist/src/augmentations.js +14 -12
- package/dist/src/components.js +62 -20
- package/dist/src/configuration-files.js +5 -5
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +71 -14
- package/dist/src/convention-rows.js +6 -2
- package/dist/src/conventions.js +3 -1
- package/dist/src/dependencies.js +80 -26
- package/dist/src/element-access.js +341 -0
- package/dist/src/erased-stores.js +317 -0
- package/dist/src/findings/completion.js +46 -15
- package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/self-check.js +17 -4
- package/dist/src/findings-pass.js +6 -5
- package/dist/src/in-operands.js +30 -0
- package/dist/src/indexed-access.js +264 -125
- package/dist/src/intra-function-parts.js +25 -11
- package/dist/src/inventory.js +25 -0
- package/dist/src/keyof-comparisons.js +170 -0
- package/dist/src/kinds.js +17 -7
- package/dist/src/manifest.js +6 -16
- package/dist/src/matrix.js +20 -2
- package/dist/src/query.js +3 -0
- package/dist/src/references.js +12 -1
- package/dist/src/report.js +1 -1
- package/dist/src/reporters/sarif.js +4 -0
- package/dist/src/serialization-contract.js +43 -14
- package/dist/src/session.js +12 -7
- package/dist/src/suppress-file.js +58 -6
- package/dist/src/suppress-inline.js +13 -4
- package/dist/src/suppress.js +54 -24
- package/dist/src/unimplemented-callees.js +312 -0
- package/dist/src/value-reach.js +7 -7
- package/dist/src/verbs/analyze.js +11 -9
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +135 -52
- package/src/augmentations.ts +14 -11
- package/src/components.ts +103 -21
- package/src/configuration-files.ts +5 -4
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +81 -17
- package/src/convention-rows.ts +6 -2
- package/src/conventions.ts +10 -1
- package/src/dependencies.ts +107 -30
- package/src/element-access.ts +446 -0
- package/src/erased-stores.ts +413 -0
- package/src/file-facts.ts +2 -0
- package/src/findings/completion.ts +54 -20
- package/src/findings/dependencies-and-module-machinery.ts +1 -1
- package/src/findings/emitters.ts +1 -1
- package/src/findings/self-check.ts +18 -4
- package/src/findings-pass.ts +12 -5
- package/src/in-operands.ts +45 -0
- package/src/indexed-access.ts +303 -145
- package/src/intra-function-parts.ts +41 -16
- package/src/inventory.ts +32 -0
- package/src/keyof-comparisons.ts +211 -0
- package/src/kinds.ts +20 -7
- package/src/manifest.ts +6 -21
- package/src/matrix.ts +34 -2
- package/src/query.ts +10 -0
- package/src/references.ts +23 -1
- package/src/report.ts +3 -1
- package/src/reporters/sarif.ts +4 -0
- package/src/serialization-contract.ts +53 -14
- package/src/session.ts +34 -29
- package/src/suppress-file.ts +68 -5
- package/src/suppress-inline.ts +14 -4
- package/src/suppress.ts +63 -28
- package/src/unimplemented-callees.ts +411 -0
- package/src/value-reach.ts +11 -7
- package/src/verbs/analyze.ts +15 -9
package/src/matrix.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* and intersected, with the dead components computed once over every configuration.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { componentsOf, type Component } from "./components.ts";
|
|
7
|
+
import { componentsOf, type Component, type Positional } from "./components.ts";
|
|
8
8
|
import { graphOf, OUTSIDE, type Graph } from "./graph.ts";
|
|
9
9
|
import type { InventorySymbol } from "./inventory.ts";
|
|
10
10
|
import { byPosition } from "./position.ts";
|
|
@@ -36,6 +36,8 @@ export interface Configured {
|
|
|
36
36
|
* finding.
|
|
37
37
|
*/
|
|
38
38
|
readonly referenceOnly?: readonly string[] | undefined;
|
|
39
|
+
/** Each declaration's dependents, by id, as the configuration's inventory states them. */
|
|
40
|
+
readonly dependents?: ReadonlyMap<string, readonly string[]> | undefined;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/** The run's configurations, indexed for the sweep. */
|
|
@@ -56,6 +58,8 @@ export interface Matrix {
|
|
|
56
58
|
* whose project holds it, in the run's order.
|
|
57
59
|
*/
|
|
58
60
|
readonly heldIn: readonly (readonly string[])[];
|
|
61
|
+
/** Each declaration's dependents, by id, as the first configuration holding it states them. */
|
|
62
|
+
readonly dependents: ReadonlyMap<string, readonly string[]>;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
/** One declaration dead in every configuration that holds it. */
|
|
@@ -128,7 +132,13 @@ export function matrixOf(per: readonly Configured[]): Matrix {
|
|
|
128
132
|
const references: Reference[] = [];
|
|
129
133
|
const firstSeen = new Map<string, number>();
|
|
130
134
|
const judged = new Set<string>();
|
|
135
|
+
const dependents = new Map<string, readonly string[]>();
|
|
131
136
|
per.forEach((one, index) => {
|
|
137
|
+
for (const [id, after] of one.dependents ?? []) {
|
|
138
|
+
if (!dependents.has(id)) {
|
|
139
|
+
dependents.set(id, after);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
132
142
|
const referenceOnly = new Set(one.referenceOnly);
|
|
133
143
|
for (const symbol of one.symbols) {
|
|
134
144
|
const held = holders.get(symbol.id);
|
|
@@ -168,6 +178,7 @@ export function matrixOf(per: readonly Configured[]): Matrix {
|
|
|
168
178
|
[...new Set(per.flatMap((one) => one.supportFiles ?? []))],
|
|
169
179
|
),
|
|
170
180
|
heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
|
|
181
|
+
dependents,
|
|
171
182
|
};
|
|
172
183
|
}
|
|
173
184
|
|
|
@@ -257,14 +268,16 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
|
|
|
257
268
|
});
|
|
258
269
|
});
|
|
259
270
|
|
|
271
|
+
const liveUnder = liveAnywhere(union.symbols, per);
|
|
260
272
|
return {
|
|
261
|
-
liveUnder
|
|
273
|
+
liveUnder,
|
|
262
274
|
candidates,
|
|
263
275
|
components: componentsOf(
|
|
264
276
|
union,
|
|
265
277
|
dead.map((one, at) => one && heldBy[at] === undefined),
|
|
266
278
|
testOfDeadCode,
|
|
267
279
|
unreferencedTest,
|
|
280
|
+
positionsOf(union, matrix.dependents, liveUnder),
|
|
268
281
|
),
|
|
269
282
|
};
|
|
270
283
|
}
|
|
@@ -286,6 +299,25 @@ export function liveFilesAt(
|
|
|
286
299
|
);
|
|
287
300
|
}
|
|
288
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Each declaration's dependents by its place in the union, and per place whether the
|
|
304
|
+
* declaration is live under reachability in some configuration.
|
|
305
|
+
*/
|
|
306
|
+
function positionsOf(
|
|
307
|
+
union: Graph,
|
|
308
|
+
dependents: ReadonlyMap<string, readonly string[]>,
|
|
309
|
+
liveUnder: ReadonlyMap<string, readonly Relation[]>,
|
|
310
|
+
): Positional {
|
|
311
|
+
return {
|
|
312
|
+
dependents: union.symbols.map((symbol) =>
|
|
313
|
+
(dependents.get(symbol.id) ?? []).map((id) => union.at(id)).filter((at) => at !== OUTSIDE),
|
|
314
|
+
),
|
|
315
|
+
live: union.symbols.map(
|
|
316
|
+
(symbol) => liveUnder.get(symbol.id)?.includes("reachability") === true,
|
|
317
|
+
),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
289
321
|
/** The relations in the order a declaration's entry lists them. */
|
|
290
322
|
const RELATIONS: readonly Relation[] = ["reference-counting", "reachability"];
|
|
291
323
|
|
package/src/query.ts
CHANGED
|
@@ -205,6 +205,8 @@ export interface Queries {
|
|
|
205
205
|
targetOf(type: Type): Answer<Type>;
|
|
206
206
|
/** The constraint of a type parameter, where it declares one; none for another type. */
|
|
207
207
|
constraintOf(type: Type): Answer<Type | undefined>;
|
|
208
|
+
/** The object and the index of an indexed-access type; none for another type. */
|
|
209
|
+
indexedAccessOf(type: Type): Answer<{ readonly object: Type; readonly index: Type } | undefined>;
|
|
208
210
|
/** The symbol a type is declared by. */
|
|
209
211
|
symbolOfType(type: Type): Answer<TSSymbol | undefined>;
|
|
210
212
|
signaturesOf(type: Type, kind: SignatureKind): Answer<readonly Signature[]>;
|
|
@@ -516,6 +518,14 @@ export function queriesOf(
|
|
|
516
518
|
() => type.getConstraint(),
|
|
517
519
|
)
|
|
518
520
|
: undefined,
|
|
521
|
+
indexedAccessOf: (type) =>
|
|
522
|
+
type.isIndexedAccessType()
|
|
523
|
+
? one(
|
|
524
|
+
"getObjectType",
|
|
525
|
+
() => typeLocation(type),
|
|
526
|
+
() => ({ object: type.getObjectType(), index: type.getIndexType() }),
|
|
527
|
+
)
|
|
528
|
+
: undefined,
|
|
519
529
|
symbolOfType: (type) =>
|
|
520
530
|
one(
|
|
521
531
|
"getSymbol",
|
package/src/references.ts
CHANGED
|
@@ -43,7 +43,10 @@ import { namespaceUses } from "./namespace-objects.ts";
|
|
|
43
43
|
import { globExpression } from "./glob.ts";
|
|
44
44
|
import { declarationsByName, nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
45
45
|
import { augmentationReferences } from "./augmentations.ts";
|
|
46
|
+
import { elementAccessReferences } from "./element-access.ts";
|
|
46
47
|
import { indexedAccessReferences } from "./indexed-access.ts";
|
|
48
|
+
import { keyofComparisonReferences } from "./keyof-comparisons.ts";
|
|
49
|
+
import { unimplementedCalleeReads } from "./unimplemented-callees.ts";
|
|
47
50
|
import { moduleDeclarationReferences } from "./module-declarations.ts";
|
|
48
51
|
import { overrideReferences } from "./overrides.ts";
|
|
49
52
|
import { dirnamePath, queriedModulePath, relativePath, resolvePath } from "./paths.ts";
|
|
@@ -185,6 +188,13 @@ export interface ReferenceCost {
|
|
|
185
188
|
readonly contextualBatches: number;
|
|
186
189
|
/** Per-type lookups for the contextual uses: each distinct type's constituents, properties and symbol. */
|
|
187
190
|
readonly contextualLookups: number;
|
|
191
|
+
/**
|
|
192
|
+
* The requests the passes that read members through the checker's types make: element
|
|
193
|
+
* accesses, key comparisons, instantiated signatures and values handed to unimplemented
|
|
194
|
+
* callees. They are
|
|
195
|
+
* counted where the caller measures the client, through `requestCount`, and are 0 otherwise.
|
|
196
|
+
*/
|
|
197
|
+
readonly typedLookups: number;
|
|
188
198
|
}
|
|
189
199
|
|
|
190
200
|
/** One project's references, in position order, and what reading them cost. */
|
|
@@ -221,6 +231,8 @@ export interface ReferenceOptions {
|
|
|
221
231
|
* reference to a declaration of `held`, the target's, is recorded.
|
|
222
232
|
*/
|
|
223
233
|
readonly consumer?: ConsumerSide;
|
|
234
|
+
/** The client's running request count, where the caller measures the client. */
|
|
235
|
+
readonly requestCount?: () => number;
|
|
224
236
|
}
|
|
225
237
|
|
|
226
238
|
/**
|
|
@@ -1556,12 +1568,21 @@ export function references<Brand>(
|
|
|
1556
1568
|
}
|
|
1557
1569
|
}
|
|
1558
1570
|
|
|
1571
|
+
let typedLookups = 0;
|
|
1559
1572
|
if (consumer === undefined) {
|
|
1560
1573
|
found.push(...overrideReferences(project, held, chains, new Set(tests)));
|
|
1574
|
+
const before = options.requestCount?.() ?? 0;
|
|
1561
1575
|
found.push(...indexedAccessReferences(project, held, chains, root, new Set(tests)));
|
|
1576
|
+
found.push(...elementAccessReferences(project, held, chains, root, new Set(tests)));
|
|
1577
|
+
found.push(...keyofComparisonReferences(project, held, chains, root, new Set(tests)));
|
|
1578
|
+
found.push(...unimplementedCalleeReads(project, held, chains, root, new Set(tests)));
|
|
1579
|
+
typedLookups = (options.requestCount?.() ?? 0) - before;
|
|
1562
1580
|
found.push(...moduleDeclarationReferences(project, held, root, new Set(tests)));
|
|
1563
1581
|
}
|
|
1564
|
-
|
|
1582
|
+
const scripts = new Set(
|
|
1583
|
+
walked.filter((one) => one.file.externalModuleIndicator === undefined).map((one) => one.fileId),
|
|
1584
|
+
);
|
|
1585
|
+
found.push(...augmentationReferences(held, found, scripts));
|
|
1565
1586
|
found.sort(
|
|
1566
1587
|
(a, b) =>
|
|
1567
1588
|
byPosition(a.position, b.position) ||
|
|
@@ -1586,6 +1607,7 @@ export function references<Brand>(
|
|
|
1586
1607
|
patternLookups: destructured.cost.patternLookups,
|
|
1587
1608
|
contextualBatches: contextual.batches,
|
|
1588
1609
|
contextualLookups: contextual.lookups,
|
|
1610
|
+
typedLookups,
|
|
1589
1611
|
},
|
|
1590
1612
|
};
|
|
1591
1613
|
}
|
package/src/report.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type { StaleSuppression } from "./findings/self-check.ts";
|
|
|
19
19
|
import type { TestFileRule } from "./references.ts";
|
|
20
20
|
|
|
21
21
|
/** The version of the report schema this analyzer writes a report to. */
|
|
22
|
-
const SCHEMA_VERSION = "8.
|
|
22
|
+
const SCHEMA_VERSION = "8.1.0";
|
|
23
23
|
|
|
24
24
|
/** Every report schema version this analyzer reads: the one the Contract it implements admits. */
|
|
25
25
|
export const SCHEMA_VERSIONS_ACCEPTED: readonly string[] = [SCHEMA_VERSION];
|
|
@@ -45,6 +45,8 @@ interface WireDetails {
|
|
|
45
45
|
readonly position: WirePosition;
|
|
46
46
|
}[];
|
|
47
47
|
readonly write_positions?: readonly WirePosition[];
|
|
48
|
+
/** Set under the name-literal rule, which no finding of this analyzer meets; SARIF still maps it. */
|
|
49
|
+
readonly name_literal?: WirePosition;
|
|
48
50
|
readonly dependency_class?: string;
|
|
49
51
|
readonly mechanism?: string;
|
|
50
52
|
readonly entry?: {
|
package/src/reporters/sarif.ts
CHANGED
|
@@ -160,6 +160,10 @@ function relatedOf(found: WireFinding): readonly Related[] {
|
|
|
160
160
|
for (const at of found.details.write_positions ?? []) {
|
|
161
161
|
related.push({ label: "write", at, endLine: at.end_line });
|
|
162
162
|
}
|
|
163
|
+
if (found.details.name_literal !== undefined) {
|
|
164
|
+
const at = found.details.name_literal;
|
|
165
|
+
related.push({ label: "name", at, endLine: at.end_line });
|
|
166
|
+
}
|
|
163
167
|
for (const one of found.component.members ?? []) {
|
|
164
168
|
if (
|
|
165
169
|
declarationKey(one.ref, one.position.path) !==
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The serialization-contract class: the data members and `toJSON` of every
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* destination does, to a fixpoint. A function invoked through `Function.prototype.call`,
|
|
9
|
-
* `apply` or `bind` is the callee, given the arguments those methods pass it.
|
|
2
|
+
* The serialization-contract class: the data members and `toJSON` of every type a value's
|
|
3
|
+
* static type reaches, where the value reaches `JSON.stringify` or a call `ts.serializers`
|
|
4
|
+
* names, and `toString` too where it reaches an `unknown` or `any` parameter of a function
|
|
5
|
+
* outside the analysis or an erased property of a type the program does not declare. A
|
|
6
|
+
* function of the project handing such a parameter to a destination is one, to a fixpoint,
|
|
7
|
+
* and a function invoked through `call`, `apply` or `bind` is the callee.
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
10
|
import {
|
|
@@ -59,6 +57,7 @@ import { nodeKey, type Inventory } from "./inventory.ts";
|
|
|
59
57
|
import { renderPosition } from "./position.ts";
|
|
60
58
|
import { must } from "./query.ts";
|
|
61
59
|
import type { ProjectView } from "./session.ts";
|
|
60
|
+
import { erasedStores, type ErasedStores } from "./erased-stores.ts";
|
|
62
61
|
import { valueReach, type Reached } from "./value-reach.ts";
|
|
63
62
|
|
|
64
63
|
/** The serializer every run reads, whatever the configuration names. */
|
|
@@ -527,13 +526,20 @@ function destinations<Brand>(
|
|
|
527
526
|
return { invocation, at, mayReceive };
|
|
528
527
|
}
|
|
529
528
|
|
|
530
|
-
/** One
|
|
529
|
+
/** One value that carries classes and object types of the target into a destination. */
|
|
531
530
|
interface Flow {
|
|
532
531
|
readonly site: Node;
|
|
533
532
|
readonly classes: ReadonlySet<string>;
|
|
534
533
|
/** The members of the interfaces and object types the value carries. */
|
|
535
534
|
readonly members: readonly TSSymbol[];
|
|
536
535
|
readonly to: Destination;
|
|
536
|
+
/** What a retention names the destination by. */
|
|
537
|
+
readonly detail: string;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** What two reaches carry together. */
|
|
541
|
+
function joined(a: Reached, b: Reached): Reached {
|
|
542
|
+
return { classes: new Set([...a.classes, ...b.classes]), members: [...a.members, ...b.members] };
|
|
537
543
|
}
|
|
538
544
|
|
|
539
545
|
/** Each argument of a call that can receive one whose value carries a class of the target. */
|
|
@@ -542,8 +548,9 @@ function flowsOf<Brand>(
|
|
|
542
548
|
held: Inventory,
|
|
543
549
|
sites: readonly Site[],
|
|
544
550
|
to: Destinations,
|
|
551
|
+
stores: ErasedStores,
|
|
545
552
|
): readonly Flow[] {
|
|
546
|
-
const reach = valueReach(project, held);
|
|
553
|
+
const reach = valueReach(project, held, stores.inside);
|
|
547
554
|
const arguments_ = sites.flatMap((site) =>
|
|
548
555
|
to.mayReceive(site.call)
|
|
549
556
|
? to.invocation(site.call).arguments.flatMap((argument, index) => {
|
|
@@ -560,7 +567,7 @@ function flowsOf<Brand>(
|
|
|
560
567
|
const typeOf = new Map<Expression, Type | undefined>(
|
|
561
568
|
asked.map((one, index) => [one.value, types[index]]),
|
|
562
569
|
);
|
|
563
|
-
|
|
570
|
+
const passed = arguments_.flatMap((one) => {
|
|
564
571
|
const type = typeOf.get(one.value);
|
|
565
572
|
let reached: Reached = { classes: new Set<string>(), members: [] };
|
|
566
573
|
if (one.value.kind === SyntaxKind.ThisKeyword) {
|
|
@@ -571,12 +578,38 @@ function flowsOf<Brand>(
|
|
|
571
578
|
} else if (type !== undefined) {
|
|
572
579
|
reached = reach.reachedFrom(type);
|
|
573
580
|
}
|
|
581
|
+
for (const stored of stores.ofVariable(one.value) ?? []) {
|
|
582
|
+
reached = joined(reached, reach.reachedFrom(stored));
|
|
583
|
+
}
|
|
574
584
|
const carries = reached.classes.size > 0 || reached.members.length > 0;
|
|
575
585
|
const destination = carries ? to.at(one.site.call, one.argument) : undefined;
|
|
576
586
|
return destination === undefined
|
|
577
587
|
? []
|
|
578
|
-
: [
|
|
588
|
+
: [
|
|
589
|
+
{
|
|
590
|
+
site: one.value,
|
|
591
|
+
classes: reached.classes,
|
|
592
|
+
members: reached.members,
|
|
593
|
+
to: destination,
|
|
594
|
+
detail: `passed to ${destination.callee}`,
|
|
595
|
+
},
|
|
596
|
+
];
|
|
597
|
+
});
|
|
598
|
+
const left = stores.outside.flatMap((store) => {
|
|
599
|
+
const reached = reach.reachedFrom(store.type);
|
|
600
|
+
return reached.classes.size === 0 && reached.members.length === 0
|
|
601
|
+
? []
|
|
602
|
+
: [
|
|
603
|
+
{
|
|
604
|
+
site: store.value,
|
|
605
|
+
classes: reached.classes,
|
|
606
|
+
members: reached.members,
|
|
607
|
+
to: { reach: "conversions" as const, callee: store.property },
|
|
608
|
+
detail: `stored in the property ${store.property} of a type the program does not declare`,
|
|
609
|
+
},
|
|
610
|
+
];
|
|
579
611
|
});
|
|
612
|
+
return [...passed, ...left];
|
|
580
613
|
}
|
|
581
614
|
|
|
582
615
|
/**
|
|
@@ -589,7 +622,13 @@ export function serializationContract<Brand>(input: DetectorInput<Brand>): reado
|
|
|
589
622
|
const { project, held, targetRoot } = input;
|
|
590
623
|
const walked = walkProject(project, held);
|
|
591
624
|
const chains = aliasChains(project, held);
|
|
592
|
-
const flows = flowsOf(
|
|
625
|
+
const flows = flowsOf(
|
|
626
|
+
project,
|
|
627
|
+
held,
|
|
628
|
+
walked.sites,
|
|
629
|
+
destinations(input, chains, walked),
|
|
630
|
+
erasedStores(project),
|
|
631
|
+
);
|
|
593
632
|
if (flows.length === 0) {
|
|
594
633
|
return [];
|
|
595
634
|
}
|
|
@@ -609,7 +648,7 @@ export function serializationContract<Brand>(input: DetectorInput<Brand>): reado
|
|
|
609
648
|
const found: Evidence[] = [];
|
|
610
649
|
for (const flow of flows) {
|
|
611
650
|
const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
|
|
612
|
-
const detail =
|
|
651
|
+
const { detail } = flow;
|
|
613
652
|
for (const member of [...flow.classes].flatMap((id) => members.get(id) ?? [])) {
|
|
614
653
|
const symbol = byId.get(member.id);
|
|
615
654
|
const name = symbol === undefined ? "" : (memberComponent(symbol, byId) ?? "");
|
package/src/session.ts
CHANGED
|
@@ -28,13 +28,10 @@ import {
|
|
|
28
28
|
} from "./query.ts";
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* The compiler the analysis reads through
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* It is an interface because the run's own accounting for that lifetime is not on
|
|
36
|
-
* the client's surface: a snapshot reports whether it is disposed and not how many
|
|
37
|
-
* a client opened, so the count is observed at this seam.
|
|
31
|
+
* The compiler the analysis reads through: a client over a protocol to a separate
|
|
32
|
+
* process, so every accessor call is a round trip and a run holds one client and one
|
|
33
|
+
* snapshot. It is an interface because a snapshot reports whether it is disposed and not
|
|
34
|
+
* how many a client opened, so the run's count of them is observed at this seam.
|
|
38
35
|
*/
|
|
39
36
|
export interface Engine {
|
|
40
37
|
/**
|
|
@@ -187,14 +184,10 @@ export function openEngine(options: EngineOptions): Engine {
|
|
|
187
184
|
}
|
|
188
185
|
|
|
189
186
|
/**
|
|
190
|
-
* A node of one project's program, carried with that project's identity.
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* compiler will not exchange and a handle cannot reach another project's checker.
|
|
195
|
-
* Handles are only meaningful inside the program that produced them, and the
|
|
196
|
-
* cross-project step of the analysis compares rendered positions rather than
|
|
197
|
-
* handles.
|
|
187
|
+
* A node of one project's program, carried with that project's identity. `Brand` is a
|
|
188
|
+
* type parameter and never a value: each visitor introduces its own, so two projects'
|
|
189
|
+
* handle types do not exchange and a handle cannot reach another project's checker. The
|
|
190
|
+
* cross-project step compares rendered positions, never handles.
|
|
198
191
|
*/
|
|
199
192
|
export interface Handle<Brand> {
|
|
200
193
|
readonly node: Node;
|
|
@@ -253,14 +246,11 @@ export interface ProjectView<Brand> {
|
|
|
253
246
|
*/
|
|
254
247
|
aliasStepOf(symbol: TSSymbol): Answer<TSSymbol | undefined>;
|
|
255
248
|
/**
|
|
256
|
-
* The node one symbol's declaration handle names, read in this project's program,
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
* would answer out of whichever project reached the symbol first. The project is
|
|
262
|
-
* named here instead, and the answer becomes a handle of this project like any
|
|
263
|
-
* other. The read is local once the file has been fetched.
|
|
249
|
+
* The node one symbol's declaration handle names, read in this project's program, or
|
|
250
|
+
* `undefined` where this program does not hold that file. A symbol is shared by every
|
|
251
|
+
* project of the snapshot and remembers the one it was first seen in, so the project is
|
|
252
|
+
* named here, or the answer would come from whichever project reached the symbol first.
|
|
253
|
+
* The read is local once the file has been fetched.
|
|
264
254
|
*/
|
|
265
255
|
declarationAt(handle: NodeHandle): Handle<Brand> | undefined;
|
|
266
256
|
}
|
|
@@ -272,6 +262,13 @@ export interface ProjectView<Brand> {
|
|
|
272
262
|
*/
|
|
273
263
|
export type ProjectVisitor<Result> = <Brand>(project: ProjectView<Brand>) => Result;
|
|
274
264
|
|
|
265
|
+
/** One project a session opened: its configuration and the files of its own program. */
|
|
266
|
+
export interface OpenedFiles {
|
|
267
|
+
readonly configFile: string;
|
|
268
|
+
/** The program's source files no default library and no external library holds. */
|
|
269
|
+
readonly fileNames: () => readonly string[];
|
|
270
|
+
}
|
|
271
|
+
|
|
275
272
|
/**
|
|
276
273
|
* The source files of one program that `own` admits, in the program's order, none of
|
|
277
274
|
* them one of the compiler's default libraries.
|
|
@@ -487,10 +484,10 @@ function projectsOf(
|
|
|
487
484
|
/**
|
|
488
485
|
* Runs `visit` over every project the compiler configurations name, inside one never updated
|
|
489
486
|
* snapshot of one client, and answers in their order. A configuration `programs` describes is
|
|
490
|
-
* built as that program, or opened from the configuration it writes
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
487
|
+
* built as that program, or opened from the configuration it writes. One the snapshot holds
|
|
488
|
+
* nothing for ends the run under `"refuse"` and is named in the result under `"record"`.
|
|
489
|
+
* `first` runs over every opened project before the first visit and `last` after the last,
|
|
490
|
+
* both while the client answers; the snapshot and the client are then released.
|
|
494
491
|
*/
|
|
495
492
|
export function runSession<Result>(
|
|
496
493
|
engine: Engine,
|
|
@@ -499,6 +496,7 @@ export function runSession<Result>(
|
|
|
499
496
|
unopened: Unopened = "refuse",
|
|
500
497
|
programs: ReadonlyMap<string, WorkspaceProgram> = new Map(),
|
|
501
498
|
last: () => void = () => undefined,
|
|
499
|
+
first: (opened: readonly OpenedFiles[]) => void = () => undefined,
|
|
502
500
|
): SessionResult<Result> {
|
|
503
501
|
try {
|
|
504
502
|
const built: string[] = [];
|
|
@@ -523,7 +521,7 @@ export function runSession<Result>(
|
|
|
523
521
|
try {
|
|
524
522
|
const opened = projectsOf(snapshot, built, written);
|
|
525
523
|
const log = new UnansweredLog();
|
|
526
|
-
const
|
|
524
|
+
const views: ProjectView<unknown>[] = [];
|
|
527
525
|
const missing: string[] = [];
|
|
528
526
|
for (const configFile of configFiles) {
|
|
529
527
|
const project = opened.get(configFile);
|
|
@@ -534,8 +532,15 @@ export function runSession<Result>(
|
|
|
534
532
|
missing.push(configFile);
|
|
535
533
|
continue;
|
|
536
534
|
}
|
|
537
|
-
|
|
535
|
+
views.push(viewOf(project, configFile, engine, log));
|
|
538
536
|
}
|
|
537
|
+
first(
|
|
538
|
+
views.map((view) => ({
|
|
539
|
+
configFile: view.configFile,
|
|
540
|
+
fileNames: () => view.ownSourceFiles().map((file) => file.fileName),
|
|
541
|
+
})),
|
|
542
|
+
);
|
|
543
|
+
const projects = views.map((view) => visit(view));
|
|
539
544
|
last();
|
|
540
545
|
return {
|
|
541
546
|
projects,
|
package/src/suppress-file.ts
CHANGED
|
@@ -59,6 +59,13 @@ const BASELINE_SHAPE: Shape = {
|
|
|
59
59
|
recordWant: "a row naming code, symbol, path and reason, each a string",
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The reference scheme of this analyzer's language. A record of the other scheme is
|
|
64
|
+
* form-checked and then left to the analyzer of its language: it binds nothing here, counts
|
|
65
|
+
* in neither total and is reported under no code.
|
|
66
|
+
*/
|
|
67
|
+
const OWN_SCHEME = "ts://";
|
|
68
|
+
|
|
62
69
|
/** The closed key list of one entry or row. */
|
|
63
70
|
const RECORD_KEYS: ReadonlySet<string> = new Set(["code", "symbol", "path", "reason"]);
|
|
64
71
|
|
|
@@ -126,6 +133,9 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
|
|
|
126
133
|
if (path !== undefined && path !== "" && !isEntryPath(path)) {
|
|
127
134
|
return refuse(JSON.stringify(path), PATH_WANT);
|
|
128
135
|
}
|
|
136
|
+
if (!symbol.startsWith(OWN_SCHEME)) {
|
|
137
|
+
return { records: [], refusals: [] };
|
|
138
|
+
}
|
|
129
139
|
|
|
130
140
|
const written = {
|
|
131
141
|
code,
|
|
@@ -284,15 +294,68 @@ export function rowReason(provenance: Provenance): string {
|
|
|
284
294
|
return `recorded by ${provenance.analyzer} ${provenance.version}`;
|
|
285
295
|
}
|
|
286
296
|
|
|
297
|
+
/** A row of the other language's scheme a write keeps: the members the document held, and no other. */
|
|
298
|
+
interface KeptRow {
|
|
299
|
+
readonly code: string;
|
|
300
|
+
readonly symbol: string;
|
|
301
|
+
readonly path?: string;
|
|
302
|
+
readonly reason?: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The rows of the other language's scheme that the baseline document `text` holds, in
|
|
307
|
+
* document order and as written, which a write keeps. A document that is not a baseline,
|
|
308
|
+
* or a row in a shape no record takes, is refused rather than dropped.
|
|
309
|
+
*/
|
|
310
|
+
export function otherSchemeRows(text: string): readonly KeptRow[] {
|
|
311
|
+
const refuse = (detail: string): never => {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`${BASELINE_FILE} cannot be rewritten keeping the other language's rows: ${detail}`,
|
|
314
|
+
);
|
|
315
|
+
};
|
|
316
|
+
let parsed: unknown;
|
|
317
|
+
try {
|
|
318
|
+
parsed = JSON.parse(text);
|
|
319
|
+
} catch (error: unknown) {
|
|
320
|
+
return refuse(error instanceof Error ? error.message : String(error));
|
|
321
|
+
}
|
|
322
|
+
const held = isObject(parsed) ? parsed[BASELINE_SHAPE.array] : undefined;
|
|
323
|
+
if (!Array.isArray(held)) {
|
|
324
|
+
return refuse(BASELINE_SHAPE.documentWant);
|
|
325
|
+
}
|
|
326
|
+
return (held as readonly unknown[]).flatMap((value): KeptRow[] => {
|
|
327
|
+
try {
|
|
328
|
+
readRecord(value, documentSite(BASELINE_SHAPE), new Map(), BASELINE_SHAPE);
|
|
329
|
+
} catch (error: unknown) {
|
|
330
|
+
return refuse(error instanceof Error ? error.message : String(error));
|
|
331
|
+
}
|
|
332
|
+
// readRecord has checked every member is a string and the code and symbol are present.
|
|
333
|
+
const { code, symbol, path, reason } = value as Readonly<Record<string, string>>;
|
|
334
|
+
if (code === undefined || symbol === undefined || symbol.startsWith(OWN_SCHEME)) {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
return [
|
|
338
|
+
{
|
|
339
|
+
code,
|
|
340
|
+
symbol,
|
|
341
|
+
...(path === undefined ? {} : { path }),
|
|
342
|
+
...(reason === undefined ? {} : { reason }),
|
|
343
|
+
},
|
|
344
|
+
];
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
287
348
|
/**
|
|
288
|
-
* A baseline holding one row per finding, in the order given, which is the report's
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
349
|
+
* A baseline holding one row per finding, in the order given, which is the report's, after
|
|
350
|
+
* the other language's rows `kept`, so two analyzers writing one document in either order
|
|
351
|
+
* write the same bytes. Every recorded row carries the provenance as its reason. A finding
|
|
352
|
+
* missing a value a row needs, and an identity missing its name or version, are refused
|
|
353
|
+
* rather than written, because the grammar requires a reason on every row.
|
|
292
354
|
*/
|
|
293
355
|
export function writeBaseline(
|
|
294
356
|
findings: readonly Recorded[],
|
|
295
357
|
provenance: Provenance,
|
|
358
|
+
kept: readonly KeptRow[] = [],
|
|
296
359
|
): Iterable<string> {
|
|
297
360
|
if (provenance.analyzer === "" || provenance.version === "") {
|
|
298
361
|
throw new Error(
|
|
@@ -308,5 +371,5 @@ export function writeBaseline(
|
|
|
308
371
|
}
|
|
309
372
|
return { code: found.code, symbol: found.symbol, path: found.path, reason };
|
|
310
373
|
});
|
|
311
|
-
return jsonChunks({ description: BASELINE_DESCRIPTION, baseline }, 2);
|
|
374
|
+
return jsonChunks({ description: BASELINE_DESCRIPTION, baseline: [...kept, ...baseline] }, 2);
|
|
312
375
|
}
|
package/src/suppress-inline.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The inline directive: `deadset:ignore`, the first token of a `//` line comment, naming
|
|
3
3
|
* one or more codes and a reason, and binding to every declaration that begins on the line
|
|
4
|
-
* below it.
|
|
4
|
+
* below it, or on its own line where it trails a token there.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { SyntaxKind, type Node, type SourceFile } from "@typescript/native/unstable/ast";
|
|
@@ -211,6 +211,15 @@ function declarationLines(
|
|
|
211
211
|
return below;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Whether the comment at `offset` follows a token on its own line, so it binds the
|
|
216
|
+
* declarations that begin on that line rather than on the next.
|
|
217
|
+
*/
|
|
218
|
+
function trails(file: SourceFile, offset: number): boolean {
|
|
219
|
+
const { line } = file.getLineAndCharacterOfPosition(offset);
|
|
220
|
+
return tokenStart(file.text, file.getPositionOfLineAndCharacter(line, 0)) < offset;
|
|
221
|
+
}
|
|
222
|
+
|
|
214
223
|
/** One comment in the namespace, what the grammar made of it, and what it bound. */
|
|
215
224
|
export interface InlineDirective {
|
|
216
225
|
readonly site: Position;
|
|
@@ -242,11 +251,12 @@ export function inlineDirectives<Brand>(
|
|
|
242
251
|
const below = declarationLines(file, held, symbols);
|
|
243
252
|
for (const comment of comments) {
|
|
244
253
|
const site = renderPosition(file, targetRoot, comment.offset);
|
|
254
|
+
const target = trails(file, comment.offset) ? site.line : site.line + 1;
|
|
245
255
|
found.push({
|
|
246
256
|
site,
|
|
247
257
|
text: comment.text,
|
|
248
258
|
directive: classifyComment(comment.text),
|
|
249
|
-
bound: [...(below.get(
|
|
259
|
+
bound: [...(below.get(target) ?? [])],
|
|
250
260
|
});
|
|
251
261
|
}
|
|
252
262
|
}
|
|
@@ -254,8 +264,8 @@ export function inlineDirectives<Brand>(
|
|
|
254
264
|
}
|
|
255
265
|
|
|
256
266
|
/**
|
|
257
|
-
* The inline directives of a run's projects, each bound to the declarations
|
|
258
|
-
*
|
|
267
|
+
* The inline directives of a run's projects, each bound to the declarations the placement
|
|
268
|
+
* rule gives it, one directive binding the union of what each project holding its file
|
|
259
269
|
* declares there. A directive that lacks only its reason is one {@link Refusal} per code;
|
|
260
270
|
* any other malformed comment in the namespace ends the read with a
|
|
261
271
|
* {@link SuppressionError} naming the first one written.
|