@cplieger/deadset-ts 5.2.3 → 6.0.0

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 (73) hide show
  1. package/dist/src/analysis.js +28 -5
  2. package/dist/src/augmentations.js +50 -0
  3. package/dist/src/components.js +15 -15
  4. package/dist/src/conformance-record.js +2 -2
  5. package/dist/src/convention-rows.js +17 -2
  6. package/dist/src/conventions.js +82 -1
  7. package/dist/src/dependencies.js +42 -26
  8. package/dist/src/entry-points.js +28 -14
  9. package/dist/src/exempt.js +1 -0
  10. package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
  11. package/dist/src/findings/emitters.js +47 -12
  12. package/dist/src/findings/interfaces.js +4 -3
  13. package/dist/src/findings/intra-function.js +15 -2
  14. package/dist/src/findings/unused-declarations.js +13 -1
  15. package/dist/src/findings-pass.js +1 -0
  16. package/dist/src/graph.js +6 -6
  17. package/dist/src/implementations.js +1 -1
  18. package/dist/src/indexed-access.js +215 -0
  19. package/dist/src/interface-satisfaction.js +85 -23
  20. package/dist/src/intra-function-parts.js +139 -30
  21. package/dist/src/inventory.js +6 -1
  22. package/dist/src/kinds.js +13 -12
  23. package/dist/src/matrix.js +34 -9
  24. package/dist/src/module-declarations.js +180 -0
  25. package/dist/src/outside-tests.js +17 -15
  26. package/dist/src/overrides.js +70 -9
  27. package/dist/src/query.js +7 -0
  28. package/dist/src/ref.js +1 -0
  29. package/dist/src/references.js +6 -0
  30. package/dist/src/report.js +6 -1
  31. package/dist/src/reporters/sarif.js +3 -1
  32. package/dist/src/reporters/text.js +23 -3
  33. package/dist/src/roots.js +50 -10
  34. package/dist/src/version.js +1 -1
  35. package/dist/src/workflow-steps.js +82 -0
  36. package/jsr.json +1 -1
  37. package/package.json +7 -1
  38. package/src/analysis.ts +40 -4
  39. package/src/augmentations.ts +72 -0
  40. package/src/components.ts +15 -13
  41. package/src/conformance-record.ts +2 -2
  42. package/src/convention-rows.ts +25 -3
  43. package/src/conventions.ts +106 -2
  44. package/src/dependencies.ts +51 -31
  45. package/src/entry-points.ts +32 -19
  46. package/src/exempt.ts +4 -1
  47. package/src/findings/dependencies-and-module-machinery.ts +2 -2
  48. package/src/findings/emitters.ts +71 -12
  49. package/src/findings/interfaces.ts +7 -6
  50. package/src/findings/intra-function.ts +20 -2
  51. package/src/findings/unused-declarations.ts +16 -1
  52. package/src/findings-pass.ts +4 -1
  53. package/src/graph.ts +6 -6
  54. package/src/implementations.ts +1 -1
  55. package/src/indexed-access.ts +295 -0
  56. package/src/interface-satisfaction.ts +118 -27
  57. package/src/intra-function-parts.ts +196 -35
  58. package/src/inventory.ts +8 -0
  59. package/src/kinds.ts +14 -12
  60. package/src/matrix.ts +51 -8
  61. package/src/module-declarations.ts +241 -0
  62. package/src/outside-tests.ts +18 -16
  63. package/src/overrides.ts +80 -14
  64. package/src/query.ts +28 -0
  65. package/src/ref.ts +1 -0
  66. package/src/references.ts +6 -0
  67. package/src/report.ts +11 -1
  68. package/src/reporters/sarif.ts +4 -1
  69. package/src/reporters/text.ts +25 -3
  70. package/src/roots.ts +56 -7
  71. package/src/sweep.ts +12 -0
  72. package/src/version.ts +1 -1
  73. package/src/workflow-steps.ts +104 -0
@@ -0,0 +1,180 @@
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
+ import { isCallExpression, isExportDeclaration, isExternalModuleReference, isImportDeclaration, isImportEqualsDeclaration, isImportTypeNode, isLiteralTypeNode, isModuleDeclaration, isNamespaceExport, isNamespaceImport, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
9
+ import { nodeKey } from "./inventory.js";
10
+ import { renderPosition } from "./position.js";
11
+ import { UNANSWERED } from "./query.js";
12
+ import { nameComponent } from "./ref.js";
13
+ /** Whether a node is a specifier's text: a string literal or a template with no substitution. */
14
+ function literalText(node) {
15
+ return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node));
16
+ }
17
+ /** The specifier one node writes, where it is an import of any form. */
18
+ function specifierOf(node) {
19
+ if (isImportDeclaration(node) || isExportDeclaration(node)) {
20
+ const specifier = node.moduleSpecifier;
21
+ return literalText(specifier) ? specifier : undefined;
22
+ }
23
+ if (isImportEqualsDeclaration(node)) {
24
+ const reference = node.moduleReference;
25
+ return isExternalModuleReference(reference) && literalText(reference.expression)
26
+ ? reference.expression
27
+ : undefined;
28
+ }
29
+ if (isCallExpression(node) && node.expression.kind === SyntaxKind.ImportKeyword) {
30
+ const [argument] = node.arguments;
31
+ return literalText(argument) ? argument : undefined;
32
+ }
33
+ if (isImportTypeNode(node) && isLiteralTypeNode(node.argument)) {
34
+ const literal = node.argument.literal;
35
+ return literalText(literal) ? literal : undefined;
36
+ }
37
+ return undefined;
38
+ }
39
+ /**
40
+ * The export names an import or a re-export reads from the module it names: none for a
41
+ * namespace, a star or a bare form.
42
+ */
43
+ export function namesImported(node) {
44
+ if (isImportDeclaration(node)) {
45
+ const clause = node.importClause;
46
+ const bindings = clause?.namedBindings;
47
+ return [
48
+ ...(clause?.name === undefined ? [] : ["default"]),
49
+ ...(bindings === undefined || isNamespaceImport(bindings)
50
+ ? []
51
+ : bindings.elements.map((one) => (one.propertyName ?? one.name).text)),
52
+ ];
53
+ }
54
+ const clause = isExportDeclaration(node) ? node.exportClause : undefined;
55
+ return clause === undefined || isNamespaceExport(clause)
56
+ ? []
57
+ : clause.elements.map((one) => (one.propertyName ?? one.name).text);
58
+ }
59
+ /**
60
+ * Whether a specifier matches a declaration's name, the name's one `*` standing for any run.
61
+ * A relative name is resolved against its own file, so no specifier names it by its text.
62
+ */
63
+ function namesModule(specifier, name) {
64
+ if (/^(?:\.{1,2}(?:\/|$)|\/)/u.test(name)) {
65
+ return false;
66
+ }
67
+ const star = name.indexOf("*");
68
+ if (star < 0 || name.slice(star + 1).includes("*")) {
69
+ return specifier === name;
70
+ }
71
+ const prefix = name.slice(0, star);
72
+ const suffix = name.slice(star + 1);
73
+ return (specifier.length >= prefix.length + suffix.length &&
74
+ specifier.startsWith(prefix) &&
75
+ specifier.endsWith(suffix));
76
+ }
77
+ /**
78
+ * The module declarations of an inventory a specifier names by name or pattern, each with
79
+ * the members that hold the export names given.
80
+ */
81
+ export function namedBy(held) {
82
+ if (held.modules.size === 0) {
83
+ return () => [];
84
+ }
85
+ const refs = new Map();
86
+ const children = new Map();
87
+ for (const symbol of held.symbols) {
88
+ if (held.modules.has(symbol.id)) {
89
+ refs.set(symbol.id, symbol.ref);
90
+ }
91
+ if (held.modules.has(symbol.parent)) {
92
+ const siblings = children.get(symbol.parent) ?? [];
93
+ siblings.push(symbol);
94
+ children.set(symbol.parent, siblings);
95
+ }
96
+ }
97
+ return (specifier, names) => [...held.modules].flatMap(([id, name]) => {
98
+ if (!namesModule(specifier, name)) {
99
+ return [];
100
+ }
101
+ // A member is named by its declaration's reference, or an export alias's.
102
+ const ref = refs.get(id) ?? "";
103
+ const scope = ref.slice(0, ref.indexOf("#"));
104
+ const wanted = new Set(names.flatMap((one) => [
105
+ `${ref}.${nameComponent(one)}`,
106
+ `${scope}#${nameComponent(one)}:alias`,
107
+ ]));
108
+ const members = (children.get(id) ?? []).filter((child) => wanted.has(child.ref));
109
+ return [{ id, members: members.map((child) => child.id) }];
110
+ });
111
+ }
112
+ /**
113
+ * The references the imports of one project's own files make to the module declarations
114
+ * they name, each at the import's specifier.
115
+ */
116
+ export function moduleDeclarationReferences(project, held, root, testFiles) {
117
+ const imports = [];
118
+ const declared = [];
119
+ for (const file of project.ownSourceFiles()) {
120
+ const fileId = held.declarations.get(nodeKey(file, file));
121
+ if (fileId === undefined) {
122
+ continue;
123
+ }
124
+ const visit = (node, from) => {
125
+ const own = held.declarations.get(nodeKey(file, node));
126
+ if (isModuleDeclaration(node) && literalText(node.name) && own !== undefined) {
127
+ declared.push({ id: own, name: node.name });
128
+ }
129
+ const specifier = specifierOf(node);
130
+ if (specifier !== undefined) {
131
+ imports.push({ file, from, specifier, names: namesImported(node) });
132
+ }
133
+ node.forEachChild((child) => {
134
+ visit(child, own ?? from);
135
+ });
136
+ };
137
+ file.forEachChild((child) => {
138
+ visit(child, fileId);
139
+ });
140
+ }
141
+ if (declared.length === 0 || imports.length === 0) {
142
+ return [];
143
+ }
144
+ const asked = [...declared.map((one) => one.name), ...imports.map((one) => one.specifier)];
145
+ const answers = project.symbolsAt(asked.map((node) => project.handle(node)));
146
+ const moduleOf = (at) => {
147
+ const symbol = answers[at];
148
+ return symbol === undefined || symbol === UNANSWERED ? undefined : symbol.id;
149
+ };
150
+ const named = namedBy(held);
151
+ const found = [];
152
+ imports.forEach((one, index) => {
153
+ const resolved = moduleOf(declared.length + index);
154
+ const position = renderPosition(one.file, root, one.specifier.getStart());
155
+ const read = (to, resolution) => {
156
+ found.push({
157
+ from: one.from,
158
+ to,
159
+ position,
160
+ use: "read",
161
+ resolution,
162
+ test: testFiles.has(position.path),
163
+ });
164
+ };
165
+ const same = new Set(declared
166
+ .filter((_, at) => resolved !== undefined && resolved === moduleOf(at))
167
+ .map((declaration) => declaration.id));
168
+ for (const id of same) {
169
+ read(id, "batch");
170
+ }
171
+ for (const declaration of named(one.specifier.text, one.names)) {
172
+ if (!same.has(declaration.id)) {
173
+ for (const to of [declaration.id, ...declaration.members]) {
174
+ read(to, "syntax");
175
+ }
176
+ }
177
+ }
178
+ });
179
+ return found;
180
+ }
@@ -1,13 +1,15 @@
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
- * Such a file is read without a program, so what its body references beyond the modules
6
- * it names is not known.
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
  import { isCallExpression, isExportDeclaration, isImportDeclaration, isNamespaceExport, isNamespaceImport, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
9
10
  import { RESOLVED_EXTENSIONS } from "./configuration-files.js";
10
11
  import { emittedFrom } from "./emit-map.js";
12
+ import { namedBy, namesImported } from "./module-declarations.js";
11
13
  import { dirnamePath, joinPath, normalizePath, queriedModulePath } from "./paths.js";
12
14
  import { renderPosition } from "./position.js";
13
15
  import { nameComponent } from "./ref.js";
@@ -47,17 +49,11 @@ function modulesNamed(file) {
47
49
  if (isImportDeclaration(statement)) {
48
50
  const clause = statement.importClause;
49
51
  const bindings = clause?.namedBindings;
50
- name(statement.moduleSpecifier, [
51
- ...(clause?.name === undefined ? [] : ["default"]),
52
- ...(bindings === undefined || isNamespaceImport(bindings)
53
- ? []
54
- : bindings.elements.map((one) => (one.propertyName ?? one.name).text)),
55
- ], bindings === undefined ? clause?.name === undefined : isNamespaceImport(bindings));
52
+ name(statement.moduleSpecifier, namesImported(statement), bindings === undefined ? clause?.name === undefined : isNamespaceImport(bindings));
56
53
  }
57
54
  else if (isExportDeclaration(statement) && statement.moduleSpecifier !== undefined) {
58
55
  const clause = statement.exportClause;
59
- const star = clause === undefined || isNamespaceExport(clause);
60
- name(statement.moduleSpecifier, star ? [] : clause.elements.map((one) => (one.propertyName ?? one.name).text), star);
56
+ name(statement.moduleSpecifier, namesImported(statement), clause === undefined || isNamespaceExport(clause));
61
57
  }
62
58
  }
63
59
  const visit = (node) => {
@@ -106,11 +102,21 @@ export function outsideTestReferences(tests, held, targetRoot) {
106
102
  exported.set(key, [...(exported.get(key) ?? []), symbol.id]);
107
103
  }
108
104
  }
105
+ const declared = namedBy(held);
109
106
  const found = [];
110
107
  for (const { path, file } of tests) {
111
108
  const from = `${path}#`;
112
109
  const dir = dirnamePath(path) === "." ? "" : dirnamePath(path);
113
110
  for (const named of modulesNamed(file)) {
111
+ const position = renderPosition(file, targetRoot, named.specifier.getStart());
112
+ const read = (to, use) => {
113
+ found.push({ from, to, position, use, resolution: "syntax", test: true, unheld: true });
114
+ };
115
+ for (const declaration of declared(named.text, named.names)) {
116
+ for (const to of [declaration.id, ...declaration.members]) {
117
+ read(to, "read");
118
+ }
119
+ }
114
120
  const written = named.text.includes("?") ? queriedModulePath(named.text) : named.text;
115
121
  if (written === undefined) {
116
122
  continue;
@@ -121,10 +127,6 @@ export function outsideTestReferences(tests, held, targetRoot) {
121
127
  if (fileId === undefined) {
122
128
  continue;
123
129
  }
124
- const position = renderPosition(file, targetRoot, named.specifier.getStart());
125
- const read = (to, use) => {
126
- found.push({ from, to, position, use, resolution: "syntax", test: true, unheld: true });
127
- };
128
130
  if (named.evaluates) {
129
131
  read(fileId, "evaluation");
130
132
  }
@@ -1,12 +1,14 @@
1
1
  /**
2
- * The overrides of the target's class members. A call through the base class, `this`
3
- * in the base's own body or a value typed as the base, names the base's member and
4
- * runs the override of whichever subclass the value is, so the base member's
5
- * declaration references every override as a use of it.
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
  import { isClassDeclaration, isClassExpression, isExpressionWithTypeArguments, isPropertyAccessExpression, isQualifiedName, SyntaxKind, } from "@typescript/native/unstable/ast";
8
9
  import { memberComponent } from "./class-members.js";
9
10
  import { nodeKey } from "./inventory.js";
11
+ import { SymbolFlags } from "@typescript/native/unstable/sync";
10
12
  import { UNANSWERED } from "./query.js";
11
13
  /** The member kinds a subclass overrides. */
12
14
  const OVERRIDABLE = new Set(["method", "class-member"]);
@@ -38,9 +40,10 @@ function extending(project, held) {
38
40
  }
39
41
  /**
40
42
  * The references each base class member makes to its overrides, for the classes of one
41
- * project that extend another class of the target. A member a nearer base already
42
- * overrides is reached through that override. A private member is not overridden.
43
- * Each reference is placed at the override and made by its file.
43
+ * project that extend another class of the target, and each class makes to its methods
44
+ * that override a member of a class declared outside the target. A member a nearer base
45
+ * already overrides is reached through that override. A private member is not
46
+ * overridden. Each reference is placed at the override and made by its file.
44
47
  */
45
48
  export function overrideReferences(project, held, chains, testFiles) {
46
49
  const classes = extending(project, held);
@@ -62,14 +65,25 @@ export function overrideReferences(project, held, chains, testFiles) {
62
65
  members.set(symbol.parent, own);
63
66
  }
64
67
  const bases = new Map();
68
+ const outsideBases = new Map();
69
+ const own = project.ownPaths();
65
70
  const symbols = project.symbolsAt(classes.map((one) => project.handle(one.base)));
66
71
  classes.forEach(({ id }, index) => {
67
72
  const symbol = symbols[index];
68
73
  if (symbol !== undefined && symbol !== UNANSWERED) {
69
- bases.set(id, chains
74
+ const inTarget = chains
70
75
  .chainOf(symbol)
71
76
  .map((target) => target.id)
72
- .filter((target) => byId.get(target)?.kind === "class" && target !== id));
77
+ .filter((target) => byId.get(target)?.kind === "class" && target !== id);
78
+ bases.set(id, inTarget);
79
+ const declared = (symbol.flags & SymbolFlags.Alias) === 0 ? symbol : project.queries.aliased(symbol);
80
+ if (inTarget.length === 0 &&
81
+ declared !== UNANSWERED &&
82
+ (declared.flags & SymbolFlags.Class) !== 0 &&
83
+ declared.declarations.length > 0 &&
84
+ declared.declarations.every((handle) => !own.has(handle.path))) {
85
+ outsideBases.set(id, declared);
86
+ }
73
87
  }
74
88
  });
75
89
  /** The nearest member of one component the class or a class it extends declares. */
@@ -85,6 +99,21 @@ export function overrideReferences(project, held, chains, testFiles) {
85
99
  return (bases.get(classId) ?? []).flatMap((base) => declaredAbove(base, component, seen));
86
100
  };
87
101
  const found = [];
102
+ const outsideNames = outsideMemberNames(project, bases, outsideBases);
103
+ for (const [classId, names] of outsideNames) {
104
+ for (const [component, member] of members.get(classId) ?? []) {
105
+ if (member.kind === "method" && names.has(component)) {
106
+ found.push({
107
+ from: classId,
108
+ to: member.id,
109
+ position: member.position,
110
+ use: "read",
111
+ resolution: "override",
112
+ test: testFiles.has(member.position.path),
113
+ });
114
+ }
115
+ }
116
+ }
88
117
  for (const [classId, baseIds] of bases) {
89
118
  for (const [component, member] of members.get(classId) ?? []) {
90
119
  const seen = new Set([classId]);
@@ -102,3 +131,35 @@ export function overrideReferences(project, held, chains, testFiles) {
102
131
  }
103
132
  return found;
104
133
  }
134
+ /**
135
+ * Per class of the target with a class declared outside the target in its `extends`
136
+ * chain, the names of the members that outside class's instances carry.
137
+ */
138
+ function outsideMemberNames(project, bases, outsideBases) {
139
+ const direct = new Map();
140
+ for (const [classId, base] of outsideBases) {
141
+ const type = project.queries.declaredTypeOf(base);
142
+ const properties = type === UNANSWERED ? UNANSWERED : project.queries.propertiesOf(type);
143
+ if (properties !== UNANSWERED) {
144
+ direct.set(classId, new Set(properties.map((property) => property.escapedName)));
145
+ }
146
+ }
147
+ const names = new Map();
148
+ const above = (classId, seen) => {
149
+ if (seen.has(classId)) {
150
+ return new Set();
151
+ }
152
+ seen.add(classId);
153
+ return new Set([
154
+ ...(direct.get(classId) ?? []),
155
+ ...(bases.get(classId) ?? []).flatMap((base) => [...above(base, seen)]),
156
+ ]);
157
+ };
158
+ for (const classId of bases.keys()) {
159
+ const found = above(classId, new Set());
160
+ if (found.size > 0) {
161
+ names.set(classId, found);
162
+ }
163
+ }
164
+ return names;
165
+ }
package/dist/src/query.js CHANGED
@@ -211,10 +211,17 @@ export function queriesOf(checker, configFile, batch, log, ask = ASK_DIRECTLY) {
211
211
  targetOf: (type) => type.isTypeReference()
212
212
  ? one("getTarget", () => typeLocation(type), () => type.getTarget())
213
213
  : type,
214
+ constraintOf: (type) => type.isTypeParameter()
215
+ ? one("getConstraint", () => typeLocation(type), () => type.getConstraint())
216
+ : undefined,
214
217
  symbolOfType: (type) => one("getSymbol", () => typeLocation(type), () => type.getSymbol()),
215
218
  signaturesOf: (type, kind) => one("getSignaturesOfType", () => typeLocation(type), () => checker.getSignaturesOfType(type, kind)),
216
219
  resolvedSignature: (node) => one("getResolvedSignature", () => nodeLocation(node), () => checker.getResolvedSignature(node)),
217
220
  parameterType: (signature, index) => one("getParameterType", () => `signature ${String(signature.id)} parameter ${String(index)}`, () => checker.getParameterType(signature, index)),
221
+ genericOf: (signature) => signature.target === undefined
222
+ ? undefined
223
+ : one("getTarget", () => `signature ${String(signature.id)}`, () => signature.getTarget()),
224
+ typeAtPosition: (signature, index) => one("getTypeParameterAtPosition", () => `signature ${String(signature.id)} position ${String(index)}`, () => signature.getTypeParameterAtPosition(index)),
218
225
  signatureOf: (declaration) => one("getSignatureFromDeclaration", () => nodeLocation(declaration), () => checker.getSignatureFromDeclaration(declaration)),
219
226
  returnTypeOf: (signature) => one("getReturnTypeOfSignature", () => `signature ${String(signature.id)}`, () => checker.getReturnTypeOfSignature(signature)),
220
227
  typeToString: (type) => one("typeToString", () => typeLocation(type), () => checker.typeToString(type)),
package/dist/src/ref.js CHANGED
@@ -52,6 +52,7 @@ export const REF_EXPRESSIONS = [
52
52
  const REF_FORMS = REF_EXPRESSIONS.map((expression) => new RegExp(expression, "u"));
53
53
  const IDENTIFIER = new RegExp(`^${TS_IDENT}$`, "u");
54
54
  const PRIVATE_NAME = new RegExp(`^#${TS_IDENT}$`, "u");
55
+ // 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
55
56
  const ASCII_WHITESPACE = /[\t\n\v\f\r ]/gu;
56
57
  /**
57
58
  * The canonical spelling of one declared name as a fragment component: bare when
@@ -6,6 +6,9 @@ import { specifierCandidates } from "./configuration-files.js";
6
6
  import { namespaceUses } from "./namespace-objects.js";
7
7
  import { globExpression } from "./glob.js";
8
8
  import { declarationsByName, nodeKey } from "./inventory.js";
9
+ import { augmentationReferences } from "./augmentations.js";
10
+ import { indexedAccessReferences } from "./indexed-access.js";
11
+ import { moduleDeclarationReferences } from "./module-declarations.js";
9
12
  import { overrideReferences } from "./overrides.js";
10
13
  import { dirnamePath, queriedModulePath, relativePath, resolvePath } from "./paths.js";
11
14
  import { byPosition, isComponentFile, renderPosition } from "./position.js";
@@ -1069,7 +1072,10 @@ export function references(project, held, targetRoot, options) {
1069
1072
  }
1070
1073
  if (consumer === undefined) {
1071
1074
  found.push(...overrideReferences(project, held, chains, new Set(tests)));
1075
+ found.push(...indexedAccessReferences(project, held, chains, root, new Set(tests)));
1076
+ found.push(...moduleDeclarationReferences(project, held, root, new Set(tests)));
1072
1077
  }
1078
+ found.push(...augmentationReferences(held, found));
1073
1079
  found.sort((a, b) => byPosition(a.position, b.position) ||
1074
1080
  compare(a.to, b.to) ||
1075
1081
  compare(a.from, b.from) ||
@@ -4,7 +4,7 @@
4
4
  * renders this document, so no two renderings disagree about which findings exist.
5
5
  */
6
6
  /** The version of the report schema this analyzer writes a report to. */
7
- const SCHEMA_VERSION = "7.0.0";
7
+ const SCHEMA_VERSION = "8.0.0";
8
8
  /** Every report schema version this analyzer reads: the one the Contract it implements admits. */
9
9
  export const SCHEMA_VERSIONS_ACCEPTED = [SCHEMA_VERSION];
10
10
  /** The name this analyzer writes every document under and mints every identifier with. */
@@ -186,6 +186,11 @@ function totalsOf(findings, result) {
186
186
  stale_suppressions: result.staleSuppressions.length,
187
187
  pending: result.totals.pending,
188
188
  omitted: 0,
189
+ withheld: {
190
+ certain: result.totals.withheld.certain,
191
+ probable: result.totals.withheld.probable,
192
+ possible: result.totals.withheld.possible,
193
+ },
189
194
  };
190
195
  }
191
196
  /** Builds the report of one run, every array in the order the Contract fixes for it. */
@@ -3,6 +3,7 @@ import { KINDS } from "../kinds.js";
3
3
  import { lineHashes, symbolFingerprint } from "./fingerprint.js";
4
4
  import { RenderError } from "./reporter.js";
5
5
  import { utf8Bytes } from "./sha256.js";
6
+ import { withheldLine } from "./text.js";
6
7
  const SCHEMA = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json";
7
8
  const VERSION = "2.1.0";
8
9
  /** The unit a finding's column counts. */
@@ -216,6 +217,7 @@ export function sarif(report, options) {
216
217
  ...report.stale_suppressions.map((stale) => staleResult(stale, index, hashOf)),
217
218
  ];
218
219
  const languages = [...report.analyzer.languages].sort().join("+");
220
+ const withheld = withheldLine(report.totals);
219
221
  const log = {
220
222
  $schema: SCHEMA,
221
223
  version: VERSION,
@@ -233,7 +235,7 @@ export function sarif(report, options) {
233
235
  columnKind: COLUMN_KIND,
234
236
  originalUriBaseIds: { [URI_BASE_ID]: { description: { text: ROOT_DESCRIPTION } } },
235
237
  results,
236
- properties: { totals: report.totals },
238
+ properties: withheld === undefined ? { totals: report.totals } : { totals: report.totals, withheld },
237
239
  },
238
240
  ],
239
241
  };
@@ -19,11 +19,27 @@ function summary(totals) {
19
19
  `${String(totals.omitted)} omitted`,
20
20
  ].join(", ");
21
21
  }
22
+ /** The confidences a withheld line names, from the highest the minimum can withhold. */
23
+ const WITHHELD_ORDER = ["probable", "possible"];
24
+ /**
25
+ * The line naming what the minimum confidence withheld and the setting that shows it all,
26
+ * or undefined where it withheld nothing.
27
+ */
28
+ export function withheldLine(totals) {
29
+ const named = WITHHELD_ORDER.filter((confidence) => totals.withheld[confidence] > 0);
30
+ const lowest = named.at(-1);
31
+ if (lowest === undefined) {
32
+ return undefined;
33
+ }
34
+ const counts = named.map((confidence) => `${String(totals.withheld[confidence])} ${confidence}`);
35
+ return `withheld by analysis.min_confidence: ${counts.join(", ")}, shown with analysis.min_confidence set to ${lowest}`;
36
+ }
22
37
  /**
23
38
  * One line per finding in the report's order, then one per stale suppression, then the
24
- * summary. A finding line is `path:line:col: kind name: message [confidence] (CODE)`; the
25
- * summary is deliberately not in that shape, so a filter for finding lines yields exactly
26
- * them. Nothing else is written: no timestamp, duration or host detail.
39
+ * withheld line where the minimum withheld a finding, then the summary. A finding line is
40
+ * `path:line:col: kind name: message [confidence] (CODE)`; the summary is deliberately not
41
+ * in that shape, so a filter for finding lines yields exactly them. Nothing else is
42
+ * written: no timestamp, duration or host detail.
27
43
  */
28
44
  export const text = (report) => coalesced((function* lines() {
29
45
  for (const found of report.findings) {
@@ -32,5 +48,9 @@ export const text = (report) => coalesced((function* lines() {
32
48
  for (const stale of report.stale_suppressions) {
33
49
  yield `${stale.position.path}:${String(stale.position.line)}:${String(stale.position.column)}: ${STALE_KIND} ${stale.symbol}: ${stale.message} [${STALE_CONFIDENCE}] (${stale.code})\n`;
34
50
  }
51
+ const withheld = withheldLine(report.totals);
52
+ if (withheld !== undefined) {
53
+ yield `${withheld}\n`;
54
+ }
35
55
  yield `${summary(report.totals)}\n`;
36
56
  })());
package/dist/src/roots.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * decides; the published API only supposes a consumer, so a published export is live
8
8
  * under reachability and still a candidate under reference counting.
9
9
  */
10
- import { isExpressionWithTypeArguments, isFunctionLikeDeclaration, isPropertyAccessExpression, isQualifiedName, isTypeQueryNode, isTypeReferenceNode, } from "@typescript/native/unstable/ast";
10
+ import { isExpressionWithTypeArguments, isFunctionLikeDeclaration, isPropertyAccessExpression, isQualifiedName, isTypeQueryNode, isTypeReferenceNode, isVariableStatement, } from "@typescript/native/unstable/ast";
11
11
  import { aliasChains } from "./alias-chain.js";
12
12
  import { conventionEntries } from "./conventions.js";
13
13
  import { entryPoints } from "./entry-points.js";
@@ -162,7 +162,7 @@ export function roots(project, held, targetRoot, options) {
162
162
  const enter = (file, kind, source, exports) => {
163
163
  entered.push({ file, kind, source, exports });
164
164
  };
165
- const points = entryPoints(project, files, options.testFiles, options.host, targetRoot, options.parse);
165
+ const points = entryPoints(project, files, options.testFiles, options.host, targetRoot, options.parse, options.conventions?.selected);
166
166
  const published = [];
167
167
  const entries = [
168
168
  ...options.manifest.entries.map((entry) => ({
@@ -239,7 +239,7 @@ export function roots(project, held, targetRoot, options) {
239
239
  }
240
240
  }
241
241
  const api = options.publishedAPI
242
- ? publishedAPI(project, published, exportsOf, held, files.byPath)
242
+ ? publishedAPI(project, published, exportsOf, held, files.byPath, releaseTagged(held, files))
243
243
  : { ids: [], guessed: [] };
244
244
  for (const id of api.ids) {
245
245
  add(id, "published-api", "");
@@ -303,17 +303,54 @@ function exportTables(project, held, wanted) {
303
303
  });
304
304
  return { tables, answered };
305
305
  }
306
+ /** A release tag of a documentation comment: `@` and its name, between white space or the ends. */
307
+ const RELEASE_TAG = /(?:^|\s)@(?:public|beta|alpha)(?=\s|$)/u;
308
+ /**
309
+ * Whether the documentation comment of the statement that starts at `start`, the last
310
+ * `/** *\/` comment before it with only white space between the two, holds a release tag.
311
+ */
312
+ function releaseTagIn(text, from, start) {
313
+ const leading = text.slice(from, start);
314
+ const open = leading.lastIndexOf("/**");
315
+ const close = open < 0 ? -1 : leading.indexOf("*/", open + 3);
316
+ if (close < 0 || leading.slice(close + 2).trim() !== "") {
317
+ return false;
318
+ }
319
+ return RELEASE_TAG.test(leading.slice(open + 3, close));
320
+ }
321
+ /** The exported top-level declarations of the program's files a release tag marks. */
322
+ function releaseTagged(held, files) {
323
+ const exported = new Set(held.symbols.filter((symbol) => symbol.exported).map((one) => one.id));
324
+ const found = [];
325
+ for (const file of files.byPath.values()) {
326
+ for (const statement of file.statements) {
327
+ if (!releaseTagIn(file.text, statement.pos, statement.getStart())) {
328
+ continue;
329
+ }
330
+ const nodes = isVariableStatement(statement)
331
+ ? statement.declarationList.declarations
332
+ : [statement];
333
+ for (const node of nodes) {
334
+ const id = held.declarations.get(nodeKey(file, node));
335
+ if (id !== undefined && exported.has(id)) {
336
+ found.push(id);
337
+ }
338
+ }
339
+ }
340
+ }
341
+ return found;
342
+ }
306
343
  /** The kinds of container whose export table, not its members' visibility, says what it publishes. */
307
344
  const EXPORTING_CONTAINERS = new Set(["file", "namespace"]);
308
345
  /**
309
- * Every declaration a consumer of the library can name: what the published files export
310
- * and, below each, the members a consumer writes: a namespace's exported ones, any other
311
- * declaration's public and protected ones, never a private one or a type parameter. A
312
- * declaration a published one's type names is published too, exported or not. A type name
313
- * the checker leaves unanswered publishes every declaration spelled as it is, and those are
314
- * `guessed`. A published file is not itself published.
346
+ * Every declaration a consumer of the library can name: what the published files export,
347
+ * and below each the members a consumer writes, a namespace's exported ones and any other
348
+ * declaration's public and protected ones. A declaration a published one's type names is
349
+ * published too, and so is an exported declaration a release tag marks. A type name the
350
+ * checker leaves unanswered publishes every declaration spelled as it is, as `guessed`. A
351
+ * published file is not itself published.
315
352
  */
316
- function publishedAPI(project, published, exportsOf, held, byPath) {
353
+ function publishedAPI(project, published, exportsOf, held, byPath, tagged) {
317
354
  const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
318
355
  const children = childrenOf(held);
319
356
  const nodes = declarationNodes(held, byPath);
@@ -327,6 +364,9 @@ function publishedAPI(project, published, exportsOf, held, byPath) {
327
364
  pending.push(id);
328
365
  }
329
366
  };
367
+ for (const id of tagged) {
368
+ reach(id);
369
+ }
330
370
  for (const file of published) {
331
371
  for (const id of exportsOf.get(file.fileName) ?? []) {
332
372
  if (byId.get(id)?.kind === "file") {
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "5.3.0";
7
+ export const CONTRACT_VERSION = "6.0.0";