@cplieger/deadset-ts 5.2.3 → 6.0.1
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/README.md +1 -1
- package/dist/src/analysis.js +28 -5
- package/dist/src/augmentations.js +50 -0
- package/dist/src/components.js +15 -15
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/convention-rows.js +17 -2
- package/dist/src/conventions.js +82 -1
- package/dist/src/dependencies.js +42 -26
- package/dist/src/entry-points.js +28 -14
- package/dist/src/exempt.js +1 -0
- package/dist/src/findings/completion.js +9 -4
- package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
- package/dist/src/findings/emitters.js +64 -25
- package/dist/src/findings/interfaces.js +4 -3
- package/dist/src/findings/intra-function.js +15 -2
- package/dist/src/findings/reads-and-writes.js +3 -2
- package/dist/src/findings/unused-declarations.js +13 -1
- package/dist/src/findings/visibility-narrowing.js +2 -1
- package/dist/src/findings-pass.js +1 -0
- package/dist/src/graph.js +6 -6
- package/dist/src/implementations.js +1 -1
- package/dist/src/indexed-access.js +215 -0
- package/dist/src/interface-satisfaction.js +85 -23
- package/dist/src/intra-function-parts.js +139 -30
- package/dist/src/inventory.js +6 -1
- package/dist/src/kinds.js +13 -12
- package/dist/src/matrix.js +34 -9
- package/dist/src/module-declarations.js +180 -0
- package/dist/src/outside-tests.js +17 -15
- package/dist/src/overrides.js +70 -9
- package/dist/src/query.js +7 -0
- package/dist/src/ref.js +9 -0
- package/dist/src/references.js +6 -0
- package/dist/src/report.js +6 -1
- package/dist/src/reporters/sarif.js +6 -2
- package/dist/src/reporters/text.js +23 -3
- package/dist/src/roots.js +50 -10
- package/dist/src/suppress-file.js +3 -5
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +82 -0
- package/dist/src/workspace-programs.js +6 -31
- package/jsr.json +1 -1
- package/package.json +8 -2
- package/src/analysis.ts +40 -4
- package/src/augmentations.ts +72 -0
- package/src/components.ts +15 -13
- package/src/conformance-record.ts +2 -2
- package/src/convention-rows.ts +25 -3
- package/src/conventions.ts +106 -2
- package/src/dependencies.ts +51 -31
- package/src/entry-points.ts +32 -19
- package/src/exempt.ts +4 -1
- package/src/findings/completion.ts +11 -4
- package/src/findings/dependencies-and-module-machinery.ts +2 -2
- package/src/findings/emitters.ts +89 -27
- package/src/findings/interfaces.ts +7 -6
- package/src/findings/intra-function.ts +20 -2
- package/src/findings/reads-and-writes.ts +7 -2
- package/src/findings/unused-declarations.ts +16 -1
- package/src/findings/visibility-narrowing.ts +2 -1
- package/src/findings-pass.ts +4 -1
- package/src/graph.ts +6 -6
- package/src/implementations.ts +1 -1
- package/src/indexed-access.ts +295 -0
- package/src/interface-satisfaction.ts +118 -27
- package/src/intra-function-parts.ts +196 -35
- package/src/inventory.ts +8 -0
- package/src/kinds.ts +14 -12
- package/src/matrix.ts +51 -8
- package/src/module-declarations.ts +241 -0
- package/src/outside-tests.ts +18 -16
- package/src/overrides.ts +80 -14
- package/src/query.ts +28 -0
- package/src/ref.ts +10 -0
- package/src/references.ts +6 -0
- package/src/report.ts +11 -1
- package/src/reporters/sarif.ts +9 -2
- package/src/reporters/text.ts +25 -3
- package/src/roots.ts +56 -7
- package/src/suppress-file.ts +3 -6
- package/src/sweep.ts +12 -0
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +104 -0
- package/src/workspace-programs.ts +6 -35
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The imports that name a `declare module` declaration whose name is a string literal: an
|
|
3
|
+
* augmentation or an ambient module, a wildcard one included. An import names it when its
|
|
4
|
+
* specifier equals the name, matches it with the one `*` standing for any run, or resolves
|
|
5
|
+
* to the same module. One that names it by name or pattern also reads the members it
|
|
6
|
+
* imports, whatever file the specifier resolves to, as a compiler reading no such file does.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
isCallExpression,
|
|
11
|
+
isExportDeclaration,
|
|
12
|
+
isExternalModuleReference,
|
|
13
|
+
isImportDeclaration,
|
|
14
|
+
isImportEqualsDeclaration,
|
|
15
|
+
isImportTypeNode,
|
|
16
|
+
isLiteralTypeNode,
|
|
17
|
+
isModuleDeclaration,
|
|
18
|
+
isNamespaceExport,
|
|
19
|
+
isNamespaceImport,
|
|
20
|
+
isNoSubstitutionTemplateLiteral,
|
|
21
|
+
isStringLiteral,
|
|
22
|
+
SyntaxKind,
|
|
23
|
+
type Node,
|
|
24
|
+
type SourceFile,
|
|
25
|
+
} from "@typescript/native/unstable/ast";
|
|
26
|
+
import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
27
|
+
import { renderPosition } from "./position.ts";
|
|
28
|
+
import { UNANSWERED } from "./query.ts";
|
|
29
|
+
import { nameComponent } from "./ref.ts";
|
|
30
|
+
import type { Reference } from "./references.ts";
|
|
31
|
+
import type { ProjectView } from "./session.ts";
|
|
32
|
+
|
|
33
|
+
/** Whether a node is a specifier's text: a string literal or a template with no substitution. */
|
|
34
|
+
function literalText(node: Node | undefined): node is Node & { readonly text: string } {
|
|
35
|
+
return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The specifier one node writes, where it is an import of any form. */
|
|
39
|
+
function specifierOf(node: Node): (Node & { readonly text: string }) | undefined {
|
|
40
|
+
if (isImportDeclaration(node) || isExportDeclaration(node)) {
|
|
41
|
+
const specifier = node.moduleSpecifier;
|
|
42
|
+
return literalText(specifier) ? specifier : undefined;
|
|
43
|
+
}
|
|
44
|
+
if (isImportEqualsDeclaration(node)) {
|
|
45
|
+
const reference = node.moduleReference;
|
|
46
|
+
return isExternalModuleReference(reference) && literalText(reference.expression)
|
|
47
|
+
? reference.expression
|
|
48
|
+
: undefined;
|
|
49
|
+
}
|
|
50
|
+
if (isCallExpression(node) && node.expression.kind === SyntaxKind.ImportKeyword) {
|
|
51
|
+
const [argument] = node.arguments;
|
|
52
|
+
return literalText(argument) ? argument : undefined;
|
|
53
|
+
}
|
|
54
|
+
if (isImportTypeNode(node) && isLiteralTypeNode(node.argument)) {
|
|
55
|
+
const literal = node.argument.literal;
|
|
56
|
+
return literalText(literal) ? literal : undefined;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The export names an import or a re-export reads from the module it names: none for a
|
|
63
|
+
* namespace, a star or a bare form.
|
|
64
|
+
*/
|
|
65
|
+
export function namesImported(node: Node): readonly string[] {
|
|
66
|
+
if (isImportDeclaration(node)) {
|
|
67
|
+
const clause = node.importClause;
|
|
68
|
+
const bindings = clause?.namedBindings;
|
|
69
|
+
return [
|
|
70
|
+
...(clause?.name === undefined ? [] : ["default"]),
|
|
71
|
+
...(bindings === undefined || isNamespaceImport(bindings)
|
|
72
|
+
? []
|
|
73
|
+
: bindings.elements.map((one) => (one.propertyName ?? one.name).text)),
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
const clause = isExportDeclaration(node) ? node.exportClause : undefined;
|
|
77
|
+
return clause === undefined || isNamespaceExport(clause)
|
|
78
|
+
? []
|
|
79
|
+
: clause.elements.map((one) => (one.propertyName ?? one.name).text);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether a specifier matches a declaration's name, the name's one `*` standing for any run.
|
|
84
|
+
* A relative name is resolved against its own file, so no specifier names it by its text.
|
|
85
|
+
*/
|
|
86
|
+
function namesModule(specifier: string, name: string): boolean {
|
|
87
|
+
if (/^(?:\.{1,2}(?:\/|$)|\/)/u.test(name)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const star = name.indexOf("*");
|
|
91
|
+
if (star < 0 || name.slice(star + 1).includes("*")) {
|
|
92
|
+
return specifier === name;
|
|
93
|
+
}
|
|
94
|
+
const prefix = name.slice(0, star);
|
|
95
|
+
const suffix = name.slice(star + 1);
|
|
96
|
+
return (
|
|
97
|
+
specifier.length >= prefix.length + suffix.length &&
|
|
98
|
+
specifier.startsWith(prefix) &&
|
|
99
|
+
specifier.endsWith(suffix)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** One module declaration a specifier names, and the members of it the import reads. */
|
|
104
|
+
interface Named {
|
|
105
|
+
readonly id: string;
|
|
106
|
+
readonly members: readonly string[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The module declarations of an inventory a specifier names by name or pattern, each with
|
|
111
|
+
* the members that hold the export names given.
|
|
112
|
+
*/
|
|
113
|
+
export function namedBy(
|
|
114
|
+
held: Inventory,
|
|
115
|
+
): (specifier: string, names: readonly string[]) => readonly Named[] {
|
|
116
|
+
if (held.modules.size === 0) {
|
|
117
|
+
return () => [];
|
|
118
|
+
}
|
|
119
|
+
const refs = new Map<string, string>();
|
|
120
|
+
const children = new Map<string, InventorySymbol[]>();
|
|
121
|
+
for (const symbol of held.symbols) {
|
|
122
|
+
if (held.modules.has(symbol.id)) {
|
|
123
|
+
refs.set(symbol.id, symbol.ref);
|
|
124
|
+
}
|
|
125
|
+
if (held.modules.has(symbol.parent)) {
|
|
126
|
+
const siblings = children.get(symbol.parent) ?? [];
|
|
127
|
+
siblings.push(symbol);
|
|
128
|
+
children.set(symbol.parent, siblings);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return (specifier, names) =>
|
|
132
|
+
[...held.modules].flatMap(([id, name]) => {
|
|
133
|
+
if (!namesModule(specifier, name)) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
// A member is named by its declaration's reference, or an export alias's.
|
|
137
|
+
const ref = refs.get(id) ?? "";
|
|
138
|
+
const scope = ref.slice(0, ref.indexOf("#"));
|
|
139
|
+
const wanted = new Set(
|
|
140
|
+
names.flatMap((one) => [
|
|
141
|
+
`${ref}.${nameComponent(one)}`,
|
|
142
|
+
`${scope}#${nameComponent(one)}:alias`,
|
|
143
|
+
]),
|
|
144
|
+
);
|
|
145
|
+
const members = (children.get(id) ?? []).filter((child) => wanted.has(child.ref));
|
|
146
|
+
return [{ id, members: members.map((child) => child.id) }];
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** One import of a project's own files, and the declaration or file that holds it. */
|
|
151
|
+
interface Import {
|
|
152
|
+
readonly file: SourceFile;
|
|
153
|
+
readonly from: string;
|
|
154
|
+
readonly specifier: Node & { readonly text: string };
|
|
155
|
+
readonly names: readonly string[];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** One module declaration of the inventory, and the name node it declares. */
|
|
159
|
+
interface Declared {
|
|
160
|
+
readonly id: string;
|
|
161
|
+
readonly name: Node & { readonly text: string };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The references the imports of one project's own files make to the module declarations
|
|
166
|
+
* they name, each at the import's specifier.
|
|
167
|
+
*/
|
|
168
|
+
export function moduleDeclarationReferences<Brand>(
|
|
169
|
+
project: ProjectView<Brand>,
|
|
170
|
+
held: Inventory,
|
|
171
|
+
root: string,
|
|
172
|
+
testFiles: ReadonlySet<string>,
|
|
173
|
+
): readonly Reference[] {
|
|
174
|
+
const imports: Import[] = [];
|
|
175
|
+
const declared: Declared[] = [];
|
|
176
|
+
for (const file of project.ownSourceFiles()) {
|
|
177
|
+
const fileId = held.declarations.get(nodeKey(file, file));
|
|
178
|
+
if (fileId === undefined) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const visit = (node: Node, from: string): void => {
|
|
182
|
+
const own = held.declarations.get(nodeKey(file, node));
|
|
183
|
+
if (isModuleDeclaration(node) && literalText(node.name) && own !== undefined) {
|
|
184
|
+
declared.push({ id: own, name: node.name });
|
|
185
|
+
}
|
|
186
|
+
const specifier = specifierOf(node);
|
|
187
|
+
if (specifier !== undefined) {
|
|
188
|
+
imports.push({ file, from, specifier, names: namesImported(node) });
|
|
189
|
+
}
|
|
190
|
+
node.forEachChild((child) => {
|
|
191
|
+
visit(child, own ?? from);
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
file.forEachChild((child) => {
|
|
195
|
+
visit(child, fileId);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (declared.length === 0 || imports.length === 0) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const asked = [...declared.map((one) => one.name), ...imports.map((one) => one.specifier)];
|
|
203
|
+
const answers = project.symbolsAt(asked.map((node) => project.handle(node)));
|
|
204
|
+
const moduleOf = (at: number): number | undefined => {
|
|
205
|
+
const symbol = answers[at];
|
|
206
|
+
return symbol === undefined || symbol === UNANSWERED ? undefined : symbol.id;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const named = namedBy(held);
|
|
210
|
+
const found: Reference[] = [];
|
|
211
|
+
imports.forEach((one, index) => {
|
|
212
|
+
const resolved = moduleOf(declared.length + index);
|
|
213
|
+
const position = renderPosition(one.file, root, one.specifier.getStart());
|
|
214
|
+
const read = (to: string, resolution: Reference["resolution"]): void => {
|
|
215
|
+
found.push({
|
|
216
|
+
from: one.from,
|
|
217
|
+
to,
|
|
218
|
+
position,
|
|
219
|
+
use: "read",
|
|
220
|
+
resolution,
|
|
221
|
+
test: testFiles.has(position.path),
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
const same = new Set(
|
|
225
|
+
declared
|
|
226
|
+
.filter((_, at) => resolved !== undefined && resolved === moduleOf(at))
|
|
227
|
+
.map((declaration) => declaration.id),
|
|
228
|
+
);
|
|
229
|
+
for (const id of same) {
|
|
230
|
+
read(id, "batch");
|
|
231
|
+
}
|
|
232
|
+
for (const declaration of named(one.specifier.text, one.names)) {
|
|
233
|
+
if (!same.has(declaration.id)) {
|
|
234
|
+
for (const to of [declaration.id, ...declaration.members]) {
|
|
235
|
+
read(to, "syntax");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
return found;
|
|
241
|
+
}
|
package/src/outside-tests.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The test files no compiler configuration of a run holds. The runner executes them all
|
|
3
3
|
* the same, so each module one names is a test reference: the own file a relative
|
|
4
|
-
* specifier names, and the declarations that file exports under the names read from it
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* specifier names, and the declarations that file exports under the names read from it,
|
|
5
|
+
* or a module declaration the specifier names by name or pattern and its members of
|
|
6
|
+
* those names. Such a file is read without a program, so what its body references
|
|
7
|
+
* beyond the modules it names is not known.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import {
|
|
@@ -22,6 +23,7 @@ import { RESOLVED_EXTENSIONS, type ParseFile } from "./configuration-files.ts";
|
|
|
22
23
|
import { emittedFrom } from "./emit-map.ts";
|
|
23
24
|
import type { Host } from "./host.ts";
|
|
24
25
|
import type { Inventory } from "./inventory.ts";
|
|
26
|
+
import { namedBy, namesImported } from "./module-declarations.ts";
|
|
25
27
|
import { dirnamePath, joinPath, normalizePath, queriedModulePath } from "./paths.ts";
|
|
26
28
|
import { renderPosition } from "./position.ts";
|
|
27
29
|
import { nameComponent } from "./ref.ts";
|
|
@@ -75,21 +77,15 @@ function modulesNamed(file: SourceFile): readonly NamedModule[] {
|
|
|
75
77
|
const bindings = clause?.namedBindings;
|
|
76
78
|
name(
|
|
77
79
|
statement.moduleSpecifier,
|
|
78
|
-
|
|
79
|
-
...(clause?.name === undefined ? [] : ["default"]),
|
|
80
|
-
...(bindings === undefined || isNamespaceImport(bindings)
|
|
81
|
-
? []
|
|
82
|
-
: bindings.elements.map((one) => (one.propertyName ?? one.name).text)),
|
|
83
|
-
],
|
|
80
|
+
namesImported(statement),
|
|
84
81
|
bindings === undefined ? clause?.name === undefined : isNamespaceImport(bindings),
|
|
85
82
|
);
|
|
86
83
|
} else if (isExportDeclaration(statement) && statement.moduleSpecifier !== undefined) {
|
|
87
84
|
const clause = statement.exportClause;
|
|
88
|
-
const star = clause === undefined || isNamespaceExport(clause);
|
|
89
85
|
name(
|
|
90
86
|
statement.moduleSpecifier,
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
namesImported(statement),
|
|
88
|
+
clause === undefined || isNamespaceExport(clause),
|
|
93
89
|
);
|
|
94
90
|
}
|
|
95
91
|
}
|
|
@@ -158,11 +154,21 @@ export function outsideTestReferences(
|
|
|
158
154
|
exported.set(key, [...(exported.get(key) ?? []), symbol.id]);
|
|
159
155
|
}
|
|
160
156
|
}
|
|
157
|
+
const declared = namedBy(held);
|
|
161
158
|
const found: Reference[] = [];
|
|
162
159
|
for (const { path, file } of tests) {
|
|
163
160
|
const from = `${path}#`;
|
|
164
161
|
const dir = dirnamePath(path) === "." ? "" : dirnamePath(path);
|
|
165
162
|
for (const named of modulesNamed(file)) {
|
|
163
|
+
const position = renderPosition(file, targetRoot, named.specifier.getStart());
|
|
164
|
+
const read = (to: string, use: Reference["use"]): void => {
|
|
165
|
+
found.push({ from, to, position, use, resolution: "syntax", test: true, unheld: true });
|
|
166
|
+
};
|
|
167
|
+
for (const declaration of declared(named.text, named.names)) {
|
|
168
|
+
for (const to of [declaration.id, ...declaration.members]) {
|
|
169
|
+
read(to, "read");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
166
172
|
const written = named.text.includes("?") ? queriedModulePath(named.text) : named.text;
|
|
167
173
|
if (written === undefined) {
|
|
168
174
|
continue;
|
|
@@ -173,10 +179,6 @@ export function outsideTestReferences(
|
|
|
173
179
|
if (fileId === undefined) {
|
|
174
180
|
continue;
|
|
175
181
|
}
|
|
176
|
-
const position = renderPosition(file, targetRoot, named.specifier.getStart());
|
|
177
|
-
const read = (to: string, use: Reference["use"]): void => {
|
|
178
|
-
found.push({ from, to, position, use, resolution: "syntax", test: true, unheld: true });
|
|
179
|
-
};
|
|
180
182
|
if (named.evaluates) {
|
|
181
183
|
read(fileId, "evaluation");
|
|
182
184
|
}
|
package/src/overrides.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The overrides of the target's class members. A call through the base class
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The overrides of the target's class members. A call through the base class names the
|
|
3
|
+
* base's member and runs the override of whichever subclass the value is, so the base
|
|
4
|
+
* member's declaration references every override. A method overriding a member of a class
|
|
5
|
+
* declared outside the target, at any depth of its `extends` chain, is called by outside
|
|
6
|
+
* code holding the instance through that class, so its own class references it.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import {
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
import type { AliasChains } from "./alias-chain.ts";
|
|
19
20
|
import { memberComponent } from "./class-members.ts";
|
|
20
21
|
import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
22
|
+
import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/unstable/sync";
|
|
21
23
|
import { UNANSWERED } from "./query.ts";
|
|
22
24
|
import type { Reference } from "./references.ts";
|
|
23
25
|
import type { ProjectView } from "./session.ts";
|
|
@@ -58,9 +60,10 @@ function extending<Brand>(
|
|
|
58
60
|
|
|
59
61
|
/**
|
|
60
62
|
* The references each base class member makes to its overrides, for the classes of one
|
|
61
|
-
* project that extend another class of the target
|
|
62
|
-
*
|
|
63
|
-
*
|
|
63
|
+
* project that extend another class of the target, and each class makes to its methods
|
|
64
|
+
* that override a member of a class declared outside the target. A member a nearer base
|
|
65
|
+
* already overrides is reached through that override. A private member is not
|
|
66
|
+
* overridden. Each reference is placed at the override and made by its file.
|
|
64
67
|
*/
|
|
65
68
|
export function overrideReferences<Brand>(
|
|
66
69
|
project: ProjectView<Brand>,
|
|
@@ -89,17 +92,28 @@ export function overrideReferences<Brand>(
|
|
|
89
92
|
members.set(symbol.parent, own);
|
|
90
93
|
}
|
|
91
94
|
const bases = new Map<string, readonly string[]>();
|
|
95
|
+
const outsideBases = new Map<string, TSSymbol>();
|
|
96
|
+
const own = project.ownPaths();
|
|
92
97
|
const symbols = project.symbolsAt(classes.map((one) => project.handle(one.base)));
|
|
93
98
|
classes.forEach(({ id }, index) => {
|
|
94
99
|
const symbol = symbols[index];
|
|
95
100
|
if (symbol !== undefined && symbol !== UNANSWERED) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
const inTarget = chains
|
|
102
|
+
.chainOf(symbol)
|
|
103
|
+
.map((target) => target.id)
|
|
104
|
+
.filter((target) => byId.get(target)?.kind === "class" && target !== id);
|
|
105
|
+
bases.set(id, inTarget);
|
|
106
|
+
const declared =
|
|
107
|
+
(symbol.flags & SymbolFlags.Alias) === 0 ? symbol : project.queries.aliased(symbol);
|
|
108
|
+
if (
|
|
109
|
+
inTarget.length === 0 &&
|
|
110
|
+
declared !== UNANSWERED &&
|
|
111
|
+
(declared.flags & SymbolFlags.Class) !== 0 &&
|
|
112
|
+
declared.declarations.length > 0 &&
|
|
113
|
+
declared.declarations.every((handle) => !own.has(handle.path))
|
|
114
|
+
) {
|
|
115
|
+
outsideBases.set(id, declared);
|
|
116
|
+
}
|
|
103
117
|
}
|
|
104
118
|
});
|
|
105
119
|
|
|
@@ -117,6 +131,21 @@ export function overrideReferences<Brand>(
|
|
|
117
131
|
};
|
|
118
132
|
|
|
119
133
|
const found: Reference[] = [];
|
|
134
|
+
const outsideNames = outsideMemberNames(project, bases, outsideBases);
|
|
135
|
+
for (const [classId, names] of outsideNames) {
|
|
136
|
+
for (const [component, member] of members.get(classId) ?? []) {
|
|
137
|
+
if (member.kind === "method" && names.has(component)) {
|
|
138
|
+
found.push({
|
|
139
|
+
from: classId,
|
|
140
|
+
to: member.id,
|
|
141
|
+
position: member.position,
|
|
142
|
+
use: "read",
|
|
143
|
+
resolution: "override",
|
|
144
|
+
test: testFiles.has(member.position.path),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
120
149
|
for (const [classId, baseIds] of bases) {
|
|
121
150
|
for (const [component, member] of members.get(classId) ?? []) {
|
|
122
151
|
const seen = new Set([classId]);
|
|
@@ -134,3 +163,40 @@ export function overrideReferences<Brand>(
|
|
|
134
163
|
}
|
|
135
164
|
return found;
|
|
136
165
|
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Per class of the target with a class declared outside the target in its `extends`
|
|
169
|
+
* chain, the names of the members that outside class's instances carry.
|
|
170
|
+
*/
|
|
171
|
+
function outsideMemberNames<Brand>(
|
|
172
|
+
project: ProjectView<Brand>,
|
|
173
|
+
bases: ReadonlyMap<string, readonly string[]>,
|
|
174
|
+
outsideBases: ReadonlyMap<string, TSSymbol>,
|
|
175
|
+
): ReadonlyMap<string, ReadonlySet<string>> {
|
|
176
|
+
const direct = new Map<string, ReadonlySet<string>>();
|
|
177
|
+
for (const [classId, base] of outsideBases) {
|
|
178
|
+
const type = project.queries.declaredTypeOf(base);
|
|
179
|
+
const properties = type === UNANSWERED ? UNANSWERED : project.queries.propertiesOf(type);
|
|
180
|
+
if (properties !== UNANSWERED) {
|
|
181
|
+
direct.set(classId, new Set(properties.map((property) => property.escapedName)));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const names = new Map<string, ReadonlySet<string>>();
|
|
185
|
+
const above = (classId: string, seen: Set<string>): ReadonlySet<string> => {
|
|
186
|
+
if (seen.has(classId)) {
|
|
187
|
+
return new Set();
|
|
188
|
+
}
|
|
189
|
+
seen.add(classId);
|
|
190
|
+
return new Set([
|
|
191
|
+
...(direct.get(classId) ?? []),
|
|
192
|
+
...(bases.get(classId) ?? []).flatMap((base) => [...above(base, seen)]),
|
|
193
|
+
]);
|
|
194
|
+
};
|
|
195
|
+
for (const classId of bases.keys()) {
|
|
196
|
+
const found = above(classId, new Set());
|
|
197
|
+
if (found.size > 0) {
|
|
198
|
+
names.set(classId, found);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return names;
|
|
202
|
+
}
|
package/src/query.ts
CHANGED
|
@@ -203,11 +203,17 @@ export interface Queries {
|
|
|
203
203
|
constituents(type: Type): Answer<readonly Type[]>;
|
|
204
204
|
/** The generic type a type reference instantiates; the type itself for another type. */
|
|
205
205
|
targetOf(type: Type): Answer<Type>;
|
|
206
|
+
/** The constraint of a type parameter, where it declares one; none for another type. */
|
|
207
|
+
constraintOf(type: Type): Answer<Type | undefined>;
|
|
206
208
|
/** The symbol a type is declared by. */
|
|
207
209
|
symbolOfType(type: Type): Answer<TSSymbol | undefined>;
|
|
208
210
|
signaturesOf(type: Type, kind: SignatureKind): Answer<readonly Signature[]>;
|
|
209
211
|
resolvedSignature(node: Node): Answer<Signature | undefined>;
|
|
210
212
|
parameterType(signature: Signature, index: number): Answer<Type | undefined>;
|
|
213
|
+
/** The generic signature an instantiated one instantiates; none for another signature. */
|
|
214
|
+
genericOf(signature: Signature): Answer<Signature | undefined>;
|
|
215
|
+
/** The type of the parameter an argument at one position binds, a rest element's included. */
|
|
216
|
+
typeAtPosition(signature: Signature, index: number): Answer<Type>;
|
|
211
217
|
signatureOf(declaration: Node): Answer<Signature | undefined>;
|
|
212
218
|
returnTypeOf(signature: Signature): Answer<Type | undefined>;
|
|
213
219
|
typeToString(type: Type): Answer<string>;
|
|
@@ -502,6 +508,14 @@ export function queriesOf(
|
|
|
502
508
|
() => type.getTarget(),
|
|
503
509
|
)
|
|
504
510
|
: type,
|
|
511
|
+
constraintOf: (type) =>
|
|
512
|
+
type.isTypeParameter()
|
|
513
|
+
? one(
|
|
514
|
+
"getConstraint",
|
|
515
|
+
() => typeLocation(type),
|
|
516
|
+
() => type.getConstraint(),
|
|
517
|
+
)
|
|
518
|
+
: undefined,
|
|
505
519
|
symbolOfType: (type) =>
|
|
506
520
|
one(
|
|
507
521
|
"getSymbol",
|
|
@@ -526,6 +540,20 @@ export function queriesOf(
|
|
|
526
540
|
() => `signature ${String(signature.id)} parameter ${String(index)}`,
|
|
527
541
|
() => checker.getParameterType(signature, index),
|
|
528
542
|
),
|
|
543
|
+
genericOf: (signature) =>
|
|
544
|
+
signature.target === undefined
|
|
545
|
+
? undefined
|
|
546
|
+
: one(
|
|
547
|
+
"getTarget",
|
|
548
|
+
() => `signature ${String(signature.id)}`,
|
|
549
|
+
() => signature.getTarget(),
|
|
550
|
+
),
|
|
551
|
+
typeAtPosition: (signature, index) =>
|
|
552
|
+
one(
|
|
553
|
+
"getTypeParameterAtPosition",
|
|
554
|
+
() => `signature ${String(signature.id)} position ${String(index)}`,
|
|
555
|
+
() => signature.getTypeParameterAtPosition(index),
|
|
556
|
+
),
|
|
529
557
|
signatureOf: (declaration) =>
|
|
530
558
|
one(
|
|
531
559
|
"getSignatureFromDeclaration",
|
package/src/ref.ts
CHANGED
|
@@ -60,6 +60,7 @@ const REF_FORMS: readonly RegExp[] = REF_EXPRESSIONS.map(
|
|
|
60
60
|
|
|
61
61
|
const IDENTIFIER = new RegExp(`^${TS_IDENT}$`, "u");
|
|
62
62
|
const PRIVATE_NAME = new RegExp(`^#${TS_IDENT}$`, "u");
|
|
63
|
+
// Stryker disable next-line Regex: its \v to \V mutant is no valid escape under the u flag, so the whole module fails to parse
|
|
63
64
|
const ASCII_WHITESPACE = /[\t\n\v\f\r ]/gu;
|
|
64
65
|
|
|
65
66
|
/** The section of a manifest that declares a dependency. */
|
|
@@ -168,6 +169,15 @@ export function renderRef(module: Module, fragment: Fragment): string {
|
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
/**
|
|
173
|
+
* One declaration's key among a run's: its reference and the file that declares it. Two
|
|
174
|
+
* packages of one name spell one reference for two declarations, so the reference alone
|
|
175
|
+
* keys neither.
|
|
176
|
+
*/
|
|
177
|
+
export function declarationKey(ref: string, path: string): string {
|
|
178
|
+
return `${ref}\u0000${path}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
171
181
|
/**
|
|
172
182
|
* Whether one string is a reference of this language's grammar, in its canonical
|
|
173
183
|
* spelling. A string that is not, whether misspelled or spelled in a form the
|
package/src/references.ts
CHANGED
|
@@ -42,6 +42,9 @@ import { specifierCandidates } from "./configuration-files.ts";
|
|
|
42
42
|
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
|
+
import { augmentationReferences } from "./augmentations.ts";
|
|
46
|
+
import { indexedAccessReferences } from "./indexed-access.ts";
|
|
47
|
+
import { moduleDeclarationReferences } from "./module-declarations.ts";
|
|
45
48
|
import { overrideReferences } from "./overrides.ts";
|
|
46
49
|
import { dirnamePath, queriedModulePath, relativePath, resolvePath } from "./paths.ts";
|
|
47
50
|
import { byPosition, isComponentFile, renderPosition, type Position } from "./position.ts";
|
|
@@ -1494,7 +1497,10 @@ export function references<Brand>(
|
|
|
1494
1497
|
|
|
1495
1498
|
if (consumer === undefined) {
|
|
1496
1499
|
found.push(...overrideReferences(project, held, chains, new Set(tests)));
|
|
1500
|
+
found.push(...indexedAccessReferences(project, held, chains, root, new Set(tests)));
|
|
1501
|
+
found.push(...moduleDeclarationReferences(project, held, root, new Set(tests)));
|
|
1497
1502
|
}
|
|
1503
|
+
found.push(...augmentationReferences(held, found));
|
|
1498
1504
|
found.sort(
|
|
1499
1505
|
(a, b) =>
|
|
1500
1506
|
byPosition(a.position, b.position) ||
|
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 = "
|
|
22
|
+
const SCHEMA_VERSION = "8.0.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];
|
|
@@ -131,6 +131,11 @@ export interface WireTotals {
|
|
|
131
131
|
readonly stale_suppressions: number;
|
|
132
132
|
readonly pending: number;
|
|
133
133
|
readonly omitted: number;
|
|
134
|
+
readonly withheld: {
|
|
135
|
+
readonly certain: number;
|
|
136
|
+
readonly probable: number;
|
|
137
|
+
readonly possible: number;
|
|
138
|
+
};
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
/** One project configuration of the matrix a run analyzed. */
|
|
@@ -438,6 +443,11 @@ function totalsOf(findings: readonly WireFinding[], result: PassResult): WireTot
|
|
|
438
443
|
stale_suppressions: result.staleSuppressions.length,
|
|
439
444
|
pending: result.totals.pending,
|
|
440
445
|
omitted: 0,
|
|
446
|
+
withheld: {
|
|
447
|
+
certain: result.totals.withheld.certain,
|
|
448
|
+
probable: result.totals.withheld.probable,
|
|
449
|
+
possible: result.totals.withheld.possible,
|
|
450
|
+
},
|
|
441
451
|
};
|
|
442
452
|
}
|
|
443
453
|
|
package/src/reporters/sarif.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Confidence, Severity } from "../config.ts";
|
|
2
2
|
import { jsonChunks } from "../json-chunks.ts";
|
|
3
3
|
import { KINDS, type KindRow } from "../kinds.ts";
|
|
4
|
+
import { declarationKey } from "../ref.ts";
|
|
4
5
|
import type { Report, WireFinding, WireStaleSuppression } from "../report.ts";
|
|
5
6
|
import { lineHashes, symbolFingerprint } from "./fingerprint.ts";
|
|
6
7
|
import { RenderError, type RenderOptions } from "./reporter.ts";
|
|
7
8
|
import { utf8Bytes } from "./sha256.ts";
|
|
9
|
+
import { withheldLine } from "./text.ts";
|
|
8
10
|
|
|
9
11
|
const SCHEMA =
|
|
10
12
|
"https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json";
|
|
@@ -159,7 +161,10 @@ function relatedOf(found: WireFinding): readonly Related[] {
|
|
|
159
161
|
related.push({ label: "write", at, endLine: at.end_line });
|
|
160
162
|
}
|
|
161
163
|
for (const one of found.component.members ?? []) {
|
|
162
|
-
if (
|
|
164
|
+
if (
|
|
165
|
+
declarationKey(one.ref, one.position.path) !==
|
|
166
|
+
declarationKey(found.symbol.ref, found.position.path)
|
|
167
|
+
) {
|
|
163
168
|
related.push({ label: "member", at: one.position, endLine: one.position.end_line });
|
|
164
169
|
}
|
|
165
170
|
}
|
|
@@ -271,6 +276,7 @@ export function sarif(report: Report, options: RenderOptions): Iterable<string>
|
|
|
271
276
|
...report.stale_suppressions.map((stale) => staleResult(stale, index, hashOf)),
|
|
272
277
|
];
|
|
273
278
|
const languages = [...report.analyzer.languages].sort().join("+");
|
|
279
|
+
const withheld = withheldLine(report.totals);
|
|
274
280
|
const log = {
|
|
275
281
|
$schema: SCHEMA,
|
|
276
282
|
version: VERSION,
|
|
@@ -288,7 +294,8 @@ export function sarif(report: Report, options: RenderOptions): Iterable<string>
|
|
|
288
294
|
columnKind: COLUMN_KIND,
|
|
289
295
|
originalUriBaseIds: { [URI_BASE_ID]: { description: { text: ROOT_DESCRIPTION } } },
|
|
290
296
|
results,
|
|
291
|
-
properties:
|
|
297
|
+
properties:
|
|
298
|
+
withheld === undefined ? { totals: report.totals } : { totals: report.totals, withheld },
|
|
292
299
|
},
|
|
293
300
|
],
|
|
294
301
|
};
|
package/src/reporters/text.ts
CHANGED
|
@@ -24,11 +24,29 @@ function summary(totals: WireTotals): string {
|
|
|
24
24
|
].join(", ");
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** The confidences a withheld line names, from the highest the minimum can withhold. */
|
|
28
|
+
const WITHHELD_ORDER = ["probable", "possible"] as const;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The line naming what the minimum confidence withheld and the setting that shows it all,
|
|
32
|
+
* or undefined where it withheld nothing.
|
|
33
|
+
*/
|
|
34
|
+
export function withheldLine(totals: WireTotals): string | undefined {
|
|
35
|
+
const named = WITHHELD_ORDER.filter((confidence) => totals.withheld[confidence] > 0);
|
|
36
|
+
const lowest = named.at(-1);
|
|
37
|
+
if (lowest === undefined) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const counts = named.map((confidence) => `${String(totals.withheld[confidence])} ${confidence}`);
|
|
41
|
+
return `withheld by analysis.min_confidence: ${counts.join(", ")}, shown with analysis.min_confidence set to ${lowest}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
/**
|
|
28
45
|
* One line per finding in the report's order, then one per stale suppression, then the
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
46
|
+
* withheld line where the minimum withheld a finding, then the summary. A finding line is
|
|
47
|
+
* `path:line:col: kind name: message [confidence] (CODE)`; the summary is deliberately not
|
|
48
|
+
* in that shape, so a filter for finding lines yields exactly them. Nothing else is
|
|
49
|
+
* written: no timestamp, duration or host detail.
|
|
32
50
|
*/
|
|
33
51
|
export const text = (report: Report): Iterable<string> =>
|
|
34
52
|
coalesced(
|
|
@@ -39,6 +57,10 @@ export const text = (report: Report): Iterable<string> =>
|
|
|
39
57
|
for (const stale of report.stale_suppressions) {
|
|
40
58
|
yield `${stale.position.path}:${String(stale.position.line)}:${String(stale.position.column)}: ${STALE_KIND} ${stale.symbol}: ${stale.message} [${STALE_CONFIDENCE}] (${stale.code})\n`;
|
|
41
59
|
}
|
|
60
|
+
const withheld = withheldLine(report.totals);
|
|
61
|
+
if (withheld !== undefined) {
|
|
62
|
+
yield `${withheld}\n`;
|
|
63
|
+
}
|
|
42
64
|
yield `${summary(report.totals)}\n`;
|
|
43
65
|
})(),
|
|
44
66
|
);
|