@cplieger/deadset-ts 8.0.0-dev.1 → 8.0.0-dev.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/src/analysis.js +30 -6
  2. package/dist/src/augmentations.js +105 -12
  3. package/dist/src/build-scripts.js +437 -0
  4. package/dist/src/conformance-record.js +1 -1
  5. package/dist/src/convention-rows.js +62 -2
  6. package/dist/src/conventions.js +15 -0
  7. package/dist/src/dependencies.js +89 -0
  8. package/dist/src/entry-points.js +9 -18
  9. package/dist/src/findings/completion.js +122 -49
  10. package/dist/src/findings/intra-function.js +4 -2
  11. package/dist/src/findings/reads-and-writes.js +7 -6
  12. package/dist/src/findings/unused-declarations.js +13 -5
  13. package/dist/src/findings/visibility-narrowing.js +8 -3
  14. package/dist/src/graph.js +5 -3
  15. package/dist/src/intra-function-parts.js +45 -17
  16. package/dist/src/kinds.js +8 -8
  17. package/dist/src/manifest.js +18 -0
  18. package/dist/src/matrix.js +76 -4
  19. package/dist/src/references.js +34 -3
  20. package/dist/src/report.js +23 -4
  21. package/dist/src/reporters/sarif.js +12 -8
  22. package/dist/src/roots.js +138 -11
  23. package/dist/src/serialization-contract.js +258 -8
  24. package/dist/src/suppress.js +1 -0
  25. package/dist/src/sweep.js +7 -9
  26. package/dist/src/test-globals.js +234 -0
  27. package/dist/src/test-support.js +8 -11
  28. package/dist/src/type-errors.js +11 -3
  29. package/dist/src/unseen-loads.js +107 -0
  30. package/dist/src/verbs/analyze.js +24 -2
  31. package/dist/src/yaml-document.js +355 -0
  32. package/jsr.json +1 -1
  33. package/package.json +1 -1
  34. package/src/analysis.ts +48 -8
  35. package/src/augmentations.ts +134 -16
  36. package/src/build-scripts.ts +564 -0
  37. package/src/conformance-record.ts +1 -1
  38. package/src/convention-rows.ts +72 -2
  39. package/src/conventions.ts +16 -0
  40. package/src/dependencies.ts +96 -0
  41. package/src/entry-points.ts +10 -25
  42. package/src/file-facts.ts +3 -0
  43. package/src/finding.ts +5 -0
  44. package/src/findings/completion.ts +153 -56
  45. package/src/findings/intra-function.ts +4 -2
  46. package/src/findings/reads-and-writes.ts +7 -6
  47. package/src/findings/unused-declarations.ts +17 -5
  48. package/src/findings/visibility-narrowing.ts +9 -3
  49. package/src/graph.ts +5 -3
  50. package/src/intra-function-parts.ts +55 -16
  51. package/src/kinds.ts +8 -8
  52. package/src/manifest.ts +21 -3
  53. package/src/matrix.ts +122 -4
  54. package/src/references.ts +38 -2
  55. package/src/report.ts +56 -16
  56. package/src/reporters/sarif.ts +16 -8
  57. package/src/roots.ts +162 -12
  58. package/src/serialization-contract.ts +309 -7
  59. package/src/suppress.ts +1 -0
  60. package/src/sweep.ts +21 -9
  61. package/src/test-globals.ts +275 -0
  62. package/src/test-support.ts +17 -13
  63. package/src/type-errors.ts +14 -3
  64. package/src/unseen-loads.ts +145 -0
  65. package/src/verbs/analyze.ts +28 -2
  66. package/src/yaml-document.ts +377 -0
@@ -43,7 +43,10 @@ import { references, testFileRulesOf } from "./references.js";
43
43
  import { roots, unmatchedEverywhere, } from "./roots.js";
44
44
  import { serializationContract } from "./serialization-contract.js";
45
45
  import { outsideTestReferences, testFilesAt } from "./outside-tests.js";
46
+ import { mergedGlobalInterfaces } from "./augmentations.js";
47
+ import { unseenLoadsOf } from "./unseen-loads.js";
46
48
  import { joinedTestFiles, withJoinedTests } from "./test-joins.js";
49
+ import { GLOBALS_RUNNER, namesTestGlobals, withTestGlobals } from "./test-globals.js";
47
50
  import { supportReferences, testSupportFiles } from "./test-support.js";
48
51
  import { diagnosticsOf, ownedAs, runSession, } from "./session.js";
49
52
  import { accessorsOf, storesOf } from "./stores.js";
@@ -195,7 +198,7 @@ function readProjects(engine, host, scope, config, stage) {
195
198
  const joins = discovered.derived
196
199
  ? joinedTestFiles(engine, host, absoluteRoot, discovered.configFiles, testPatterns)
197
200
  : new Map();
198
- const programs = withBaseUrlPaths(engine, host, [...discovered.configFiles, ...consumerOf.keys()], workspace?.programs ?? new Map());
201
+ const programs = withTestGlobals(engine, host, parse, discovered.configFiles, withBaseUrlPaths(engine, host, [...discovered.configFiles, ...consumerOf.keys()], workspace?.programs ?? new Map()));
199
202
  const session = runSession(engine, [...discovered.configFiles, ...consumerOf.keys()], (opened) => {
200
203
  const consumer = consumerOf.get(opened.configFile);
201
204
  const setup = consumer === undefined ? workspace?.unbuildable(opened.configFile) : undefined;
@@ -499,6 +502,9 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
499
502
  : { generated: projectRead.generated }),
500
503
  }, detectors, disabled);
501
504
  const components = project.ownSourceFiles().filter(isComponentFile);
505
+ const injected = project.program
506
+ .getSourceFileNames()
507
+ .some((name) => name.endsWith(`/node_modules/${GLOBALS_RUNNER}/globals.d.ts`));
502
508
  const pathOf = (file) => relativePath(targetRoot, file.fileName) ?? file.fileName;
503
509
  return {
504
510
  projectRead,
@@ -506,11 +512,22 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
506
512
  testFiles: resolved.testFilePaths,
507
513
  evidence,
508
514
  componentFiles: components.map(pathOf),
515
+ augmented: mergedGlobalInterfaces(project, projectRead.held, sourceFilesOf(project, targetRoot)).map((id) => ({ id, kind: "global-augmentation", source: "" })),
509
516
  imports: new Map(project.ownSourceFiles().flatMap((file) => {
510
517
  const path = relativePath(targetRoot, file.fileName);
511
- return path === undefined
512
- ? []
513
- : [[path, file.imports.filter(isStringLiteralLikeNode).map((one) => one.text)]];
518
+ if (path === undefined) {
519
+ return [];
520
+ }
521
+ const specifiers = file.imports.filter(isStringLiteralLikeNode).map((one) => one.text);
522
+ // A file calling the globals a program declares is written for their runner.
523
+ return [
524
+ [
525
+ path,
526
+ injected && namesTestGlobals(project, file)
527
+ ? [...specifiers, GLOBALS_RUNNER]
528
+ : specifiers,
529
+ ],
530
+ ];
514
531
  })),
515
532
  componentWarnings: components.flatMap((file) => readComponent(file.originalText, extensionOf(file.fileName)).warnings.map((warning) => ({
516
533
  path: pathOf(file),
@@ -531,7 +548,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
531
548
  }));
532
549
  // Test-support code is code only test code imports in any configuration of the run: a
533
550
  // configuration that holds no test file says nothing about what the tests import.
534
- const support = testSupportFiles(withOutside.flatMap((one) => one.projectRead.held.symbols), withOutside.flatMap((one) => one.references), withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability), withOutside.flatMap((one) => one.testFiles), testEvidence(host, targetRoot, withOutside));
551
+ const { support, testUsed } = testSupportFiles(withOutside.flatMap((one) => one.projectRead.held.symbols), withOutside.flatMap((one) => one.references), withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability), withOutside.flatMap((one) => one.testFiles), testEvidence(host, targetRoot, withOutside));
535
552
  const settled = settledSatisfaction(withOutside.map((one) => ({ evidence: one.evidence, held: one.projectRead.held })), [...withOutside.flatMap((one) => one.references), ...read.consumers.references]);
536
553
  const answers = withOutside.map((one, index) => {
537
554
  const { projectRead } = one;
@@ -544,13 +561,17 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
544
561
  const made = supportReferences([...one.references, ...(satisfied?.references ?? [])], support);
545
562
  const held = new Set(projectRead.held.symbols.map((symbol) => symbol.position.path));
546
563
  const supportFiles = [...support].filter((path) => held.has(path)).sort();
564
+ const testUsedFiles = [...testUsed].filter((path) => held.has(path)).sort();
547
565
  const configured = {
548
566
  configuration: projectRead.configuration,
549
567
  symbols: projectRead.held.symbols,
550
568
  references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
551
- roots: projectRead.rooted.liveUnderReachability,
569
+ // The augmentations are rooted after test support is read: an outside declaration
570
+ // naming one calls nothing of its file.
571
+ roots: [...projectRead.rooted.liveUnderReachability, ...one.augmented],
552
572
  testFiles: [...one.testFiles, ...supportFiles],
553
573
  ...(supportFiles.length === 0 ? {} : { supportFiles }),
574
+ ...(testUsedFiles.length === 0 ? {} : { testUsedFiles }),
554
575
  ...(projectRead.held.ambient.size === 0 ? {} : { ambient: projectRead.held.ambient }),
555
576
  ...(projectRead.referenceOnly.length === 0
556
577
  ? {}
@@ -759,6 +780,7 @@ export function readEmitterRun(engine, host, scope, config, mode, detectors) {
759
780
  directives: inlineDirectives(project, read.held, targetRoot).filter((directive) => !read.referenceOnly.includes(directive.site.path)),
760
781
  declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
761
782
  parts: projectParts(project, read.held, references, targetRoot),
783
+ unseenLoads: unseenLoadsOf(project, read.configuration, targetRoot, testFiles),
762
784
  }));
763
785
  // Whether a declaration carries the marker decides its code, so one whose marker went
764
786
  // unread is reported by nothing.
@@ -834,6 +856,7 @@ export function emitterInputOver(host, scope, config, read, input, selfCheck) {
834
856
  heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
835
857
  generated: new Set(projects.flatMap((one) => one.extra.generated)),
836
858
  inOperands: new Set(projects.flatMap((one) => one.extra.inOperands)),
859
+ unseenLoads: projects.flatMap((one) => one.extra.unseenLoads),
837
860
  },
838
861
  boundary: {
839
862
  consumers: {
@@ -884,6 +907,7 @@ function readAnalysis(engine, host, scope, config, provenance, mode, detectors)
884
907
  librariesUnfound: read.librariesUnfound,
885
908
  conventionsApplied: read.conventionsApplied,
886
909
  unmappedEntries: unmappedEverywhere(read.projects),
910
+ unseenLoads: read.projects.flatMap((one) => one.extra.unseenLoads),
887
911
  },
888
912
  configured: read.projects.map((one) => one.configured),
889
913
  symbols,
@@ -6,6 +6,8 @@
6
6
  * name, which the language or a library types values by where the target never names it,
7
7
  * as `import.meta` or `process.env`, so a use of its member is a use of it.
8
8
  */
9
+ import { isInterfaceDeclaration, isModuleBlock, isModuleDeclaration, } from "@typescript/native/ast";
10
+ import { nodeKey } from "./inventory.js";
9
11
  import { referenceKey } from "./matrix.js";
10
12
  /** Whether one declaration is a `declare global` block: a module-level namespace named `global`. */
11
13
  function isGlobalBlock(symbol, held, files) {
@@ -15,45 +17,136 @@ function isGlobalBlock(symbol, held, files) {
15
17
  !held.modules.has(symbol.id));
16
18
  }
17
19
  /**
18
- * For each reference to a declaration an augmentation block, or the global scope of a
19
- * file in `scripts`, holds, a read at the same place of every interface and namespace
20
- * between it and the block or the file, and of the block itself. `scripts` holds the
21
- * files that are no module, by identifier.
20
+ * Per declaration, the interfaces and namespaces between it and the augmentation block or
21
+ * the file in `scripts` that holds it, with the block itself, or none where no such block
22
+ * or file holds it; and whether that holder is the global scope rather than a module
23
+ * declaration. `scripts` holds the files that are no module, by identifier.
22
24
  */
23
- export function augmentationReferences(held, found, scripts = new Set()) {
25
+ function holdersOf(held, scripts) {
24
26
  const files = new Set(held.symbols.filter((one) => one.kind === "file").map((one) => one.id));
25
27
  const globals = new Set(held.symbols.filter((one) => isGlobalBlock(one, held, files)).map((one) => one.id));
26
28
  if (globals.size === 0 && held.modules.size === 0 && scripts.size === 0) {
27
- return [];
29
+ return undefined;
28
30
  }
29
31
  const byId = new Map(held.symbols.map((one) => [one.id, one]));
30
- const holders = (id) => {
32
+ return (id) => {
31
33
  const ids = [];
32
34
  for (let at = byId.get(byId.get(id)?.parent ?? ""); at !== undefined; at = byId.get(at.parent)) {
33
35
  if (globals.has(at.id) || held.modules.has(at.id)) {
34
- return [...ids, at.id];
36
+ return { ids: [...ids, at.id], global: globals.has(at.id) };
35
37
  }
36
38
  if (scripts.has(at.id)) {
37
- return ids;
39
+ return { ids, global: true };
38
40
  }
39
41
  if (at.kind === "interface" || at.kind === "namespace") {
40
42
  ids.push(at.id);
41
43
  }
42
44
  }
45
+ return { ids: [], global: false };
46
+ };
47
+ }
48
+ /**
49
+ * The ambient interfaces of the global scope among `interfaces`: each one a declaration
50
+ * file or a `declare` holds that a `declare global` block, or the top level of a file in
51
+ * `scripts`, declares, directly or inside a namespace there.
52
+ */
53
+ export function ambientGlobalInterfaces(held, interfaces, scripts) {
54
+ const holders = holdersOf(held, scripts);
55
+ return holders === undefined
56
+ ? []
57
+ : interfaces.filter((id) => held.ambient.has(id) && holders(id).global);
58
+ }
59
+ /**
60
+ * Each ambient global interface of the project's own files that merges into an interface
61
+ * declared outside them, with every interface, namespace and block between it and the
62
+ * global scope: the outside declaration names it wherever a value is typed by it. Where
63
+ * every outside declaration is a package's, not the compiler's library, and declares no
64
+ * member, each member it declares as well: such an interface is a slot the package reads
65
+ * by type, as `keyof` does, in declarations the analysis does not walk.
66
+ */
67
+ export function mergedGlobalInterfaces(project, held, files) {
68
+ const scripts = new Set();
69
+ const names = new Map();
70
+ for (const file of files.byName.values()) {
71
+ const fileId = held.declarations.get(nodeKey(file, file));
72
+ if (fileId !== undefined && file.externalModuleIndicator === undefined) {
73
+ scripts.add(fileId);
74
+ }
75
+ const visit = (node) => {
76
+ if (isInterfaceDeclaration(node)) {
77
+ const id = held.declarations.get(nodeKey(file, node));
78
+ if (id !== undefined) {
79
+ names.set(id, node.name);
80
+ }
81
+ }
82
+ else if (isModuleDeclaration(node) || isModuleBlock(node)) {
83
+ node.forEachChild(visit);
84
+ }
85
+ };
86
+ file.forEachChild(visit);
87
+ }
88
+ const global = new Set(ambientGlobalInterfaces(held, [...names.keys()], scripts));
89
+ const asked = [...names].filter(([id]) => global.has(id));
90
+ if (asked.length === 0) {
43
91
  return [];
92
+ }
93
+ const answers = project.symbolsAt(asked.map(([, name]) => project.handle(name)));
94
+ const holders = holdersOf(held, scripts);
95
+ const empty = (handle) => {
96
+ if (project.program.getSourceFileMetadata(handle.path)?.isDefaultLibrary === true) {
97
+ return false;
98
+ }
99
+ const node = project.declarationAt(handle)?.node;
100
+ return node !== undefined && isInterfaceDeclaration(node) && node.members.length === 0;
44
101
  };
102
+ return [
103
+ ...new Set(asked.flatMap(([id], at) => {
104
+ const symbol = answers[at];
105
+ if (typeof symbol !== "object") {
106
+ return [];
107
+ }
108
+ const outside = symbol.declarations.filter((handle) => !files.byName.has(handle.path));
109
+ if (outside.length === 0) {
110
+ return [];
111
+ }
112
+ const members = outside.every(empty)
113
+ ? held.symbols.filter((one) => one.parent === id).map((one) => one.id)
114
+ : [];
115
+ return [id, ...(holders?.(id).ids ?? []), ...members];
116
+ })),
117
+ ];
118
+ }
119
+ /**
120
+ * For each reference to a declaration an augmentation block, or the global scope of a
121
+ * file in `scripts` (the files that are no module), holds, a read at the same place of
122
+ * every interface and namespace between it and the block or the file, and of the block
123
+ * itself. A read through an interface of `merged`, an ambient global interface merging
124
+ * into a language or library one, is a production read whoever writes it: it types a
125
+ * value the runtime provides, so a test reading a member needs it as code does.
126
+ */
127
+ export function augmentationReferences(held, found, scripts = new Set(), merged = new Set()) {
128
+ const holders = holdersOf(held, scripts);
129
+ if (holders === undefined) {
130
+ return [];
131
+ }
132
+ const fileOf = new Map(held.symbols.filter((one) => one.kind === "file").map((one) => [one.position.path, one.id]));
45
133
  // Two uses at one place of declarations one block holds read the block once there.
46
134
  const made = new Set(found.map(referenceKey));
47
135
  const reads = new Map();
48
136
  for (const reference of found) {
49
- for (const to of holders(reference.to)) {
137
+ const { ids } = holders(reference.to);
138
+ // A test's read through such an interface is held by the test file, which the test
139
+ // runner loads whichever of its declarations runs.
140
+ const typed = reference.test && ids.some((id) => merged.has(id));
141
+ const from = typed ? (fileOf.get(reference.position.path) ?? reference.from) : reference.from;
142
+ for (const to of ids) {
50
143
  const read = {
51
- from: reference.from,
144
+ from,
52
145
  to,
53
146
  position: reference.position,
54
147
  use: "read",
55
148
  resolution: reference.resolution,
56
- test: reference.test,
149
+ test: reference.test && !typed,
57
150
  };
58
151
  const key = referenceKey(read);
59
152
  if (!made.has(key) && !reads.has(key)) {