@cplieger/deadset-ts 8.0.0-dev.2 → 8.0.0-dev.4
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 +61 -10
- package/dist/src/augmentations.js +63 -1
- package/dist/src/build-scripts.js +437 -0
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +56 -9
- package/dist/src/convention-rows.js +39 -2
- package/dist/src/conventions.js +15 -0
- package/dist/src/dependencies.js +98 -1
- package/dist/src/discover.js +14 -82
- package/dist/src/entry-points.js +9 -18
- package/dist/src/exempt.js +8 -0
- package/dist/src/findings/completion.js +66 -28
- package/dist/src/findings/intra-function.js +4 -2
- package/dist/src/findings/reads-and-writes.js +7 -6
- package/dist/src/findings/unused-declarations.js +4 -1
- package/dist/src/graph.js +5 -3
- package/dist/src/intra-function-parts.js +45 -17
- package/dist/src/kinds.js +8 -8
- package/dist/src/manifest.js +18 -0
- package/dist/src/matrix.js +38 -4
- package/dist/src/query.js +1 -0
- package/dist/src/report.js +23 -4
- package/dist/src/reporters/annotations.js +15 -8
- package/dist/src/reporters/sarif.js +12 -8
- package/dist/src/roots.js +197 -42
- package/dist/src/serialization-contract.js +590 -12
- package/dist/src/sweep.js +7 -9
- package/dist/src/test-globals.js +234 -0
- package/dist/src/type-errors.js +11 -3
- package/dist/src/unseen-loads.js +107 -0
- package/dist/src/verbs/analyze.js +24 -3
- package/dist/src/workflow-steps.js +32 -15
- package/dist/src/yaml-document.js +355 -0
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +109 -12
- package/src/augmentations.ts +77 -2
- package/src/build-scripts.ts +564 -0
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +64 -12
- package/src/convention-rows.ts +47 -2
- package/src/conventions.ts +16 -0
- package/src/dependencies.ts +110 -1
- package/src/discover.ts +14 -94
- package/src/entry-points.ts +10 -25
- package/src/exempt.ts +16 -0
- package/src/file-facts.ts +3 -0
- package/src/findings/boundary.ts +2 -0
- package/src/findings/completion.ts +76 -30
- package/src/findings/intra-function.ts +4 -2
- package/src/findings/reads-and-writes.ts +7 -6
- package/src/findings/unused-declarations.ts +6 -1
- package/src/graph.ts +5 -3
- package/src/intra-function-parts.ts +55 -16
- package/src/kinds.ts +8 -8
- package/src/manifest.ts +21 -3
- package/src/matrix.ts +51 -4
- package/src/query.ts +11 -0
- package/src/report.ts +56 -16
- package/src/reporters/annotations.ts +21 -10
- package/src/reporters/reporter.ts +1 -3
- package/src/reporters/sarif.ts +16 -8
- package/src/roots.ts +226 -46
- package/src/serialization-contract.ts +758 -12
- package/src/sweep.ts +23 -9
- package/src/test-globals.ts +275 -0
- package/src/type-errors.ts +14 -3
- package/src/unseen-loads.ts +145 -0
- package/src/verbs/analyze.ts +28 -3
- package/src/workflow-steps.ts +35 -17
- package/src/yaml-document.ts +377 -0
package/src/contextual-uses.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The uses contextual types make of the target's members, which no name node resolves to.
|
|
3
3
|
* An object-literal property writes the member of its name on each constituent of the
|
|
4
|
-
* literal's contextual type that declares one
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the
|
|
4
|
+
* literal's contextual type that declares one, and so does a property of a literal whose
|
|
5
|
+
* type is inferred, where a value of that type reaches such a position. A value reaching a
|
|
6
|
+
* position typed by another object type reads each of its members that type declares,
|
|
7
|
+
* through array elements and the parameters and results of functions and constructors
|
|
8
|
+
* too; a class instance there is the interface-satisfaction class's evidence instead.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import {
|
|
@@ -109,10 +110,12 @@ interface Checked {
|
|
|
109
110
|
readonly source: Type;
|
|
110
111
|
readonly targets: readonly Type[];
|
|
111
112
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
113
|
+
* What the source is: a type whose members the target declares, which the position
|
|
114
|
+
* reads; one declared outside the target, so code the analysis does not hold reads the
|
|
115
|
+
* members of the target's types it declares; or an object literal's own type, whose
|
|
116
|
+
* properties write the position's members of their names.
|
|
114
117
|
*/
|
|
115
|
-
readonly
|
|
118
|
+
readonly sourceKind: "members" | "outside" | "literal";
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
/** One member of an object type, by the inventory's declarations and the checker's declaration handles. */
|
|
@@ -287,6 +290,37 @@ export function contextualUses<Brand>(
|
|
|
287
290
|
return members.length > 0 && members.every((member) => !member.own);
|
|
288
291
|
};
|
|
289
292
|
|
|
293
|
+
const literalTypes = new Map<number, boolean>();
|
|
294
|
+
/**
|
|
295
|
+
* Whether an object type is an object literal's own, inferred where no contextual type
|
|
296
|
+
* names it: every member a property an object literal of the target writes.
|
|
297
|
+
*/
|
|
298
|
+
const literalTyped = (part: Type): boolean => {
|
|
299
|
+
let known = literalTypes.get(part.id);
|
|
300
|
+
if (known === undefined) {
|
|
301
|
+
const members = [...membersOf(part).values()];
|
|
302
|
+
known =
|
|
303
|
+
members.length > 0 &&
|
|
304
|
+
members.every(
|
|
305
|
+
(member) =>
|
|
306
|
+
member.ids.length === 0 &&
|
|
307
|
+
member.declarations.length > 0 &&
|
|
308
|
+
member.declarations.every((handle) => literalProperty(handle) !== undefined),
|
|
309
|
+
);
|
|
310
|
+
literalTypes.set(part.id, known);
|
|
311
|
+
}
|
|
312
|
+
return known;
|
|
313
|
+
};
|
|
314
|
+
/** The name node of the object literal property one declaration handle names, if it is one. */
|
|
315
|
+
const literalProperty = (handle: NodeHandle): Node | undefined => {
|
|
316
|
+
const node = project.declarationAt(handle)?.node;
|
|
317
|
+
return node !== undefined &&
|
|
318
|
+
(isPropertyAssignment(node) || isShorthandPropertyAssignment(node)) &&
|
|
319
|
+
isObjectLiteralExpression(node.parent)
|
|
320
|
+
? node.name
|
|
321
|
+
: undefined;
|
|
322
|
+
};
|
|
323
|
+
|
|
290
324
|
const elements = new Map<number, Type | undefined>();
|
|
291
325
|
/** The element type of an array type, read once per type. */
|
|
292
326
|
const elementOf = (type: Type): Type | undefined => {
|
|
@@ -379,9 +413,11 @@ export function contextualUses<Brand>(
|
|
|
379
413
|
const sources = partsOf(source).filter((one) => !targets.some((part) => part.id === one.id));
|
|
380
414
|
for (const one of sources) {
|
|
381
415
|
if (carries(one)) {
|
|
382
|
-
into.push({ source: one, targets,
|
|
416
|
+
into.push({ source: one, targets, sourceKind: "members" });
|
|
383
417
|
} else if (outside(one)) {
|
|
384
|
-
into.push({ source: one, targets,
|
|
418
|
+
into.push({ source: one, targets, sourceKind: "outside" });
|
|
419
|
+
} else if (literalTyped(one)) {
|
|
420
|
+
into.push({ source: one, targets, sourceKind: "literal" });
|
|
385
421
|
}
|
|
386
422
|
const element = elementOf(one);
|
|
387
423
|
const called = element === undefined ? callsOf(one) : [];
|
|
@@ -423,7 +459,7 @@ export function contextualUses<Brand>(
|
|
|
423
459
|
}
|
|
424
460
|
seen.add(type.id);
|
|
425
461
|
return partsOf(type).some((part) => {
|
|
426
|
-
if (carries(part) || outside(part)) {
|
|
462
|
+
if (carries(part) || outside(part) || literalTyped(part)) {
|
|
427
463
|
return true;
|
|
428
464
|
}
|
|
429
465
|
const element = elementOf(part);
|
|
@@ -444,6 +480,8 @@ export function contextualUses<Brand>(
|
|
|
444
480
|
cap,
|
|
445
481
|
);
|
|
446
482
|
const reads: ContextualUse[] = [];
|
|
483
|
+
// A literal property writes a position's members once, however many values carry it there.
|
|
484
|
+
const carriedWrites = new Set<string>();
|
|
447
485
|
sourced.forEach(({ flow, type }, index) => {
|
|
448
486
|
const position = positions[index];
|
|
449
487
|
if (position === undefined || !isAnswered(position)) {
|
|
@@ -452,10 +490,24 @@ export function contextualUses<Brand>(
|
|
|
452
490
|
const checked: Checked[] = [];
|
|
453
491
|
compared(type, position, checked, new Set());
|
|
454
492
|
const ids = new Set<string>();
|
|
455
|
-
for (const { source, targets,
|
|
493
|
+
for (const { source, targets, sourceKind } of checked) {
|
|
456
494
|
const tables = targets.map(membersOf);
|
|
457
495
|
const sourceMembers = membersOf(source);
|
|
458
|
-
if (
|
|
496
|
+
if (sourceKind === "literal") {
|
|
497
|
+
for (const [name, member] of sourceMembers) {
|
|
498
|
+
const written = [...new Set(tables.flatMap((table) => table.get(name)?.ids ?? []))];
|
|
499
|
+
for (const at of written.length === 0 ? [] : member.declarations) {
|
|
500
|
+
const node = literalProperty(at);
|
|
501
|
+
const key = `${at.path}#${String(at.index)}>${written.join(",")}`;
|
|
502
|
+
if (node !== undefined && !carriedWrites.has(key)) {
|
|
503
|
+
carriedWrites.add(key);
|
|
504
|
+
writes.push({ at: node, from: flow.from, ids: written });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (sourceKind === "outside") {
|
|
459
511
|
for (const table of tables) {
|
|
460
512
|
for (const [name, member] of table) {
|
|
461
513
|
if (sourceMembers.has(name)) {
|
package/src/convention-rows.ts
CHANGED
|
@@ -146,8 +146,8 @@ const SCRIPT = "{js,jsx,ts,tsx}";
|
|
|
146
146
|
const MODULE = "{js,mjs,ts,mts}";
|
|
147
147
|
const ANY_MODULE = "{js,mjs,cjs,ts,mts,cts}";
|
|
148
148
|
const VITE_CONFIG = `vite.config.${ANY_MODULE}`;
|
|
149
|
-
/** The legacy configuration files of ESLint
|
|
150
|
-
const ESLINTRC = [".eslintrc", ".eslintrc.json", ".eslintrc.{js,cjs}"];
|
|
149
|
+
/** The legacy configuration files of ESLint: JSON, YAML or a CommonJS module. */
|
|
150
|
+
const ESLINTRC = [".eslintrc", ".eslintrc.json", ".eslintrc.{yml,yaml}", ".eslintrc.{js,cjs}"];
|
|
151
151
|
|
|
152
152
|
const SVELTEKIT_ROUTE_FILES = "+{page,layout,server,page.server,layout.server,error}";
|
|
153
153
|
|
|
@@ -209,6 +209,35 @@ const ANGULAR_PACKAGE_KEYS: readonly PackageKey[] = [
|
|
|
209
209
|
"scripts",
|
|
210
210
|
].map((key) => ({ key, files: ["angular.json"] }));
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* The row of a code generator that writes its output below one directory its
|
|
214
|
+
* configuration's `property` sets, `fallback` where no configuration file sets it, so
|
|
215
|
+
* every file there is a generated file.
|
|
216
|
+
*/
|
|
217
|
+
function outputDirectoryRow(row: {
|
|
218
|
+
readonly name: string;
|
|
219
|
+
readonly package: string;
|
|
220
|
+
readonly range: string;
|
|
221
|
+
readonly files: readonly string[];
|
|
222
|
+
readonly property: string;
|
|
223
|
+
readonly fallback: string;
|
|
224
|
+
}): ConventionRow {
|
|
225
|
+
return {
|
|
226
|
+
name: row.name,
|
|
227
|
+
package: row.package,
|
|
228
|
+
range: row.range,
|
|
229
|
+
entries: [],
|
|
230
|
+
generated: ["<output>"],
|
|
231
|
+
moves: [
|
|
232
|
+
{
|
|
233
|
+
id: "output",
|
|
234
|
+
defaults: [row.fallback],
|
|
235
|
+
readings: [{ property: row.property, files: row.files }],
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
212
241
|
/**
|
|
213
242
|
* The test runners and the assertion, mock and runner libraries whose import is test
|
|
214
243
|
* evidence beside each runner a row reads test files for. Each applies whether or not a
|
|
@@ -299,6 +328,7 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
|
299
328
|
range: ">=4.0.0 <10.0.0",
|
|
300
329
|
entries: [],
|
|
301
330
|
moves: [],
|
|
331
|
+
packageKeys: [{ key: "parser", files: ESLINTRC }],
|
|
302
332
|
prefixedNames: [
|
|
303
333
|
{ property: "plugins", files: ESLINTRC, prefix: "eslint-plugin" },
|
|
304
334
|
{ property: "extends", files: ESLINTRC, prefix: "eslint-plugin", marker: "plugin:" },
|
|
@@ -414,6 +444,14 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
|
414
444
|
{ key: "oclif.hooks", directory: false },
|
|
415
445
|
],
|
|
416
446
|
},
|
|
447
|
+
outputDirectoryRow({
|
|
448
|
+
name: "panda",
|
|
449
|
+
package: "@pandacss/dev",
|
|
450
|
+
range: ">=0.1.0 <2.0.0",
|
|
451
|
+
files: [`panda.config.${ANY_MODULE}`],
|
|
452
|
+
property: "outdir",
|
|
453
|
+
fallback: "styled-system",
|
|
454
|
+
}),
|
|
417
455
|
{
|
|
418
456
|
name: "parcel",
|
|
419
457
|
package: "parcel",
|
|
@@ -481,6 +519,13 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
|
481
519
|
},
|
|
482
520
|
],
|
|
483
521
|
},
|
|
522
|
+
{
|
|
523
|
+
name: "react-scripts",
|
|
524
|
+
package: "react-scripts",
|
|
525
|
+
range: ">=1.0.0 <6.0.0",
|
|
526
|
+
entries: [`src/index.${SCRIPT}`, "src/setupProxy.js", `src/setupTests.${SCRIPT}`],
|
|
527
|
+
moves: [],
|
|
528
|
+
},
|
|
484
529
|
{
|
|
485
530
|
name: "remix",
|
|
486
531
|
package: "@remix-run/dev",
|
package/src/conventions.ts
CHANGED
|
@@ -66,6 +66,7 @@ import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from
|
|
|
66
66
|
import type { SetupFailure } from "./setup-failure.ts";
|
|
67
67
|
import type { SourceFiles } from "./source-files.ts";
|
|
68
68
|
import { satisfies } from "./version-range.ts";
|
|
69
|
+
import { yamlDocument } from "./yaml-document.ts";
|
|
69
70
|
|
|
70
71
|
/** One manifest the analysis reads. */
|
|
71
72
|
export interface ConventionManifest {
|
|
@@ -151,6 +152,13 @@ type ParseSource = (fileName: string, text: string) => SourceFile;
|
|
|
151
152
|
|
|
152
153
|
const JSON_EXTENSION = ".json";
|
|
153
154
|
|
|
155
|
+
/**
|
|
156
|
+
* A configuration file its tool reads as a YAML document, read as the JSON document it
|
|
157
|
+
* denotes. Its nodes' offsets are that JSON text's, so no move may read one: a failure's
|
|
158
|
+
* line would name the wrong line.
|
|
159
|
+
*/
|
|
160
|
+
const YAML_NAME = /\.ya?ml$/u;
|
|
161
|
+
|
|
154
162
|
/** A configuration file with no extension that its tool reads as a JSON document. */
|
|
155
163
|
const JSON_RC = /^\.[\w-]+rc$/u;
|
|
156
164
|
|
|
@@ -883,6 +891,14 @@ function readingFiles(
|
|
|
883
891
|
} catch {
|
|
884
892
|
continue;
|
|
885
893
|
}
|
|
894
|
+
if (YAML_NAME.test(name)) {
|
|
895
|
+
const value = yamlDocument(text);
|
|
896
|
+
if (value !== undefined) {
|
|
897
|
+
const json = JSON.stringify(value);
|
|
898
|
+
found.push({ name, text: json, file: parse(`${path}${JSON_EXTENSION}`, json) });
|
|
899
|
+
}
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
886
902
|
found.push({
|
|
887
903
|
name,
|
|
888
904
|
text,
|
package/src/dependencies.ts
CHANGED
|
@@ -48,7 +48,7 @@ import type { DependencySection, Module } from "./ref.ts";
|
|
|
48
48
|
import { UNANSWERED } from "./query.ts";
|
|
49
49
|
import type { Handle, ProjectView } from "./session.ts";
|
|
50
50
|
import { sourceFilesOf, type SourceFiles } from "./source-files.ts";
|
|
51
|
-
import { workflowTokens } from "./workflow-steps.ts";
|
|
51
|
+
import { workflowCommands, workflowTokens } from "./workflow-steps.ts";
|
|
52
52
|
|
|
53
53
|
const MANIFEST = "package.json";
|
|
54
54
|
const MODULES_DIR = "node_modules";
|
|
@@ -871,6 +871,110 @@ function scriptTokensOf(manifest: Record<string, unknown>): ReadonlySet<string>
|
|
|
871
871
|
return found;
|
|
872
872
|
}
|
|
873
873
|
|
|
874
|
+
/**
|
|
875
|
+
* The runtime flags whose value is a module loaded before the entry, as `--flag value` or
|
|
876
|
+
* `--flag=value`: https://nodejs.org/api/cli.html#--importmodule,
|
|
877
|
+
* https://nodejs.org/api/cli.html#--experimental-loadermodule,
|
|
878
|
+
* https://nodejs.org/api/cli.html#-r---require-module
|
|
879
|
+
*/
|
|
880
|
+
const PRELOAD_FLAGS: ReadonlySet<string> = new Set([
|
|
881
|
+
"--experimental-loader",
|
|
882
|
+
"--import",
|
|
883
|
+
"--loader",
|
|
884
|
+
"--require",
|
|
885
|
+
"-r",
|
|
886
|
+
]);
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* The programs that pass a preload flag to the runtime they start: Node itself, and the
|
|
890
|
+
* runners that forward Node's flags or read the same `--require`:
|
|
891
|
+
* https://mochajs.org/#-require-module-r-module, https://tsx.is/node-enhancement,
|
|
892
|
+
* https://typestrong.org/ts-node/docs/options#require,
|
|
893
|
+
* https://github.com/remy/nodemon#running-non-node-scripts
|
|
894
|
+
*/
|
|
895
|
+
const PRELOADING_PROGRAMS: ReadonlySet<string> = new Set([
|
|
896
|
+
"mocha",
|
|
897
|
+
"node",
|
|
898
|
+
"nodemon",
|
|
899
|
+
"ts-node",
|
|
900
|
+
"tsx",
|
|
901
|
+
]);
|
|
902
|
+
|
|
903
|
+
/** The programs that run the command after them, and an environment assignment before it. */
|
|
904
|
+
const LAUNCHERS: ReadonlySet<string> = new Set(["cross-env", "env", "npx"]);
|
|
905
|
+
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/u;
|
|
906
|
+
|
|
907
|
+
/** The commands one script string runs, split at the shell's list and pipeline operators. */
|
|
908
|
+
const COMMAND_SEPARATORS = /&&|\|\||;|\||&/u;
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* The command strings one manifest runs: each of its `scripts`, and the `exec` command of
|
|
912
|
+
* nodemon's configuration, in the manifest's `nodemonConfig` or the `nodemon.json` beside
|
|
913
|
+
* it, which nodemon runs the watched script with:
|
|
914
|
+
* https://github.com/remy/nodemon#config-files
|
|
915
|
+
*/
|
|
916
|
+
function manifestCommands(
|
|
917
|
+
host: Host,
|
|
918
|
+
dir: string,
|
|
919
|
+
manifest: Record<string, unknown>,
|
|
920
|
+
): readonly string[] {
|
|
921
|
+
const scripts = manifest["scripts"];
|
|
922
|
+
const nodemon = [manifest["nodemonConfig"], readJSON(host, joinPath(dir, "nodemon.json"))?.value];
|
|
923
|
+
return [
|
|
924
|
+
...(isRecord(scripts) ? Object.values(scripts) : []),
|
|
925
|
+
...nodemon.map((config) => (isRecord(config) ? config["exec"] : undefined)),
|
|
926
|
+
].filter((command): command is string => typeof command === "string");
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* The packages `scripts` preload by a runtime flag's bare module specifier, a flag
|
|
931
|
+
* counting only in a command whose program passes it to the runtime, and for Node only
|
|
932
|
+
* before the script it runs, whose own arguments come after it.
|
|
933
|
+
*/
|
|
934
|
+
function preloadsOf(scripts: readonly string[]): ReadonlySet<string> {
|
|
935
|
+
const found = new Set<string>();
|
|
936
|
+
for (const script of scripts) {
|
|
937
|
+
for (const command of script.split(COMMAND_SEPARATORS)) {
|
|
938
|
+
const tokens = scriptTokens(command);
|
|
939
|
+
let at = 0;
|
|
940
|
+
while (
|
|
941
|
+
at < tokens.length &&
|
|
942
|
+
(ASSIGNMENT.test(tokens[at] ?? "") || LAUNCHERS.has(tokens[at] ?? ""))
|
|
943
|
+
) {
|
|
944
|
+
at += 1;
|
|
945
|
+
}
|
|
946
|
+
const path = tokens[at] ?? "";
|
|
947
|
+
const program = path.slice(path.lastIndexOf("/") + 1);
|
|
948
|
+
if (!PRELOADING_PROGRAMS.has(program)) {
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
for (at += 1; at < tokens.length; at += 1) {
|
|
952
|
+
const token = tokens[at] ?? "";
|
|
953
|
+
if (!token.startsWith("-")) {
|
|
954
|
+
if (program === "node") {
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
const equals = token.indexOf("=");
|
|
960
|
+
const flag = equals < 0 ? token : token.slice(0, equals);
|
|
961
|
+
if (!PRELOAD_FLAGS.has(flag)) {
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (equals < 0) {
|
|
965
|
+
at += 1;
|
|
966
|
+
}
|
|
967
|
+
const value = equals < 0 ? tokens[at] : token.slice(equals + 1);
|
|
968
|
+
const name = value === undefined ? undefined : packageOfSpecifier(value);
|
|
969
|
+
if (name !== undefined) {
|
|
970
|
+
found.add(name);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return found;
|
|
976
|
+
}
|
|
977
|
+
|
|
874
978
|
/** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
|
|
875
979
|
function isInside(path: string, dir: string): boolean {
|
|
876
980
|
return dir === "" || path.startsWith(`${dir}/`);
|
|
@@ -966,6 +1070,7 @@ export function dependenciesOf(
|
|
|
966
1070
|
}
|
|
967
1071
|
}
|
|
968
1072
|
const workflow = new Set(workflowTokens(host, targetRoot));
|
|
1073
|
+
const workflowPreloads = preloadsOf(workflowCommands(host, targetRoot));
|
|
969
1074
|
const declared: DeclaredDependency[] = [];
|
|
970
1075
|
const ran = new Set<string>();
|
|
971
1076
|
const peers = new Map<string, readonly string[]>();
|
|
@@ -976,12 +1081,16 @@ export function dependenciesOf(
|
|
|
976
1081
|
continue;
|
|
977
1082
|
}
|
|
978
1083
|
const tokens = scriptTokensOf(read.value);
|
|
1084
|
+
const preloads = preloadsOf(manifestCommands(host, dir, read.value));
|
|
979
1085
|
for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
|
|
980
1086
|
const found = installedOf(host, dir, dependency.name);
|
|
981
1087
|
if (found === undefined && uninstalledHere.has(dir)) {
|
|
982
1088
|
continue;
|
|
983
1089
|
}
|
|
984
1090
|
declared.push(dependency);
|
|
1091
|
+
if (preloads.has(dependency.name) || workflowPreloads.has(dependency.name)) {
|
|
1092
|
+
ran.add(declarationKey(dependency));
|
|
1093
|
+
}
|
|
985
1094
|
if (found === undefined) {
|
|
986
1095
|
continue;
|
|
987
1096
|
}
|
package/src/discover.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Diagnostic } from "@typescript/native/sync";
|
|
|
3
3
|
import { baseUrlPaths } from "./base-url.ts";
|
|
4
4
|
import type { BuildConfiguration, ProjectConfiguration } from "./config.ts";
|
|
5
5
|
import type { Host } from "./host.ts";
|
|
6
|
-
import { dirnamePath,
|
|
6
|
+
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
7
7
|
import type { Scope } from "./scope.ts";
|
|
8
8
|
import { SetupError } from "./setup-failure.ts";
|
|
9
9
|
import type { Engine, ProjectDiagnostics } from "./session.ts";
|
|
@@ -347,55 +347,8 @@ function derivedId(targetRoot: string, configFile: string): string {
|
|
|
347
347
|
return relativePath(targetRoot, configFile) ?? configFile;
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
-
/**
|
|
351
|
-
* The code of a configuration whose `extends` chain returns to a configuration it already
|
|
352
|
-
* passed. The parse reports it and returns, and a snapshot opened from it never answers:
|
|
353
|
-
* https://github.com/microsoft/TypeScript/issues/64754
|
|
354
|
-
*/
|
|
355
|
-
const CIRCULAR_EXTENDS = 18000;
|
|
356
|
-
|
|
357
350
|
type Parsed = ReturnType<Engine["parseConfigFile"]>;
|
|
358
351
|
|
|
359
|
-
/** The configuration files one parsed configuration's own `extends` names by path. */
|
|
360
|
-
function extendedPaths(parsed: Parsed, configFile: string): readonly string[] {
|
|
361
|
-
const { raw } = parsed as { readonly raw?: unknown };
|
|
362
|
-
const named: unknown = typeof raw === "object" && raw !== null ? Reflect.get(raw, "extends") : [];
|
|
363
|
-
return (Array.isArray(named) ? named : [named])
|
|
364
|
-
.filter((one): one is string => typeof one === "string")
|
|
365
|
-
.filter((one) => one.startsWith("./") || one.startsWith("../") || isAbsolutePath(one))
|
|
366
|
-
.map((one) => resolvePath(dirnamePath(configFile), one));
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* The `extends` chain from `configFile` to the first configuration it reaches twice, each
|
|
371
|
-
* link by path, or undefined where no chain written by path returns.
|
|
372
|
-
*/
|
|
373
|
-
function extendsCycle(
|
|
374
|
-
engine: Engine,
|
|
375
|
-
configFile: string,
|
|
376
|
-
parsed: Parsed,
|
|
377
|
-
): readonly string[] | undefined {
|
|
378
|
-
const walk = (chain: readonly string[], at: Parsed): readonly string[] | undefined => {
|
|
379
|
-
for (const next of extendedPaths(at, chain.at(-1) ?? configFile)) {
|
|
380
|
-
if (chain.includes(next)) {
|
|
381
|
-
return [...chain, next];
|
|
382
|
-
}
|
|
383
|
-
let read: Parsed;
|
|
384
|
-
try {
|
|
385
|
-
read = engine.parseConfigFile(next);
|
|
386
|
-
} catch {
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
const cycle = walk([...chain, next], read);
|
|
390
|
-
if (cycle !== undefined) {
|
|
391
|
-
return cycle;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
return undefined;
|
|
395
|
-
};
|
|
396
|
-
return walk([configFile], parsed);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
352
|
/** The configuration files one parsed configuration's `references` name. */
|
|
400
353
|
function referencesOf(parsed: Parsed): readonly string[] {
|
|
401
354
|
// The parse answers a directory reference as the directory; the compiler reads a
|
|
@@ -406,51 +359,17 @@ function referencesOf(parsed: Parsed): readonly string[] {
|
|
|
406
359
|
);
|
|
407
360
|
}
|
|
408
361
|
|
|
409
|
-
/** Why one parsed configuration's own `extends` chain cannot be opened, where it is circular. */
|
|
410
|
-
function circularity(engine: Engine, configFile: string, parsed: Parsed): string | undefined {
|
|
411
|
-
const chain = extendsCycle(engine, configFile, parsed);
|
|
412
|
-
if (chain === undefined && !parsed.errors.some(({ code }) => code === CIRCULAR_EXTENDS)) {
|
|
413
|
-
return undefined;
|
|
414
|
-
}
|
|
415
|
-
return `TS${String(CIRCULAR_EXTENDS)}: Circularity detected while resolving configuration: ${(chain ?? [configFile]).join(" -> ")}`;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
362
|
/**
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
363
|
+
* The code of a configuration whose `extends` chain returns to a configuration it already
|
|
364
|
+
* passed. A snapshot that opens two configurations whose `extends` chains reach one such
|
|
365
|
+
* cycle panics (`OwnerCache.AddOwner: entry not found`, typescript 7.1.0-dev.20261014.1),
|
|
366
|
+
* so the analysis refuses one before any snapshot is asked to open it.
|
|
422
367
|
*/
|
|
423
|
-
|
|
424
|
-
engine: Engine,
|
|
425
|
-
references: readonly string[],
|
|
426
|
-
visited: Set<string>,
|
|
427
|
-
): string | undefined {
|
|
428
|
-
for (const reference of references) {
|
|
429
|
-
if (visited.has(reference)) {
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
visited.add(reference);
|
|
433
|
-
let parsed: Parsed;
|
|
434
|
-
try {
|
|
435
|
-
parsed = engine.parseConfigFile(reference);
|
|
436
|
-
} catch {
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
const reason =
|
|
440
|
-
circularity(engine, reference, parsed) ??
|
|
441
|
-
referencedCircularity(engine, referencesOf(parsed), visited);
|
|
442
|
-
if (reason !== undefined) {
|
|
443
|
-
return reason;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
return undefined;
|
|
447
|
-
}
|
|
368
|
+
const CIRCULAR_EXTENDS = 18000;
|
|
448
369
|
|
|
449
370
|
/**
|
|
450
|
-
* The
|
|
451
|
-
*
|
|
452
|
-
* configuration it references is circular, which the analysis refuses before any
|
|
453
|
-
* snapshot is asked to open it.
|
|
371
|
+
* The configurations one configuration file's `references` name, or why it cannot be read:
|
|
372
|
+
* the compiler throws, or its parse reports the `extends` chain circular.
|
|
454
373
|
*/
|
|
455
374
|
function readConfiguration(
|
|
456
375
|
engine: Engine,
|
|
@@ -458,11 +377,12 @@ function readConfiguration(
|
|
|
458
377
|
): { readonly references: readonly string[] } | { readonly reason: string } {
|
|
459
378
|
try {
|
|
460
379
|
const parsed = engine.parseConfigFile(configFile);
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
380
|
+
const circular = parsed.errors.find(({ code }) => code === CIRCULAR_EXTENDS);
|
|
381
|
+
// The compiler leaves the message's `{0}` unfilled:
|
|
382
|
+
// https://github.com/microsoft/TypeScript/pull/64755
|
|
383
|
+
return circular === undefined
|
|
384
|
+
? { references: referencesOf(parsed) }
|
|
385
|
+
: { reason: `TS${String(circular.code)}: ${circular.text.replace("{0}", () => configFile)}` };
|
|
466
386
|
} catch (error: unknown) {
|
|
467
387
|
return { reason: error instanceof Error ? error.message : String(error) };
|
|
468
388
|
}
|
package/src/entry-points.ts
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
import {
|
|
15
15
|
isArrayLiteralExpression,
|
|
16
16
|
isAsExpression,
|
|
17
|
-
isBinaryExpression,
|
|
18
17
|
isCallExpression,
|
|
19
18
|
isExportAssignment,
|
|
20
19
|
isIdentifier,
|
|
@@ -27,7 +26,6 @@ import {
|
|
|
27
26
|
isPropertyAssignment,
|
|
28
27
|
isSatisfiesExpression,
|
|
29
28
|
isStringLiteral,
|
|
30
|
-
isTemplateExpression,
|
|
31
29
|
SyntaxKind,
|
|
32
30
|
type Node,
|
|
33
31
|
type SourceFile,
|
|
@@ -40,6 +38,7 @@ import {
|
|
|
40
38
|
type ParseFile,
|
|
41
39
|
} from "./configuration-files.ts";
|
|
42
40
|
import type { Selected } from "./conventions.ts";
|
|
41
|
+
import { buildScriptEntries } from "./build-scripts.ts";
|
|
43
42
|
import { globExpression } from "./glob.ts";
|
|
44
43
|
import type { Host } from "./host.ts";
|
|
45
44
|
import { htmlEntries } from "./html-entries.ts";
|
|
@@ -48,6 +47,7 @@ import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
|
|
|
48
47
|
import { UNANSWERED } from "./query.ts";
|
|
49
48
|
import type { ProjectView } from "./session.ts";
|
|
50
49
|
import type { SourceFiles } from "./source-files.ts";
|
|
50
|
+
import { computedLoadDirectory, leadingLiteral } from "./unseen-loads.ts";
|
|
51
51
|
import { workflowEntries } from "./workflow-steps.ts";
|
|
52
52
|
|
|
53
53
|
/** Which rule entered a file. */
|
|
@@ -56,6 +56,8 @@ export type EntryRule =
|
|
|
56
56
|
| "configuration-file"
|
|
57
57
|
/** A file a relative string of a configuration module or a JSON configuration file names. */
|
|
58
58
|
| "configuration-string"
|
|
59
|
+
/** An entry a build script's bundler call names, whose exports the bundle exposes. */
|
|
60
|
+
| "bundler-entry"
|
|
59
61
|
/** The test runner: its workspace configuration, its setup files and the test files. */
|
|
60
62
|
| "test-runner"
|
|
61
63
|
/** The test files below the browser-test runner's test directory. */
|
|
@@ -324,10 +326,7 @@ function configurationEntries(
|
|
|
324
326
|
strings(dirnamePath(document.path), unselectedIn(document.path, document.strings));
|
|
325
327
|
}
|
|
326
328
|
for (const { file } of outside) {
|
|
327
|
-
strings(
|
|
328
|
-
dirnamePath(file.fileName),
|
|
329
|
-
moduleStrings(file).map((one) => one.text),
|
|
330
|
-
);
|
|
329
|
+
strings(dirnamePath(file.fileName), unselected(file));
|
|
331
330
|
}
|
|
332
331
|
return found;
|
|
333
332
|
}
|
|
@@ -541,18 +540,6 @@ function workersOf<Brand>(
|
|
|
541
540
|
return found;
|
|
542
541
|
}
|
|
543
542
|
|
|
544
|
-
/** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
|
|
545
|
-
function leadingLiteral(node: Node): string | undefined {
|
|
546
|
-
const at = unwrapped(node);
|
|
547
|
-
if (isTemplateExpression(at)) {
|
|
548
|
-
return at.head.text;
|
|
549
|
-
}
|
|
550
|
-
if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
|
|
551
|
-
return literalOf(unwrapped(at.left)) ?? leadingLiteral(at.left);
|
|
552
|
-
}
|
|
553
|
-
return undefined;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
543
|
/** Whether one call is `import.meta.glob(...)`. */
|
|
557
544
|
function isMetaGlob(node: Node): boolean {
|
|
558
545
|
if (!isCallExpression(node) || !isPropertyAccessExpression(node.expression)) {
|
|
@@ -579,13 +566,8 @@ function computedImportsOf(file: SourceFile, files: SourceFiles): readonly Entry
|
|
|
579
566
|
const dir = dirnamePath(file.fileName);
|
|
580
567
|
const found: EntryPoint[] = [];
|
|
581
568
|
const below = (prefix: string): void => {
|
|
582
|
-
const target =
|
|
583
|
-
if (
|
|
584
|
-
!isModuleRelative(prefix) ||
|
|
585
|
-
!prefix.endsWith("/") ||
|
|
586
|
-
target === dir ||
|
|
587
|
-
relativePath(target, dir) !== undefined
|
|
588
|
-
) {
|
|
569
|
+
const target = computedLoadDirectory(dir, prefix);
|
|
570
|
+
if (target === undefined) {
|
|
589
571
|
return;
|
|
590
572
|
}
|
|
591
573
|
for (const candidate of files.byName.values()) {
|
|
@@ -680,6 +662,9 @@ export function entryPoints<Brand>(
|
|
|
680
662
|
...(names === undefined ? {} : { names }),
|
|
681
663
|
});
|
|
682
664
|
}
|
|
665
|
+
for (const { file, source } of buildScriptEntries(host, targetRoot, files, parse)) {
|
|
666
|
+
found.push({ file, rule: "bundler-entry", source, exports: true });
|
|
667
|
+
}
|
|
683
668
|
for (const { file, source } of workflowEntries(host, targetRoot, files)) {
|
|
684
669
|
found.push({ file, rule: "workflow-step", source, exports: true });
|
|
685
670
|
}
|
package/src/exempt.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface Evidence {
|
|
|
37
37
|
readonly whileLive?: string;
|
|
38
38
|
/** Where the evidence is an implementation of interface methods, which and how. */
|
|
39
39
|
readonly implementing?: Implementing;
|
|
40
|
+
/** Whether the evidence holds only where a write the mode counts stores into the declaration. */
|
|
41
|
+
readonly whileWritten?: true;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/** What a detector reads of one project, while the project's view is open. */
|
|
@@ -193,12 +195,26 @@ export function evidenceRecords<Brand>(
|
|
|
193
195
|
site: evidence.site,
|
|
194
196
|
...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
|
|
195
197
|
...(evidence.implementing === undefined ? {} : { implementing: evidence.implementing }),
|
|
198
|
+
...(evidence.whileWritten === undefined ? {} : { whileWritten: evidence.whileWritten }),
|
|
196
199
|
});
|
|
197
200
|
}
|
|
198
201
|
}
|
|
199
202
|
return found;
|
|
200
203
|
}
|
|
201
204
|
|
|
205
|
+
/**
|
|
206
|
+
* The records that hold given the declarations a write the mode counts stores into: one
|
|
207
|
+
* that holds only while written holds where its declaration is among them.
|
|
208
|
+
*/
|
|
209
|
+
export function writtenRecords(
|
|
210
|
+
records: readonly Exemption[],
|
|
211
|
+
written: ReadonlySet<string>,
|
|
212
|
+
): readonly Exemption[] {
|
|
213
|
+
return records.flatMap(({ whileWritten, ...record }) =>
|
|
214
|
+
whileWritten === undefined || written.has(record.id) ? [record] : [],
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
202
218
|
/** The records that hold under one mode, beside the evidence a production mode found in a test file. */
|
|
203
219
|
export function heldRecords(records: readonly Exemption[], holding: Holding): Detected {
|
|
204
220
|
const found: Exemption[] = [];
|