@cplieger/deadset-ts 5.0.0 → 5.1.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 +145 -39
- package/dist/src/configuration-files.js +108 -4
- package/dist/src/conformance-record.js +3 -9
- package/dist/src/contextual-uses.js +247 -0
- package/dist/src/convention-rows.js +6 -0
- package/dist/src/conventions.js +123 -14
- package/dist/src/dependencies.js +8 -2
- package/dist/src/destructuring.js +6 -6
- package/dist/src/entry-points.js +8 -5
- package/dist/src/exempt-classes.js +6 -1
- package/dist/src/exempt.js +35 -16
- package/dist/src/file-facts.js +23 -20
- package/dist/src/findings/completion.js +27 -3
- package/dist/src/findings/interfaces.js +6 -2
- package/dist/src/findings/reads-and-writes.js +53 -9
- package/dist/src/findings/unused-declarations.js +40 -1
- package/dist/src/generated-file.js +24 -0
- package/dist/src/graph.js +1 -1
- package/dist/src/interface-satisfaction.js +2 -68
- package/dist/src/inventory.js +17 -4
- package/dist/src/kinds.js +4 -3
- package/dist/src/manifest.js +29 -4
- package/dist/src/outside-tests.js +139 -0
- package/dist/src/overrides.js +104 -0
- package/dist/src/paths.js +17 -0
- package/dist/src/query.js +15 -0
- package/dist/src/references.js +229 -11
- package/dist/src/resolve.js +12 -11
- package/dist/src/roots.js +121 -37
- package/dist/src/session.js +4 -3
- package/dist/src/stores.js +3 -0
- package/dist/src/template-field.js +60 -3
- package/dist/src/value-flow.js +74 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +194 -49
- package/src/configuration-files.ts +137 -2
- package/src/conformance-record.ts +3 -10
- package/src/contextual-uses.ts +358 -0
- package/src/convention-rows.ts +8 -0
- package/src/conventions.ts +161 -13
- package/src/dependencies.ts +14 -1
- package/src/destructuring.ts +16 -6
- package/src/entry-points.ts +16 -3
- package/src/exempt-classes.ts +6 -1
- package/src/exempt.ts +67 -16
- package/src/file-facts.ts +26 -20
- package/src/findings/completion.ts +31 -3
- package/src/findings/interfaces.ts +9 -2
- package/src/findings/reads-and-writes.ts +70 -9
- package/src/findings/unused-declarations.ts +51 -1
- package/src/generated-file.ts +27 -0
- package/src/graph.ts +4 -4
- package/src/interface-satisfaction.ts +1 -92
- package/src/inventory.ts +19 -4
- package/src/kinds.ts +5 -3
- package/src/manifest.ts +40 -4
- package/src/outside-tests.ts +191 -0
- package/src/overrides.ts +136 -0
- package/src/paths.ts +18 -0
- package/src/query.ts +26 -0
- package/src/references.ts +294 -22
- package/src/resolve.ts +14 -13
- package/src/roots.ts +152 -44
- package/src/session.ts +4 -2
- package/src/stores.ts +9 -0
- package/src/template-field.ts +65 -3
- package/src/value-flow.ts +104 -0
- package/src/version.ts +1 -1
package/src/conventions.ts
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
isNamespaceImport,
|
|
23
23
|
isNoSubstitutionTemplateLiteral,
|
|
24
24
|
isNonNullExpression,
|
|
25
|
+
isNumericLiteral,
|
|
26
|
+
isObjectBindingPattern,
|
|
25
27
|
isObjectLiteralExpression,
|
|
26
28
|
isParenthesizedExpression,
|
|
27
29
|
isPropertyAccessExpression,
|
|
@@ -33,7 +35,9 @@ import {
|
|
|
33
35
|
isVariableStatement,
|
|
34
36
|
SyntaxKind,
|
|
35
37
|
type AsExpression,
|
|
38
|
+
type Identifier,
|
|
36
39
|
type Node,
|
|
40
|
+
type ObjectBindingPattern,
|
|
37
41
|
type NonNullExpression,
|
|
38
42
|
type ObjectLiteralExpression,
|
|
39
43
|
type ParenthesizedExpression,
|
|
@@ -81,6 +85,14 @@ interface ConventionGlob {
|
|
|
81
85
|
readonly excludes: readonly RegExp[];
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
/** One directory an applied row names as generated, read against its manifest's directory. */
|
|
89
|
+
interface GeneratedDirectory {
|
|
90
|
+
readonly dir: string;
|
|
91
|
+
readonly row: string;
|
|
92
|
+
/** The directory below the manifest's directory, `/`-separated. */
|
|
93
|
+
readonly below: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
84
96
|
/** A setup failure a row met, at the directory of the manifest that declared it. */
|
|
85
97
|
interface ConventionFailure {
|
|
86
98
|
readonly dir: string;
|
|
@@ -92,6 +104,7 @@ export interface Conventions {
|
|
|
92
104
|
/** The rows applied, ordered by name, then manifest, then their compact encoding. */
|
|
93
105
|
readonly applied: readonly AppliedConvention[];
|
|
94
106
|
readonly globs: readonly ConventionGlob[];
|
|
107
|
+
readonly generated: readonly GeneratedDirectory[];
|
|
95
108
|
readonly failures: readonly ConventionFailure[];
|
|
96
109
|
}
|
|
97
110
|
|
|
@@ -105,9 +118,11 @@ interface ConventionEntry {
|
|
|
105
118
|
/** Parses one file's text with no program. */
|
|
106
119
|
type ParseSource = (fileName: string, text: string) => SourceFile;
|
|
107
120
|
|
|
121
|
+
/** The sections an installed package is required by, so its absence fails the run. */
|
|
122
|
+
const REQUIRED_SECTIONS = ["dependencies", "devDependencies"] as const;
|
|
123
|
+
|
|
108
124
|
const DEPENDENCY_SECTIONS = [
|
|
109
|
-
|
|
110
|
-
"devDependencies",
|
|
125
|
+
...REQUIRED_SECTIONS,
|
|
111
126
|
"peerDependencies",
|
|
112
127
|
"optionalDependencies",
|
|
113
128
|
] as const;
|
|
@@ -124,11 +139,15 @@ function readJSON(host: Host, path: string): unknown {
|
|
|
124
139
|
}
|
|
125
140
|
}
|
|
126
141
|
|
|
127
|
-
function declares(
|
|
142
|
+
function declares(
|
|
143
|
+
manifest: unknown,
|
|
144
|
+
name: string,
|
|
145
|
+
sections: readonly string[] = DEPENDENCY_SECTIONS,
|
|
146
|
+
): boolean {
|
|
128
147
|
if (!isRecord(manifest)) {
|
|
129
148
|
return false;
|
|
130
149
|
}
|
|
131
|
-
return
|
|
150
|
+
return sections.some((section) => {
|
|
132
151
|
const held = manifest[section];
|
|
133
152
|
return isRecord(held) && Object.hasOwn(held, name);
|
|
134
153
|
});
|
|
@@ -174,21 +193,33 @@ function unwrapped(node: Node): Node {
|
|
|
174
193
|
return at;
|
|
175
194
|
}
|
|
176
195
|
|
|
177
|
-
/** The key one property is written with, where it is written as a name or
|
|
196
|
+
/** The key one property is written with, where it is written as a name or as literal text. */
|
|
178
197
|
function keyOf(property: Node): string | undefined {
|
|
179
198
|
if (!isPropertyAssignment(property) && !isShorthandPropertyAssignment(property)) {
|
|
180
199
|
return undefined;
|
|
181
200
|
}
|
|
182
201
|
const { name } = property;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
202
|
+
const key = isComputedPropertyName(name) ? name.expression : name;
|
|
203
|
+
if (
|
|
204
|
+
(isIdentifier(key) && key === name) ||
|
|
205
|
+
isStringLiteral(key) ||
|
|
206
|
+
isNoSubstitutionTemplateLiteral(key)
|
|
207
|
+
) {
|
|
208
|
+
return key.text;
|
|
188
209
|
}
|
|
189
210
|
return undefined;
|
|
190
211
|
}
|
|
191
212
|
|
|
213
|
+
/** Whether one member's computed name is no literal, so it may set any key. */
|
|
214
|
+
function computesKey(member: Node): boolean {
|
|
215
|
+
const name = (member as { readonly name?: Node }).name;
|
|
216
|
+
if (name === undefined || !isComputedPropertyName(name)) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
const key = name.expression;
|
|
220
|
+
return !(isStringLiteral(key) || isNumericLiteral(key) || isNoSubstitutionTemplateLiteral(key));
|
|
221
|
+
}
|
|
222
|
+
|
|
192
223
|
/** The values one file's top-level variable declarations bind, by name. */
|
|
193
224
|
function declaredValues(file: SourceFile): ReadonlyMap<string, Node> {
|
|
194
225
|
const values = new Map<string, Node>();
|
|
@@ -247,6 +278,79 @@ function defaultExports(file: SourceFile): readonly Node[] {
|
|
|
247
278
|
return found;
|
|
248
279
|
}
|
|
249
280
|
|
|
281
|
+
/** The declarations whose name is a binding of the scope they are written in. */
|
|
282
|
+
const BINDING_KINDS: ReadonlySet<SyntaxKind> = new Set([
|
|
283
|
+
SyntaxKind.VariableDeclaration,
|
|
284
|
+
SyntaxKind.Parameter,
|
|
285
|
+
SyntaxKind.BindingElement,
|
|
286
|
+
SyntaxKind.FunctionDeclaration,
|
|
287
|
+
SyntaxKind.ClassDeclaration,
|
|
288
|
+
SyntaxKind.ImportClause,
|
|
289
|
+
SyntaxKind.ImportSpecifier,
|
|
290
|
+
SyntaxKind.NamespaceImport,
|
|
291
|
+
SyntaxKind.ImportEqualsDeclaration,
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
/** Whether one file declares a binding named `require` at any depth, which shadows the global. */
|
|
295
|
+
function declaresRequire(file: SourceFile): boolean {
|
|
296
|
+
let found = false;
|
|
297
|
+
const visit = (node: Node): void => {
|
|
298
|
+
const name = (node as { readonly name?: Node }).name;
|
|
299
|
+
if (
|
|
300
|
+
name !== undefined &&
|
|
301
|
+
isIdentifier(name) &&
|
|
302
|
+
name.text === "require" &&
|
|
303
|
+
BINDING_KINDS.has(node.kind)
|
|
304
|
+
) {
|
|
305
|
+
found = true;
|
|
306
|
+
}
|
|
307
|
+
if (!found) {
|
|
308
|
+
node.forEachChild(visit);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
file.forEachChild(visit);
|
|
312
|
+
return found;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The top-level `const`, `let` and `var` declarations that bind a `require` of one module,
|
|
317
|
+
* written with the module's name as its one string-literal argument, in a file that
|
|
318
|
+
* declares no binding named `require`: each binds as an import of the module does.
|
|
319
|
+
*/
|
|
320
|
+
function requireBindings(
|
|
321
|
+
file: SourceFile,
|
|
322
|
+
module: string,
|
|
323
|
+
): readonly (Identifier | ObjectBindingPattern)[] {
|
|
324
|
+
const found: (Identifier | ObjectBindingPattern)[] = [];
|
|
325
|
+
for (const statement of file.statements) {
|
|
326
|
+
if (!isVariableStatement(statement)) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
330
|
+
const value =
|
|
331
|
+
declaration.initializer === undefined ? undefined : unwrapped(declaration.initializer);
|
|
332
|
+
const [argument, ...rest] =
|
|
333
|
+
value !== undefined && isCallExpression(value) ? value.arguments : [];
|
|
334
|
+
if (
|
|
335
|
+
value === undefined ||
|
|
336
|
+
!isCallExpression(value) ||
|
|
337
|
+
!isIdentifier(value.expression) ||
|
|
338
|
+
value.expression.text !== "require" ||
|
|
339
|
+
argument === undefined ||
|
|
340
|
+
rest.length > 0 ||
|
|
341
|
+
!isStringLiteral(argument) ||
|
|
342
|
+
argument.text !== module
|
|
343
|
+
) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (isIdentifier(declaration.name) || isObjectBindingPattern(declaration.name)) {
|
|
347
|
+
found.push(declaration.name);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return found.length > 0 && declaresRequire(file) ? [] : found;
|
|
352
|
+
}
|
|
353
|
+
|
|
250
354
|
/**
|
|
251
355
|
* The first argument of every call one file makes to one module's export, through a
|
|
252
356
|
* named or a namespace import; `undefined` for a call with no argument.
|
|
@@ -254,6 +358,26 @@ function defaultExports(file: SourceFile): readonly Node[] {
|
|
|
254
358
|
function optionsArguments(file: SourceFile, call: OptionsCall): readonly (Node | undefined)[] {
|
|
255
359
|
const names = new Set<string>();
|
|
256
360
|
const namespaces = new Set<string>();
|
|
361
|
+
for (const name of requireBindings(file, call.module)) {
|
|
362
|
+
if (isIdentifier(name)) {
|
|
363
|
+
namespaces.add(name.text);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
for (const element of name.elements) {
|
|
367
|
+
const local = element.name;
|
|
368
|
+
const exported = element.propertyName ?? local;
|
|
369
|
+
if (
|
|
370
|
+
element.dotDotDotToken === undefined &&
|
|
371
|
+
local !== undefined &&
|
|
372
|
+
isIdentifier(local) &&
|
|
373
|
+
exported !== undefined &&
|
|
374
|
+
(isIdentifier(exported) || isStringLiteral(exported)) &&
|
|
375
|
+
exported.text === call.export
|
|
376
|
+
) {
|
|
377
|
+
names.add(local.text);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
257
381
|
for (const statement of file.statements) {
|
|
258
382
|
const bindings = isImportDeclaration(statement)
|
|
259
383
|
? statement.importClause?.namedBindings
|
|
@@ -333,7 +457,7 @@ function readPath(
|
|
|
333
457
|
): Node | undefined {
|
|
334
458
|
const [key, ...rest] = keys;
|
|
335
459
|
for (const member of object.properties) {
|
|
336
|
-
if (isSpreadAssignment(member)) {
|
|
460
|
+
if (isSpreadAssignment(member) || computesKey(member)) {
|
|
337
461
|
return member;
|
|
338
462
|
}
|
|
339
463
|
if (keyOf(member) !== key) {
|
|
@@ -518,6 +642,7 @@ export function readConventions(
|
|
|
518
642
|
): Conventions {
|
|
519
643
|
const applied: AppliedConvention[] = [];
|
|
520
644
|
const globs: ConventionGlob[] = [];
|
|
645
|
+
const generated: GeneratedDirectory[] = [];
|
|
521
646
|
const failures: ConventionFailure[] = [];
|
|
522
647
|
for (const manifest of manifests) {
|
|
523
648
|
const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
|
|
@@ -532,7 +657,7 @@ export function readConventions(
|
|
|
532
657
|
}
|
|
533
658
|
const version = versions.get(row.package);
|
|
534
659
|
if (version === undefined) {
|
|
535
|
-
if (missing.has(row.package)) {
|
|
660
|
+
if (missing.has(row.package) || !declares(declared, row.package, REQUIRED_SECTIONS)) {
|
|
536
661
|
continue;
|
|
537
662
|
}
|
|
538
663
|
missing.add(row.package);
|
|
@@ -580,6 +705,9 @@ export function readConventions(
|
|
|
580
705
|
const excludes = [
|
|
581
706
|
...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
|
|
582
707
|
].map(globExpression);
|
|
708
|
+
for (const below of new Set((row.generated ?? []).flatMap((one) => expand(one, resolved)))) {
|
|
709
|
+
generated.push({ dir: manifest.dir, row: row.name, below });
|
|
710
|
+
}
|
|
583
711
|
for (const glob of new Set(row.entries.flatMap((entry) => expand(entry, resolved)))) {
|
|
584
712
|
globs.push({
|
|
585
713
|
dir: manifest.dir,
|
|
@@ -597,7 +725,27 @@ export function readConventions(
|
|
|
597
725
|
compare(a.manifest, b.manifest) ||
|
|
598
726
|
compare(JSON.stringify(a), JSON.stringify(b)),
|
|
599
727
|
);
|
|
600
|
-
return { applied: unique, globs, failures };
|
|
728
|
+
return { applied: unique, globs, generated, failures };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Every own file of one project below a directory an applied row names as generated, by
|
|
733
|
+
* its path below the target root, with the name of the first such row.
|
|
734
|
+
*/
|
|
735
|
+
export function generatedFiles(
|
|
736
|
+
conventions: Conventions,
|
|
737
|
+
files: SourceFiles,
|
|
738
|
+
): ReadonlyMap<string, string> {
|
|
739
|
+
const found = new Map<string, string>();
|
|
740
|
+
for (const { dir, row, below } of conventions.generated) {
|
|
741
|
+
const root = joinPath(dir, below);
|
|
742
|
+
for (const [path, file] of files.byPath) {
|
|
743
|
+
if (!found.has(path) && relativePath(root, file.fileName) !== undefined) {
|
|
744
|
+
found.set(path, row);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return found;
|
|
601
749
|
}
|
|
602
750
|
|
|
603
751
|
/** Every own file of one project an applied row's globs match, once per row. */
|
package/src/dependencies.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
declaredIn as declaredBeside,
|
|
28
28
|
dependenciesNamed,
|
|
29
29
|
moduleStrings,
|
|
30
|
+
type ParseFile,
|
|
30
31
|
} from "./configuration-files.ts";
|
|
31
32
|
import type { FindingPosition } from "./finding.ts";
|
|
32
33
|
import type { Host } from "./host.ts";
|
|
@@ -284,6 +285,7 @@ export function projectNeeds<Brand>(
|
|
|
284
285
|
held: Inventory,
|
|
285
286
|
host: Host,
|
|
286
287
|
targetRoot: string,
|
|
288
|
+
parse?: ParseFile,
|
|
287
289
|
): ProjectNeeds {
|
|
288
290
|
const programPackages = new Set<string>();
|
|
289
291
|
for (const name of project.program.getSourceFileNames()) {
|
|
@@ -329,7 +331,12 @@ export function projectNeeds<Brand>(
|
|
|
329
331
|
}
|
|
330
332
|
return names;
|
|
331
333
|
};
|
|
332
|
-
const configuration = configurationFiles(
|
|
334
|
+
const configuration = configurationFiles(
|
|
335
|
+
host,
|
|
336
|
+
targetRoot,
|
|
337
|
+
sourceFilesOf(project, targetRoot),
|
|
338
|
+
parse,
|
|
339
|
+
);
|
|
333
340
|
for (const { file } of configuration.modules) {
|
|
334
341
|
const names = declaredAt(file.fileName);
|
|
335
342
|
for (const { node, text } of moduleStrings(file)) {
|
|
@@ -342,6 +349,12 @@ export function projectNeeds<Brand>(
|
|
|
342
349
|
use([PROJECT_OWNER], dependenciesNamed(text, names));
|
|
343
350
|
}
|
|
344
351
|
}
|
|
352
|
+
for (const { file, manifestDir } of configuration.outside) {
|
|
353
|
+
const names = declaredAt(joinPath(manifestDir, "package.json"));
|
|
354
|
+
for (const { text } of moduleStrings(file)) {
|
|
355
|
+
use([PROJECT_OWNER], dependenciesNamed(text, names));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
345
358
|
return { packages, uses, unanswered };
|
|
346
359
|
}
|
|
347
360
|
|
package/src/destructuring.ts
CHANGED
|
@@ -25,7 +25,13 @@ import {
|
|
|
25
25
|
type Node,
|
|
26
26
|
} from "@typescript/native/unstable/ast";
|
|
27
27
|
import { TypeFlags, type Symbol as TSSymbol, type Type } from "@typescript/native/unstable/sync";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
isAnswered,
|
|
30
|
+
propertyTables,
|
|
31
|
+
UNANSWERED,
|
|
32
|
+
type Answer,
|
|
33
|
+
type PropertyTables,
|
|
34
|
+
} from "./query.ts";
|
|
29
35
|
import type { ProjectView } from "./session.ts";
|
|
30
36
|
|
|
31
37
|
/** One property one object pattern reads. */
|
|
@@ -228,7 +234,11 @@ type Found = Answer<Type | undefined>;
|
|
|
228
234
|
* read once per type for the whole project, since one type is destructured in many
|
|
229
235
|
* places and a table answers every name of it.
|
|
230
236
|
*/
|
|
231
|
-
export function destructuring<Brand>(
|
|
237
|
+
export function destructuring<Brand>(
|
|
238
|
+
project: ProjectView<Brand>,
|
|
239
|
+
cap: number,
|
|
240
|
+
properties: PropertyTables = propertyTables(project.queries),
|
|
241
|
+
): Destructuring {
|
|
232
242
|
const queries = project.queries;
|
|
233
243
|
const tables = new Map<number, Answer<ReadonlyMap<string, TSSymbol>>>();
|
|
234
244
|
let patternBatches = 0;
|
|
@@ -241,10 +251,10 @@ export function destructuring<Brand>(project: ProjectView<Brand>, cap: number):
|
|
|
241
251
|
}
|
|
242
252
|
let table: Answer<ReadonlyMap<string, TSSymbol>> = new Map<string, TSSymbol>();
|
|
243
253
|
if ((type.flags & TypeFlags.AnyOrUnknown) === 0) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
table = isAnswered(properties)
|
|
247
|
-
? new Map(properties.map((property) => [property.name, property]))
|
|
254
|
+
const read = properties.of(type);
|
|
255
|
+
patternLookups += read.asked ? 1 : 0;
|
|
256
|
+
table = isAnswered(read.properties)
|
|
257
|
+
? new Map(read.properties.map((property) => [property.name, property]))
|
|
248
258
|
: UNANSWERED;
|
|
249
259
|
}
|
|
250
260
|
tables.set(type.id, table);
|
package/src/entry-points.ts
CHANGED
|
@@ -30,7 +30,12 @@ import {
|
|
|
30
30
|
type SourceFile,
|
|
31
31
|
} from "@typescript/native/unstable/ast";
|
|
32
32
|
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
configurationFiles,
|
|
35
|
+
filesNamedBy,
|
|
36
|
+
moduleStrings,
|
|
37
|
+
type ParseFile,
|
|
38
|
+
} from "./configuration-files.ts";
|
|
34
39
|
import { globExpression } from "./glob.ts";
|
|
35
40
|
import type { Host } from "./host.ts";
|
|
36
41
|
import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
|
|
@@ -260,8 +265,9 @@ function configurationEntries(
|
|
|
260
265
|
host: Host,
|
|
261
266
|
targetRoot: string,
|
|
262
267
|
files: SourceFiles,
|
|
268
|
+
parse: ParseFile | undefined,
|
|
263
269
|
): readonly EntryPoint[] {
|
|
264
|
-
const { modules, documents } = configurationFiles(host, targetRoot, files);
|
|
270
|
+
const { modules, documents, outside } = configurationFiles(host, targetRoot, files, parse);
|
|
265
271
|
const found: EntryPoint[] = [];
|
|
266
272
|
const strings = (dir: string, texts: readonly string[]): void => {
|
|
267
273
|
for (const text of texts) {
|
|
@@ -280,6 +286,12 @@ function configurationEntries(
|
|
|
280
286
|
for (const document of documents) {
|
|
281
287
|
strings(dirnamePath(document.path), document.strings);
|
|
282
288
|
}
|
|
289
|
+
for (const { file } of outside) {
|
|
290
|
+
strings(
|
|
291
|
+
dirnamePath(file.fileName),
|
|
292
|
+
moduleStrings(file).map((one) => one.text),
|
|
293
|
+
);
|
|
294
|
+
}
|
|
283
295
|
return found;
|
|
284
296
|
}
|
|
285
297
|
|
|
@@ -507,8 +519,9 @@ export function entryPoints<Brand>(
|
|
|
507
519
|
testFiles: readonly string[],
|
|
508
520
|
host: Host,
|
|
509
521
|
targetRoot: string,
|
|
522
|
+
parse?: ParseFile,
|
|
510
523
|
): readonly EntryPoint[] {
|
|
511
|
-
const found: EntryPoint[] = [...configurationEntries(host, targetRoot, files)];
|
|
524
|
+
const found: EntryPoint[] = [...configurationEntries(host, targetRoot, files, parse)];
|
|
512
525
|
const tests = testFiles.map((pattern) => ({ pattern, expression: globExpression(pattern) }));
|
|
513
526
|
for (const [path, file] of files.byPath) {
|
|
514
527
|
const name = basename(path);
|
package/src/exempt-classes.ts
CHANGED
|
@@ -49,7 +49,12 @@ export const EXEMPTION_CLASSES = [
|
|
|
49
49
|
confidence: "certain",
|
|
50
50
|
typescriptVisibility: { private: false, privateName: false },
|
|
51
51
|
},
|
|
52
|
-
{
|
|
52
|
+
{
|
|
53
|
+
class: "generated-file",
|
|
54
|
+
languages: ["go", "ts"],
|
|
55
|
+
confidence: "certain",
|
|
56
|
+
typescriptVisibility: { private: true, privateName: false },
|
|
57
|
+
},
|
|
53
58
|
{ class: "linkname-cgo-asm-plugin", languages: ["go"], confidence: "certain" },
|
|
54
59
|
{
|
|
55
60
|
class: "template-field",
|
package/src/exempt.ts
CHANGED
|
@@ -48,6 +48,11 @@ export interface DetectorInput<Brand> {
|
|
|
48
48
|
readonly ts: TSSection;
|
|
49
49
|
/** The absolute path of each declared consumer, whose modules are part of the analysis. */
|
|
50
50
|
readonly consumers: readonly string[];
|
|
51
|
+
/**
|
|
52
|
+
* The project's generated files, by path below the target root, each with the
|
|
53
|
+
* convention row that names its directory as generated.
|
|
54
|
+
*/
|
|
55
|
+
readonly generated?: ReadonlyMap<string, string>;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
/**
|
|
@@ -161,12 +166,44 @@ export function computeExemptions<Brand>(
|
|
|
161
166
|
input: DetectorInput<Brand>,
|
|
162
167
|
detectors: Detectors,
|
|
163
168
|
holding: Holding,
|
|
169
|
+
): readonly Exemption[] {
|
|
170
|
+
return detectExemptions(input, detectors, holding).records;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** One project's records, and the evidence a production mode found in a test file. */
|
|
174
|
+
export interface Detected {
|
|
175
|
+
readonly records: readonly Exemption[];
|
|
176
|
+
/**
|
|
177
|
+
* Under a production mode, each record whose evidence is written in a test file. It
|
|
178
|
+
* holds nothing, and it is a test reference to its declaration, as a reference the
|
|
179
|
+
* test file wrote would be.
|
|
180
|
+
*/
|
|
181
|
+
readonly inTestFiles: readonly Exemption[];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The records of {@link computeExemptions}, beside the test-file evidence it set aside. */
|
|
185
|
+
export function detectExemptions<Brand>(
|
|
186
|
+
input: DetectorInput<Brand>,
|
|
187
|
+
detectors: Detectors,
|
|
188
|
+
holding: Holding,
|
|
189
|
+
): Detected {
|
|
190
|
+
return heldRecords(evidenceRecords(input, detectors, holding.disabled), holding);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Every record one project's detectors found, one per piece of evidence, before the
|
|
195
|
+
* mode decides which of them hold: what {@link heldRecords} reads.
|
|
196
|
+
*/
|
|
197
|
+
export function evidenceRecords<Brand>(
|
|
198
|
+
input: DetectorInput<Brand>,
|
|
199
|
+
detectors: Detectors,
|
|
200
|
+
disabled: ReadonlySet<ExemptionClass>,
|
|
164
201
|
): readonly Exemption[] {
|
|
165
202
|
const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
|
|
166
203
|
const found: Exemption[] = [];
|
|
167
204
|
for (const exemptionClass of TS_EXEMPTION_CLASSES) {
|
|
168
205
|
const detect = detectors.get(exemptionClass);
|
|
169
|
-
if (detect === undefined ||
|
|
206
|
+
if (detect === undefined || disabled.has(exemptionClass)) {
|
|
170
207
|
continue;
|
|
171
208
|
}
|
|
172
209
|
const allowed = typescriptVisibilityOf(exemptionClass);
|
|
@@ -174,9 +211,6 @@ export function computeExemptions<Brand>(
|
|
|
174
211
|
if (!retainable(allowed, visibility.get(evidence.id))) {
|
|
175
212
|
continue;
|
|
176
213
|
}
|
|
177
|
-
if (holding.mode.production && holding.testFiles.has(evidence.site.path)) {
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
214
|
found.push({
|
|
181
215
|
id: evidence.id,
|
|
182
216
|
class: exemptionClass,
|
|
@@ -186,7 +220,21 @@ export function computeExemptions<Brand>(
|
|
|
186
220
|
});
|
|
187
221
|
}
|
|
188
222
|
}
|
|
189
|
-
return
|
|
223
|
+
return found;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The records that hold under one mode, beside the evidence a production mode found in a test file. */
|
|
227
|
+
export function heldRecords(records: readonly Exemption[], holding: Holding): Detected {
|
|
228
|
+
const found: Exemption[] = [];
|
|
229
|
+
const inTestFiles: Exemption[] = [];
|
|
230
|
+
for (const record of records) {
|
|
231
|
+
if (holding.mode.production && holding.testFiles.has(record.site.path)) {
|
|
232
|
+
inTestFiles.push(record);
|
|
233
|
+
} else {
|
|
234
|
+
found.push(record);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { records: exemptionsOf(found), inTestFiles };
|
|
190
238
|
}
|
|
191
239
|
|
|
192
240
|
/** Two strings ordered bytewise. */
|
|
@@ -247,17 +295,18 @@ function byDeclaration(records: readonly Exemption[]): ReadonlyMap<string, reado
|
|
|
247
295
|
}
|
|
248
296
|
|
|
249
297
|
/**
|
|
250
|
-
* The records that held back a declaration some configuration
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* on a declaration that a relation or a mark holds live in every configuration held
|
|
257
|
-
* nothing back. The order is the declarations' site order, and for one declaration
|
|
258
|
-
* the input's.
|
|
298
|
+
* The records that held back a declaration: one some configuration's graph, swept
|
|
299
|
+
* again with no exemption and its marks, judges a candidate, whatever another
|
|
300
|
+
* configuration answers, or one in `writeOnly`, which the run would report as written
|
|
301
|
+
* and never read without the record's read. A declaration a relation or a mark holds
|
|
302
|
+
* live in every configuration is held back by no record. The order is the
|
|
303
|
+
* declarations' site order, and for one declaration the input's.
|
|
259
304
|
*/
|
|
260
|
-
export function retainedIn(
|
|
305
|
+
export function retainedIn(
|
|
306
|
+
matrix: Matrix,
|
|
307
|
+
input: SweepInput,
|
|
308
|
+
writeOnly: ReadonlySet<string> = new Set(),
|
|
309
|
+
): readonly Exemption[] {
|
|
261
310
|
const exempt = input.exempt ?? [];
|
|
262
311
|
if (exempt.length === 0) {
|
|
263
312
|
return [];
|
|
@@ -268,7 +317,9 @@ export function retainedIn(matrix: Matrix, input: SweepInput): readonly Exemptio
|
|
|
268
317
|
sweep(graph, bare).candidates.map((candidate) => candidate.id),
|
|
269
318
|
),
|
|
270
319
|
);
|
|
271
|
-
const held = byDeclaration(
|
|
320
|
+
const held = byDeclaration(
|
|
321
|
+
exempt.filter((record) => dead.has(record.id) || writeOnly.has(record.id)),
|
|
322
|
+
);
|
|
272
323
|
return matrix.union.symbols.flatMap((symbol) => held.get(symbol.id) ?? []);
|
|
273
324
|
}
|
|
274
325
|
|
package/src/file-facts.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface FileFacts {
|
|
|
47
47
|
* reference names is included by the file that names it.
|
|
48
48
|
*/
|
|
49
49
|
readonly heldByInclusion: ReadonlySet<string>;
|
|
50
|
+
/** The paths below the target root of the files below a directory an applied row names as generated. */
|
|
51
|
+
readonly generated?: ReadonlySet<string>;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/** The line of the last character of one text, counted from one; an empty text is one line. */
|
|
@@ -63,13 +65,11 @@ function lastLine(text: string): number {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/**
|
|
66
|
-
*
|
|
67
|
-
* read is skipped, as discovery skips it
|
|
68
|
-
* with it: a tree the analysis may not enter holds no file it can report.
|
|
68
|
+
* The path below the target root of every source file there, in path order. A directory
|
|
69
|
+
* that cannot be read is skipped, as discovery skips it.
|
|
69
70
|
*/
|
|
70
|
-
export function
|
|
71
|
-
const
|
|
72
|
-
const found: TreeFile[] = [];
|
|
71
|
+
export function sourcePaths(host: Host, targetRoot: string): readonly string[] {
|
|
72
|
+
const found: string[] = [];
|
|
73
73
|
const pending = [""];
|
|
74
74
|
for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
|
|
75
75
|
let entries: readonly { readonly name: string; readonly directory: boolean }[];
|
|
@@ -86,23 +86,29 @@ export function sourceTree(host: Host, targetRoot: string): readonly TreeFile[]
|
|
|
86
86
|
}
|
|
87
87
|
continue;
|
|
88
88
|
}
|
|
89
|
-
if (
|
|
90
|
-
|
|
89
|
+
if (SOURCE_FILE.test(entry.name) && !DECLARATION_FILE.test(entry.name)) {
|
|
90
|
+
found.push(path);
|
|
91
91
|
}
|
|
92
|
-
let text: string;
|
|
93
|
-
try {
|
|
94
|
-
text = host.readFile(joinPath(targetRoot, path));
|
|
95
|
-
} catch {
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
found.push({
|
|
99
|
-
path,
|
|
100
|
-
ref: renderRef(scopeOf(path), { of: "module" }),
|
|
101
|
-
endLine: lastLine(text),
|
|
102
|
-
});
|
|
103
92
|
}
|
|
104
93
|
}
|
|
105
|
-
return found.sort((a, b) => (a
|
|
94
|
+
return found.sort((a, b) => (a < b ? -1 : 1));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Every source file below the target root, in path order. A file that cannot be read is
|
|
99
|
+
* skipped: a tree the analysis may not enter holds no file it can report.
|
|
100
|
+
*/
|
|
101
|
+
export function sourceTree(host: Host, targetRoot: string): readonly TreeFile[] {
|
|
102
|
+
const scopeOf = packageScope(host, targetRoot);
|
|
103
|
+
return sourcePaths(host, targetRoot).flatMap((path) => {
|
|
104
|
+
let text: string;
|
|
105
|
+
try {
|
|
106
|
+
text = host.readFile(joinPath(targetRoot, path));
|
|
107
|
+
} catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
return [{ path, ref: renderRef(scopeOf(path), { of: "module" }), endLine: lastLine(text) }];
|
|
111
|
+
});
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
/** Whether one file changes what the program sees by being included in it. */
|