@cplieger/deadset-ts 7.0.0-dev.1 → 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 +95 -60
- package/dist/src/augmentations.js +14 -12
- package/dist/src/component-layer.js +1 -1
- package/dist/src/components.js +62 -20
- package/dist/src/configuration-files.js +230 -30
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +104 -13
- package/dist/src/convention-rows.js +10 -0
- package/dist/src/conventions.js +35 -1
- package/dist/src/dependencies.js +90 -32
- package/dist/src/discover.js +1 -1
- package/dist/src/edges.js +1 -1
- package/dist/src/element-access.js +341 -0
- package/dist/src/erased-stores.js +317 -0
- package/dist/src/exempt-classes.js +9 -22
- package/dist/src/exempt.js +3 -15
- package/dist/src/findings/completion.js +78 -16
- package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
- package/dist/src/findings/emitters.js +1 -5
- package/dist/src/findings/self-check.js +27 -10
- package/dist/src/findings-pass.js +6 -5
- package/dist/src/in-operands.js +30 -0
- package/dist/src/indexed-access.js +264 -119
- package/dist/src/intra-function-parts.js +25 -11
- package/dist/src/inventory.js +28 -12
- 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 +14 -3
- package/dist/src/report.js +4 -1
- package/dist/src/reporters/sarif.js +4 -0
- package/dist/src/roots.js +0 -1
- package/dist/src/run.js +0 -2
- package/dist/src/schema.js +1 -1
- package/dist/src/scope.js +2 -2
- package/dist/src/serialization-contract.js +43 -14
- package/dist/src/session.js +12 -8
- package/dist/src/suppress-file.js +58 -6
- package/dist/src/suppress-inline.js +14 -5
- package/dist/src/suppress.js +64 -35
- 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/dist/src/workspace-programs.js +0 -3
- package/dist/src/workspace-resolution.js +0 -2
- package/dist/src/workspace.js +0 -3
- package/jsr.json +1 -1
- package/package.json +3 -2
- package/src/analysis.ts +145 -82
- package/src/augmentations.ts +14 -11
- package/src/calls.ts +3 -3
- package/src/component-files.ts +7 -7
- package/src/component-layer.ts +2 -2
- package/src/components.ts +104 -22
- package/src/config.ts +4 -4
- package/src/configuration-files.ts +250 -31
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +135 -17
- package/src/convention-rows.ts +23 -0
- package/src/conventions.ts +53 -1
- package/src/dependencies.ts +119 -38
- package/src/discover.ts +3 -3
- package/src/edges.ts +3 -3
- package/src/element-access.ts +446 -0
- package/src/entry-points.ts +1 -1
- package/src/erased-stores.ts +413 -0
- package/src/exempt-classes.ts +10 -24
- package/src/exempt.ts +5 -29
- package/src/file-facts.ts +3 -1
- package/src/finding.ts +3 -1
- package/src/findings/completion.ts +97 -21
- package/src/findings/dependencies-and-module-machinery.ts +1 -1
- package/src/findings/emitters.ts +4 -9
- package/src/findings/self-check.ts +31 -14
- package/src/findings-pass.ts +14 -7
- package/src/graph.ts +2 -2
- package/src/html-entries.ts +1 -1
- package/src/in-operands.ts +45 -0
- package/src/indexed-access.ts +303 -137
- package/src/intra-function-parts.ts +41 -16
- package/src/inventory.ts +37 -33
- package/src/json-document.ts +1 -1
- package/src/keyof-comparisons.ts +211 -0
- package/src/kinds.ts +20 -7
- package/src/manifest.ts +7 -22
- package/src/matrix.ts +34 -2
- package/src/namespace-objects.ts +1 -1
- package/src/pnpm-workspace.ts +1 -1
- package/src/query.ts +10 -0
- package/src/references.ts +30 -7
- package/src/report.ts +11 -5
- package/src/reporters/sarif.ts +4 -0
- package/src/roots.ts +0 -3
- package/src/run.ts +0 -2
- package/src/schema.ts +2 -2
- package/src/scope.ts +2 -2
- package/src/serialization-contract.ts +53 -14
- package/src/session.ts +34 -31
- package/src/stores.ts +1 -1
- package/src/suppress-file.ts +68 -5
- package/src/suppress-inline.ts +17 -7
- package/src/suppress.ts +79 -47
- package/src/template-field.ts +1 -1
- package/src/unimplemented-callees.ts +411 -0
- package/src/value-reach.ts +12 -8
- package/src/verbs/analyze.ts +15 -9
- package/src/verbs/verb.ts +1 -4
- package/src/workspace-programs.ts +2 -9
- package/src/workspace-resolution.ts +0 -3
- package/src/workspace-run.ts +1 -1
- package/src/workspace.ts +1 -9
package/dist/src/references.js
CHANGED
|
@@ -7,7 +7,10 @@ import { namespaceUses } from "./namespace-objects.js";
|
|
|
7
7
|
import { globExpression } from "./glob.js";
|
|
8
8
|
import { declarationsByName, nodeKey } from "./inventory.js";
|
|
9
9
|
import { augmentationReferences } from "./augmentations.js";
|
|
10
|
+
import { elementAccessReferences } from "./element-access.js";
|
|
10
11
|
import { indexedAccessReferences } from "./indexed-access.js";
|
|
12
|
+
import { keyofComparisonReferences } from "./keyof-comparisons.js";
|
|
13
|
+
import { unimplementedCalleeReads } from "./unimplemented-callees.js";
|
|
11
14
|
import { moduleDeclarationReferences } from "./module-declarations.js";
|
|
12
15
|
import { overrideReferences } from "./overrides.js";
|
|
13
16
|
import { dirnamePath, queriedModulePath, relativePath, resolvePath } from "./paths.js";
|
|
@@ -816,7 +819,8 @@ export function references(project, held, targetRoot, options) {
|
|
|
816
819
|
const testFile = new Map();
|
|
817
820
|
for (const { file } of walked) {
|
|
818
821
|
const path = renderPosition(file, root, 0).path;
|
|
819
|
-
const test = patterns.some((pattern) => pattern.test(path))
|
|
822
|
+
const test = patterns.some((pattern) => pattern.test(path)) ||
|
|
823
|
+
options.testFileNames?.has(file.fileName) === true;
|
|
820
824
|
testFile.set(file, test);
|
|
821
825
|
if (test) {
|
|
822
826
|
tests.push(path);
|
|
@@ -1109,18 +1113,24 @@ export function references(project, held, targetRoot, options) {
|
|
|
1109
1113
|
}
|
|
1110
1114
|
}
|
|
1111
1115
|
}
|
|
1116
|
+
let typedLookups = 0;
|
|
1112
1117
|
if (consumer === undefined) {
|
|
1113
1118
|
found.push(...overrideReferences(project, held, chains, new Set(tests)));
|
|
1119
|
+
const before = options.requestCount?.() ?? 0;
|
|
1114
1120
|
found.push(...indexedAccessReferences(project, held, chains, root, new Set(tests)));
|
|
1121
|
+
found.push(...elementAccessReferences(project, held, chains, root, new Set(tests)));
|
|
1122
|
+
found.push(...keyofComparisonReferences(project, held, chains, root, new Set(tests)));
|
|
1123
|
+
found.push(...unimplementedCalleeReads(project, held, chains, root, new Set(tests)));
|
|
1124
|
+
typedLookups = (options.requestCount?.() ?? 0) - before;
|
|
1115
1125
|
found.push(...moduleDeclarationReferences(project, held, root, new Set(tests)));
|
|
1116
1126
|
}
|
|
1117
|
-
|
|
1127
|
+
const scripts = new Set(walked.filter((one) => one.file.externalModuleIndicator === undefined).map((one) => one.fileId));
|
|
1128
|
+
found.push(...augmentationReferences(held, found, scripts));
|
|
1118
1129
|
found.sort((a, b) => byPosition(a.position, b.position) ||
|
|
1119
1130
|
compare(a.to, b.to) ||
|
|
1120
1131
|
compare(a.from, b.from) ||
|
|
1121
1132
|
compare(a.use, b.use));
|
|
1122
1133
|
return {
|
|
1123
|
-
configFile: project.configFile,
|
|
1124
1134
|
references: consumer === undefined
|
|
1125
1135
|
? found
|
|
1126
1136
|
: found.map((reference) => ({ ...reference, consumer: consumer.id })),
|
|
@@ -1135,6 +1145,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
1135
1145
|
patternLookups: destructured.cost.patternLookups,
|
|
1136
1146
|
contextualBatches: contextual.batches,
|
|
1137
1147
|
contextualLookups: contextual.lookups,
|
|
1148
|
+
typedLookups,
|
|
1138
1149
|
},
|
|
1139
1150
|
};
|
|
1140
1151
|
}
|
package/dist/src/report.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* renders this document, so no two renderings disagree about which findings exist.
|
|
5
5
|
*/
|
|
6
6
|
/** The version of the report schema this analyzer writes a report to. */
|
|
7
|
-
const SCHEMA_VERSION = "8.
|
|
7
|
+
const SCHEMA_VERSION = "8.1.0";
|
|
8
8
|
/** Every report schema version this analyzer reads: the one the Contract it implements admits. */
|
|
9
9
|
export const SCHEMA_VERSIONS_ACCEPTED = [SCHEMA_VERSION];
|
|
10
10
|
/** The name this analyzer writes every document under and mints every identifier with. */
|
|
@@ -55,6 +55,9 @@ function wireDetails(details) {
|
|
|
55
55
|
},
|
|
56
56
|
}),
|
|
57
57
|
...(details.overlap === undefined ? {} : { overlap: details.overlap }),
|
|
58
|
+
...(details.removesLastUseOf === undefined
|
|
59
|
+
? {}
|
|
60
|
+
: { removes_last_use_of: details.removesLastUseOf }),
|
|
58
61
|
};
|
|
59
62
|
}
|
|
60
63
|
/** One finding as the finding schema spells it, member for member in its order. */
|
|
@@ -122,6 +122,10 @@ function relatedOf(found) {
|
|
|
122
122
|
for (const at of found.details.write_positions ?? []) {
|
|
123
123
|
related.push({ label: "write", at, endLine: at.end_line });
|
|
124
124
|
}
|
|
125
|
+
if (found.details.name_literal !== undefined) {
|
|
126
|
+
const at = found.details.name_literal;
|
|
127
|
+
related.push({ label: "name", at, endLine: at.end_line });
|
|
128
|
+
}
|
|
125
129
|
for (const one of found.component.members ?? []) {
|
|
126
130
|
if (declarationKey(one.ref, one.position.path) !==
|
|
127
131
|
declarationKey(found.symbol.ref, found.position.path)) {
|
package/dist/src/roots.js
CHANGED
|
@@ -268,7 +268,6 @@ export function roots(project, held, targetRoot, options) {
|
|
|
268
268
|
return site || compare(a.kind, b.kind) || compare(a.source, b.source);
|
|
269
269
|
});
|
|
270
270
|
return {
|
|
271
|
-
configFile: project.configFile,
|
|
272
271
|
liveUnderReachability: ordered,
|
|
273
272
|
unmatched,
|
|
274
273
|
unanswered: answered ? api.guessed : held.symbols.map((symbol) => symbol.id),
|
package/dist/src/run.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ConfigError, REPOSITORY_DOCUMENT } from "./config.js";
|
|
2
2
|
import { DiscoveryError, renderDiagnostic } from "./discover.js";
|
|
3
|
-
import { EMITTERS } from "./findings/emitters.js";
|
|
4
3
|
import { joinPath, resolvePath } from "./paths.js";
|
|
5
4
|
import { readScope, ScopeError, scopeForDir } from "./scope.js";
|
|
6
5
|
import { SetupError, setupLine } from "./setup-failure.js";
|
|
@@ -201,7 +200,6 @@ function invoke(verb, own, args, streams, openClient) {
|
|
|
201
200
|
});
|
|
202
201
|
return opened;
|
|
203
202
|
},
|
|
204
|
-
emitters: EMITTERS,
|
|
205
203
|
});
|
|
206
204
|
}
|
|
207
205
|
catch (error) {
|
package/dist/src/schema.js
CHANGED
|
@@ -216,7 +216,7 @@ export function malformed(label, path, detail) {
|
|
|
216
216
|
* A refusal of a key the closed key list does not declare, naming the nearest key
|
|
217
217
|
* it does.
|
|
218
218
|
*/
|
|
219
|
-
|
|
219
|
+
function unimplementedKey(label, path) {
|
|
220
220
|
const nearest = nearestKey(path);
|
|
221
221
|
let hint = "";
|
|
222
222
|
if (nearest === path) {
|
package/dist/src/scope.js
CHANGED
|
@@ -2,8 +2,8 @@ import { ConfigError } from "./config.js";
|
|
|
2
2
|
import { dirnamePath, isAbsolutePath, joinPath, resolvePath } from "./paths.js";
|
|
3
3
|
import { checkDocument } from "./schema.js";
|
|
4
4
|
/** The roles a scope document declares for a module. */
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const ROLE_TARGET = "target";
|
|
6
|
+
const ROLE_CONSUMER = "consumer";
|
|
7
7
|
/**
|
|
8
8
|
* A scope document above this length is refused rather than decoded. It names one
|
|
9
9
|
* target and its consumers, so a larger file is not one.
|
|
@@ -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
|
import { isArrayLiteralExpression, isArrowFunction, isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isSetAccessorDeclaration, isSpreadElement, isStringLiteral, isVariableDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
12
10
|
import { SignatureKind, SymbolFlags, TypeFlags, } from "@typescript/native/unstable/sync";
|
|
@@ -17,6 +15,7 @@ import { names, namesADeclaration, resolveEntries, spelledEntry, } from "./confi
|
|
|
17
15
|
import { nodeKey } from "./inventory.js";
|
|
18
16
|
import { renderPosition } from "./position.js";
|
|
19
17
|
import { must } from "./query.js";
|
|
18
|
+
import { erasedStores } from "./erased-stores.js";
|
|
20
19
|
import { valueReach } from "./value-reach.js";
|
|
21
20
|
/** The serializer every run reads, whatever the configuration names. */
|
|
22
21
|
const JSON_STRINGIFY = { shape: "global", global: "JSON.stringify" };
|
|
@@ -351,9 +350,13 @@ function destinations(input, chains, walked) {
|
|
|
351
350
|
};
|
|
352
351
|
return { invocation, at, mayReceive };
|
|
353
352
|
}
|
|
353
|
+
/** What two reaches carry together. */
|
|
354
|
+
function joined(a, b) {
|
|
355
|
+
return { classes: new Set([...a.classes, ...b.classes]), members: [...a.members, ...b.members] };
|
|
356
|
+
}
|
|
354
357
|
/** Each argument of a call that can receive one whose value carries a class of the target. */
|
|
355
|
-
function flowsOf(project, held, sites, to) {
|
|
356
|
-
const reach = valueReach(project, held);
|
|
358
|
+
function flowsOf(project, held, sites, to, stores) {
|
|
359
|
+
const reach = valueReach(project, held, stores.inside);
|
|
357
360
|
const arguments_ = sites.flatMap((site) => to.mayReceive(site.call)
|
|
358
361
|
? to.invocation(site.call).arguments.flatMap((argument, index) => {
|
|
359
362
|
const value = evaluated(argument);
|
|
@@ -363,7 +366,7 @@ function flowsOf(project, held, sites, to) {
|
|
|
363
366
|
const asked = arguments_.filter((one) => one.value.kind !== SyntaxKind.ThisKeyword);
|
|
364
367
|
const types = typesAt(project, asked.map((one) => one.value));
|
|
365
368
|
const typeOf = new Map(asked.map((one, index) => [one.value, types[index]]));
|
|
366
|
-
|
|
369
|
+
const passed = arguments_.flatMap((one) => {
|
|
367
370
|
const type = typeOf.get(one.value);
|
|
368
371
|
let reached = { classes: new Set(), members: [] };
|
|
369
372
|
if (one.value.kind === SyntaxKind.ThisKeyword) {
|
|
@@ -375,12 +378,38 @@ function flowsOf(project, held, sites, to) {
|
|
|
375
378
|
else if (type !== undefined) {
|
|
376
379
|
reached = reach.reachedFrom(type);
|
|
377
380
|
}
|
|
381
|
+
for (const stored of stores.ofVariable(one.value) ?? []) {
|
|
382
|
+
reached = joined(reached, reach.reachedFrom(stored));
|
|
383
|
+
}
|
|
378
384
|
const carries = reached.classes.size > 0 || reached.members.length > 0;
|
|
379
385
|
const destination = carries ? to.at(one.site.call, one.argument) : undefined;
|
|
380
386
|
return destination === undefined
|
|
381
387
|
? []
|
|
382
|
-
: [
|
|
388
|
+
: [
|
|
389
|
+
{
|
|
390
|
+
site: one.value,
|
|
391
|
+
classes: reached.classes,
|
|
392
|
+
members: reached.members,
|
|
393
|
+
to: destination,
|
|
394
|
+
detail: `passed to ${destination.callee}`,
|
|
395
|
+
},
|
|
396
|
+
];
|
|
397
|
+
});
|
|
398
|
+
const left = stores.outside.flatMap((store) => {
|
|
399
|
+
const reached = reach.reachedFrom(store.type);
|
|
400
|
+
return reached.classes.size === 0 && reached.members.length === 0
|
|
401
|
+
? []
|
|
402
|
+
: [
|
|
403
|
+
{
|
|
404
|
+
site: store.value,
|
|
405
|
+
classes: reached.classes,
|
|
406
|
+
members: reached.members,
|
|
407
|
+
to: { reach: "conversions", callee: store.property },
|
|
408
|
+
detail: `stored in the property ${store.property} of a type the program does not declare`,
|
|
409
|
+
},
|
|
410
|
+
];
|
|
383
411
|
});
|
|
412
|
+
return [...passed, ...left];
|
|
384
413
|
}
|
|
385
414
|
/**
|
|
386
415
|
* The serialization-contract detector: each data member and `toJSON` of each class,
|
|
@@ -392,7 +421,7 @@ export function serializationContract(input) {
|
|
|
392
421
|
const { project, held, targetRoot } = input;
|
|
393
422
|
const walked = walkProject(project, held);
|
|
394
423
|
const chains = aliasChains(project, held);
|
|
395
|
-
const flows = flowsOf(project, held, walked.sites, destinations(input, chains, walked));
|
|
424
|
+
const flows = flowsOf(project, held, walked.sites, destinations(input, chains, walked), erasedStores(project));
|
|
396
425
|
if (flows.length === 0) {
|
|
397
426
|
return [];
|
|
398
427
|
}
|
|
@@ -406,7 +435,7 @@ export function serializationContract(input) {
|
|
|
406
435
|
const found = [];
|
|
407
436
|
for (const flow of flows) {
|
|
408
437
|
const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
|
|
409
|
-
const detail =
|
|
438
|
+
const { detail } = flow;
|
|
410
439
|
for (const member of [...flow.classes].flatMap((id) => members.get(id) ?? [])) {
|
|
411
440
|
const symbol = byId.get(member.id);
|
|
412
441
|
const name = symbol === undefined ? "" : (memberComponent(symbol, byId) ?? "");
|
package/dist/src/session.js
CHANGED
|
@@ -190,7 +190,6 @@ export function ownedAs(project, own) {
|
|
|
190
190
|
/** The three diagnostic sets one project carries, read before any analysis. */
|
|
191
191
|
export function diagnosticsOf(project) {
|
|
192
192
|
return {
|
|
193
|
-
configFile: project.configFile,
|
|
194
193
|
syntactic: project.program.getSyntacticDiagnostics(),
|
|
195
194
|
semantic: project.program.getSemanticDiagnostics(),
|
|
196
195
|
configParsing: project.program.getConfigFileParsingDiagnostics(),
|
|
@@ -228,12 +227,12 @@ function projectsOf(snapshot, built, written) {
|
|
|
228
227
|
/**
|
|
229
228
|
* Runs `visit` over every project the compiler configurations name, inside one never updated
|
|
230
229
|
* snapshot of one client, and answers in their order. A configuration `programs` describes is
|
|
231
|
-
* built as that program, or opened from the configuration it writes
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
230
|
+
* built as that program, or opened from the configuration it writes. One the snapshot holds
|
|
231
|
+
* nothing for ends the run under `"refuse"` and is named in the result under `"record"`.
|
|
232
|
+
* `first` runs over every opened project before the first visit and `last` after the last,
|
|
233
|
+
* both while the client answers; the snapshot and the client are then released.
|
|
235
234
|
*/
|
|
236
|
-
export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map(), last = () => undefined) {
|
|
235
|
+
export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map(), last = () => undefined, first = () => undefined) {
|
|
237
236
|
try {
|
|
238
237
|
const built = [];
|
|
239
238
|
const params = [];
|
|
@@ -254,7 +253,7 @@ export function runSession(engine, configFiles, visit, unopened = "refuse", prog
|
|
|
254
253
|
try {
|
|
255
254
|
const opened = projectsOf(snapshot, built, written);
|
|
256
255
|
const log = new UnansweredLog();
|
|
257
|
-
const
|
|
256
|
+
const views = [];
|
|
258
257
|
const missing = [];
|
|
259
258
|
for (const configFile of configFiles) {
|
|
260
259
|
const project = opened.get(configFile);
|
|
@@ -265,8 +264,13 @@ export function runSession(engine, configFiles, visit, unopened = "refuse", prog
|
|
|
265
264
|
missing.push(configFile);
|
|
266
265
|
continue;
|
|
267
266
|
}
|
|
268
|
-
|
|
267
|
+
views.push(viewOf(project, configFile, engine, log));
|
|
269
268
|
}
|
|
269
|
+
first(views.map((view) => ({
|
|
270
|
+
configFile: view.configFile,
|
|
271
|
+
fileNames: () => view.ownSourceFiles().map((file) => file.fileName),
|
|
272
|
+
})));
|
|
273
|
+
const projects = views.map((view) => visit(view));
|
|
270
274
|
last();
|
|
271
275
|
return {
|
|
272
276
|
projects,
|
|
@@ -27,6 +27,12 @@ const BASELINE_SHAPE = {
|
|
|
27
27
|
documentWant: "an object carrying the baseline array, and an optional description",
|
|
28
28
|
recordWant: "a row naming code, symbol, path and reason, each a string",
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* The reference scheme of this analyzer's language. A record of the other scheme is
|
|
32
|
+
* form-checked and then left to the analyzer of its language: it binds nothing here, counts
|
|
33
|
+
* in neither total and is reported under no code.
|
|
34
|
+
*/
|
|
35
|
+
const OWN_SCHEME = "ts://";
|
|
30
36
|
/** The closed key list of one entry or row. */
|
|
31
37
|
const RECORD_KEYS = new Set(["code", "symbol", "path", "reason"]);
|
|
32
38
|
const CODE_WANT = "DS followed by four digits";
|
|
@@ -80,6 +86,9 @@ function readRecord(value, site, byReference, shape) {
|
|
|
80
86
|
if (path !== undefined && path !== "" && !isEntryPath(path)) {
|
|
81
87
|
return refuse(JSON.stringify(path), PATH_WANT);
|
|
82
88
|
}
|
|
89
|
+
if (!symbol.startsWith(OWN_SCHEME)) {
|
|
90
|
+
return { records: [], refusals: [] };
|
|
91
|
+
}
|
|
83
92
|
const written = {
|
|
84
93
|
code,
|
|
85
94
|
symbol,
|
|
@@ -196,12 +205,55 @@ export function rowReason(provenance) {
|
|
|
196
205
|
return `recorded by ${provenance.analyzer} ${provenance.version}`;
|
|
197
206
|
}
|
|
198
207
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
|
|
208
|
+
* The rows of the other language's scheme that the baseline document `text` holds, in
|
|
209
|
+
* document order and as written, which a write keeps. A document that is not a baseline,
|
|
210
|
+
* or a row in a shape no record takes, is refused rather than dropped.
|
|
211
|
+
*/
|
|
212
|
+
export function otherSchemeRows(text) {
|
|
213
|
+
const refuse = (detail) => {
|
|
214
|
+
throw new Error(`${BASELINE_FILE} cannot be rewritten keeping the other language's rows: ${detail}`);
|
|
215
|
+
};
|
|
216
|
+
let parsed;
|
|
217
|
+
try {
|
|
218
|
+
parsed = JSON.parse(text);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
return refuse(error instanceof Error ? error.message : String(error));
|
|
222
|
+
}
|
|
223
|
+
const held = isObject(parsed) ? parsed[BASELINE_SHAPE.array] : undefined;
|
|
224
|
+
if (!Array.isArray(held)) {
|
|
225
|
+
return refuse(BASELINE_SHAPE.documentWant);
|
|
226
|
+
}
|
|
227
|
+
return held.flatMap((value) => {
|
|
228
|
+
try {
|
|
229
|
+
readRecord(value, documentSite(BASELINE_SHAPE), new Map(), BASELINE_SHAPE);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
return refuse(error instanceof Error ? error.message : String(error));
|
|
233
|
+
}
|
|
234
|
+
// readRecord has checked every member is a string and the code and symbol are present.
|
|
235
|
+
const { code, symbol, path, reason } = value;
|
|
236
|
+
if (code === undefined || symbol === undefined || symbol.startsWith(OWN_SCHEME)) {
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
return [
|
|
240
|
+
{
|
|
241
|
+
code,
|
|
242
|
+
symbol,
|
|
243
|
+
...(path === undefined ? {} : { path }),
|
|
244
|
+
...(reason === undefined ? {} : { reason }),
|
|
245
|
+
},
|
|
246
|
+
];
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* A baseline holding one row per finding, in the order given, which is the report's, after
|
|
251
|
+
* the other language's rows `kept`, so two analyzers writing one document in either order
|
|
252
|
+
* write the same bytes. Every recorded row carries the provenance as its reason. A finding
|
|
253
|
+
* missing a value a row needs, and an identity missing its name or version, are refused
|
|
254
|
+
* rather than written, because the grammar requires a reason on every row.
|
|
203
255
|
*/
|
|
204
|
-
export function writeBaseline(findings, provenance) {
|
|
256
|
+
export function writeBaseline(findings, provenance, kept = []) {
|
|
205
257
|
if (provenance.analyzer === "" || provenance.version === "") {
|
|
206
258
|
throw new Error(`a baseline row would carry no reason: the analyzer identity names ${JSON.stringify(provenance.analyzer)} at version ${JSON.stringify(provenance.version)}`);
|
|
207
259
|
}
|
|
@@ -212,5 +264,5 @@ export function writeBaseline(findings, provenance) {
|
|
|
212
264
|
}
|
|
213
265
|
return { code: found.code, symbol: found.symbol, path: found.path, reason };
|
|
214
266
|
});
|
|
215
|
-
return jsonChunks({ description: BASELINE_DESCRIPTION, baseline }, 2);
|
|
267
|
+
return jsonChunks({ description: BASELINE_DESCRIPTION, baseline: [...kept, ...baseline] }, 2);
|
|
216
268
|
}
|
|
@@ -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
|
import { SyntaxKind } from "@typescript/native/unstable/ast";
|
|
7
7
|
import { nodeKey } from "./inventory.js";
|
|
@@ -95,7 +95,7 @@ export function tokenStart(text, from) {
|
|
|
95
95
|
* syntax tree holds are skipped, so a `//` inside a string, a template or a regular
|
|
96
96
|
* expression starts nothing.
|
|
97
97
|
*/
|
|
98
|
-
|
|
98
|
+
function lineComments(file) {
|
|
99
99
|
const text = file.text;
|
|
100
100
|
const spans = [];
|
|
101
101
|
const visit = (node) => {
|
|
@@ -170,6 +170,14 @@ function declarationLines(file, held, symbols) {
|
|
|
170
170
|
file.forEachChild(visit);
|
|
171
171
|
return below;
|
|
172
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Whether the comment at `offset` follows a token on its own line, so it binds the
|
|
175
|
+
* declarations that begin on that line rather than on the next.
|
|
176
|
+
*/
|
|
177
|
+
function trails(file, offset) {
|
|
178
|
+
const { line } = file.getLineAndCharacterOfPosition(offset);
|
|
179
|
+
return tokenStart(file.text, file.getPositionOfLineAndCharacter(line, 0)) < offset;
|
|
180
|
+
}
|
|
173
181
|
/**
|
|
174
182
|
* Every comment in the namespace one project's own files hold, each with the declarations
|
|
175
183
|
* it binds, read while the project's view is open.
|
|
@@ -187,19 +195,20 @@ export function inlineDirectives(project, held, targetRoot) {
|
|
|
187
195
|
const below = declarationLines(file, held, symbols);
|
|
188
196
|
for (const comment of comments) {
|
|
189
197
|
const site = renderPosition(file, targetRoot, comment.offset);
|
|
198
|
+
const target = trails(file, comment.offset) ? site.line : site.line + 1;
|
|
190
199
|
found.push({
|
|
191
200
|
site,
|
|
192
201
|
text: comment.text,
|
|
193
202
|
directive: classifyComment(comment.text),
|
|
194
|
-
bound: [...(below.get(
|
|
203
|
+
bound: [...(below.get(target) ?? [])],
|
|
195
204
|
});
|
|
196
205
|
}
|
|
197
206
|
}
|
|
198
207
|
return found;
|
|
199
208
|
}
|
|
200
209
|
/**
|
|
201
|
-
* The inline directives of a run's projects, each bound to the declarations
|
|
202
|
-
*
|
|
210
|
+
* The inline directives of a run's projects, each bound to the declarations the placement
|
|
211
|
+
* rule gives it, one directive binding the union of what each project holding its file
|
|
203
212
|
* declares there. A directive that lacks only its reason is one {@link Refusal} per code;
|
|
204
213
|
* any other malformed comment in the namespace ends the read with a
|
|
205
214
|
* {@link SuppressionError} naming the first one written.
|
package/dist/src/suppress.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* The suppression grammar's records, and the ledger that decides what each one did. A
|
|
3
3
|
* record is one code bound to one site with its reason, from an inline directive, an
|
|
4
4
|
* ignore entry or a baseline row. A bound record marks its declaration live before the
|
|
5
|
-
* sweep,
|
|
6
|
-
*
|
|
5
|
+
* sweep, unless its code reports a part or a dependency row, and is in effect when it
|
|
6
|
+
* withholds a finding its code would have produced there, dormant when the configuration
|
|
7
|
+
* withholds every such finding anyway, and stale otherwise.
|
|
7
8
|
*/
|
|
8
9
|
import { ConfigError } from "./config.js";
|
|
9
10
|
import { positionKey } from "./position.js";
|
|
@@ -14,8 +15,6 @@ export const SUPPRESSION_WITHOUT_REASON = "DS1701";
|
|
|
14
15
|
export const UNSCOPED_ENTRY = "DS1702";
|
|
15
16
|
/** The code a suppression that withholds nothing is reported under. */
|
|
16
17
|
export const STALE_SUPPRESSION = "DS1703";
|
|
17
|
-
/** A target that carries no suppression. */
|
|
18
|
-
export const NO_SUPPRESSIONS = { records: [], refusals: [] };
|
|
19
18
|
/**
|
|
20
19
|
* A directive, an entry or a document the grammar refuses before any finding exists: an
|
|
21
20
|
* instruction the analysis cannot carry out. It is a malformed input, so the command line
|
|
@@ -71,22 +70,47 @@ export function blankReason(reason) {
|
|
|
71
70
|
return reason === undefined || /^[ \t\r\n]*$/u.test(reason);
|
|
72
71
|
}
|
|
73
72
|
/**
|
|
74
|
-
* The subject kinds that are a row
|
|
75
|
-
*
|
|
73
|
+
* The subject kinds that are a dependency row, each with the code that reports one. An
|
|
74
|
+
* ignore entry or a baseline row naming the row's reference and file withholds its finding
|
|
75
|
+
* and marks nothing, and an inline directive never binds one.
|
|
76
76
|
*/
|
|
77
|
-
const
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"module-directive",
|
|
81
|
-
"root",
|
|
82
|
-
"configured-declaration",
|
|
83
|
-
"suppression",
|
|
84
|
-
"edge",
|
|
77
|
+
const DEPENDENCY_SUBJECTS = new Map([
|
|
78
|
+
["dependency", ["DS1601"]],
|
|
79
|
+
["module-directive", ["DS1605"]],
|
|
85
80
|
]);
|
|
86
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* The subject kinds that are a row of a document rather than a declaration of the program,
|
|
83
|
+
* each with the codes that report one. Apart from a dependency row, nothing withholds a
|
|
84
|
+
* finding about one.
|
|
85
|
+
*/
|
|
86
|
+
const ROW_SUBJECTS = new Map([
|
|
87
|
+
["file", ["DS1501", "DS1502"]],
|
|
88
|
+
...DEPENDENCY_SUBJECTS,
|
|
89
|
+
["root", ["DS1704"]],
|
|
90
|
+
["configured-declaration", ["DS1706"]],
|
|
91
|
+
["suppression", ["DS1701", "DS1702", "DS1703"]],
|
|
92
|
+
["edge", ["DS1705"]],
|
|
93
|
+
]);
|
|
94
|
+
/** Whether a finding's subject is a row of a document rather than a declaration. */
|
|
87
95
|
export function isRowSubject(kind) {
|
|
88
96
|
return ROW_SUBJECTS.has(kind);
|
|
89
97
|
}
|
|
98
|
+
/** Whether a code reports a row of a document. */
|
|
99
|
+
export function isRowCode(code) {
|
|
100
|
+
return [...ROW_SUBJECTS.values()].some((codes) => codes.includes(code));
|
|
101
|
+
}
|
|
102
|
+
/** Whether a finding's subject is a dependency row. */
|
|
103
|
+
export function isDependencySubject(kind) {
|
|
104
|
+
return DEPENDENCY_SUBJECTS.has(kind);
|
|
105
|
+
}
|
|
106
|
+
/** Whether a code reports a dependency row, which only an ignore entry or a baseline row names. */
|
|
107
|
+
export function isDependencyCode(code) {
|
|
108
|
+
return [...DEPENDENCY_SUBJECTS.values()].some((codes) => codes.includes(code));
|
|
109
|
+
}
|
|
110
|
+
/** Whether a code reports a row of a document that no record of any mechanism can name. */
|
|
111
|
+
export function isUnsuppressibleCode(code) {
|
|
112
|
+
return isRowCode(code) && !isDependencyCode(code);
|
|
113
|
+
}
|
|
90
114
|
/**
|
|
91
115
|
* The subject kinds that are a part of a declaration rather than a declaration: a part
|
|
92
116
|
* has no reference of its own, so a finding about one names the declaration that holds
|
|
@@ -121,10 +145,19 @@ function claimKey(finding) {
|
|
|
121
145
|
* Whether one record names one finding. A record reaches a finding in two ways: it bound
|
|
122
146
|
* the declaration at the finding's position, or it names the finding's reference and the
|
|
123
147
|
* file the finding is reported in, each exactly. The second is how an entry copied from a
|
|
124
|
-
* finding about a part of a declaration reaches the part
|
|
148
|
+
* finding about a part of a declaration reaches the part, and the only way any record
|
|
149
|
+
* reaches a dependency row.
|
|
125
150
|
*/
|
|
126
151
|
export function pairs(record, finding) {
|
|
127
|
-
if (record.code !== finding.code
|
|
152
|
+
if (record.code !== finding.code) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if (DEPENDENCY_SUBJECTS.has(finding.symbol.kind)) {
|
|
156
|
+
return (record.mechanism !== "inline" &&
|
|
157
|
+
record.symbol === finding.symbol.ref &&
|
|
158
|
+
record.path === finding.position.path);
|
|
159
|
+
}
|
|
160
|
+
if (ROW_SUBJECTS.has(finding.symbol.kind)) {
|
|
128
161
|
return false;
|
|
129
162
|
}
|
|
130
163
|
if (record.bound !== "" && record.bound === positionKey(finding.position)) {
|
|
@@ -134,35 +167,31 @@ export function pairs(record, finding) {
|
|
|
134
167
|
}
|
|
135
168
|
/**
|
|
136
169
|
* Decides every record against `would`, the findings the run would produce were no record
|
|
137
|
-
* there: swept with no mark
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
170
|
+
* there: swept with no mark for a liveness kind, and as swept otherwise. The first record
|
|
171
|
+
* in reading order claims a finding, so a second naming it is stale; a part-kind record
|
|
172
|
+
* claims every finding of its code about a part of its declaration. A record is dormant
|
|
173
|
+
* when the dials withhold every finding it claims, read from those findings alone, or when
|
|
174
|
+
* it claims none and its code's severity is `allow`.
|
|
142
175
|
*/
|
|
143
176
|
export function ledgerOf(records, would, dials) {
|
|
144
177
|
const taken = new Set();
|
|
145
178
|
const verdicts = [];
|
|
146
|
-
const claims = [];
|
|
147
179
|
for (const record of records) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
180
|
+
const open = (finding) => !taken.has(claimKey(finding)) && pairs(record, finding);
|
|
181
|
+
const first = would.find(open);
|
|
182
|
+
const claimed = first === undefined ? [] : isPartKind(record.code) ? would.filter(open) : [first];
|
|
183
|
+
if (claimed.length === 0) {
|
|
184
|
+
verdicts.push(record.bound !== "" && dials.allows(record.code) ? "dormant" : "stale");
|
|
151
185
|
continue;
|
|
152
186
|
}
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
if (claimed === undefined) {
|
|
156
|
-
verdicts.push("stale");
|
|
157
|
-
continue;
|
|
187
|
+
for (const finding of claimed) {
|
|
188
|
+
taken.add(claimKey(finding));
|
|
158
189
|
}
|
|
159
|
-
|
|
160
|
-
verdicts.push(dials.withholds(claimed) ? "dormant" : "in-effect");
|
|
190
|
+
verdicts.push(claimed.every((finding) => dials.withholds(finding)) ? "dormant" : "in-effect");
|
|
161
191
|
}
|
|
162
192
|
return {
|
|
163
193
|
verdicts,
|
|
164
|
-
|
|
165
|
-
withheld: (finding) => taken.has(claimKey(finding)) && !ROW_SUBJECTS.has(finding.symbol.kind),
|
|
194
|
+
withheld: (finding) => taken.has(claimKey(finding)),
|
|
166
195
|
};
|
|
167
196
|
}
|
|
168
197
|
/** The suppression counts of one run's records and the ledger over them. */
|