@cplieger/deadset-ts 7.1.0 → 7.2.0-dev.2

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 (64) hide show
  1. package/README.md +1 -1
  2. package/dist/src/alias-chain.js +6 -2
  3. package/dist/src/analysis.js +3 -1
  4. package/dist/src/config.js +4 -0
  5. package/dist/src/conformance-record.js +2 -2
  6. package/dist/src/decorator.js +3 -2
  7. package/dist/src/dependencies.js +0 -4
  8. package/dist/src/deprecation.js +1 -1
  9. package/dist/src/findings/completion.js +1 -1
  10. package/dist/src/findings/interfaces.js +1 -8
  11. package/dist/src/findings/unused-declarations.js +8 -11
  12. package/dist/src/html-entries.js +0 -3
  13. package/dist/src/injection-container.js +28 -25
  14. package/dist/src/intra-function-parts.js +71 -22
  15. package/dist/src/inventory.js +19 -6
  16. package/dist/src/kinds.js +2 -2
  17. package/dist/src/matrix.js +1 -8
  18. package/dist/src/module-declarations.js +0 -3
  19. package/dist/src/ref.js +3 -2
  20. package/dist/src/references.js +38 -20
  21. package/dist/src/report.js +1 -1
  22. package/dist/src/resolve.js +37 -4
  23. package/dist/src/schema.js +4 -1
  24. package/dist/src/suppress-file.js +43 -9
  25. package/dist/src/suppress-inline.js +35 -12
  26. package/dist/src/suppress.js +3 -2
  27. package/dist/src/type-errors.js +2 -1
  28. package/dist/src/verbs/analyze.js +7 -3
  29. package/dist/src/version.js +1 -1
  30. package/dist/src/workspace-resolution.js +5 -8
  31. package/dist/src/workspace.js +2 -2
  32. package/jsr.json +1 -1
  33. package/package.json +2 -2
  34. package/src/alias-chain.ts +7 -2
  35. package/src/analysis.ts +3 -3
  36. package/src/config.ts +9 -1
  37. package/src/conformance-record.ts +2 -2
  38. package/src/decorator.ts +8 -2
  39. package/src/dependencies.ts +0 -4
  40. package/src/deprecation.ts +1 -1
  41. package/src/findings/completion.ts +1 -4
  42. package/src/findings/interfaces.ts +1 -9
  43. package/src/findings/unused-declarations.ts +8 -15
  44. package/src/html-entries.ts +0 -3
  45. package/src/injection-container.ts +31 -26
  46. package/src/intra-function-parts.ts +96 -24
  47. package/src/inventory.ts +27 -5
  48. package/src/kinds.ts +2 -2
  49. package/src/matrix.ts +1 -9
  50. package/src/module-declarations.ts +0 -3
  51. package/src/ref.ts +3 -2
  52. package/src/references.ts +55 -21
  53. package/src/report.ts +1 -1
  54. package/src/resolve.ts +56 -6
  55. package/src/schema.ts +10 -14
  56. package/src/signatures.ts +6 -1
  57. package/src/suppress-file.ts +48 -9
  58. package/src/suppress-inline.ts +57 -15
  59. package/src/suppress.ts +3 -2
  60. package/src/type-errors.ts +2 -1
  61. package/src/verbs/analyze.ts +7 -2
  62. package/src/version.ts +1 -1
  63. package/src/workspace-resolution.ts +5 -8
  64. package/src/workspace.ts +2 -2
@@ -79,9 +79,6 @@ function namesModule(specifier, name) {
79
79
  * the members that hold the export names given.
80
80
  */
81
81
  function namedBy(held) {
82
- if (held.modules.size === 0) {
83
- return () => [];
84
- }
85
82
  const refs = new Map();
86
83
  const children = new Map();
87
84
  for (const symbol of held.symbols) {
package/dist/src/ref.js CHANGED
@@ -25,9 +25,10 @@ const TS_COMPUTED = String.raw `\[[^\]\r\n\t ]+\]`;
25
25
  const TS_HEAD = `(?:${TS_IDENT}|${TS_QUOTED})`;
26
26
  const TS_MEMBER = `(?:${TS_IDENT}|#${TS_IDENT}|${TS_QUOTED}|${TS_COMPUTED})`;
27
27
  const TS_PACKAGE = String.raw `(?:@[A-Za-z0-9._-]+/)?[A-Za-z0-9._-]+`;
28
- const TS_FILE = String.raw `(?:[^/#\r\n]+/)*[^/#\r\n]+(?:(?:\.d)?\.[mc]?ts|\.tsx|\.[mc]?js|\.jsx)`;
28
+ const TS_DIRS = String.raw `(?:[^/#\r\n]+/)*`;
29
+ const TS_FILE = String.raw `${TS_DIRS}[^/#\r\n]+(?:(?:\.d)?\.[mc]?ts|\.tsx|\.[mc]?js|\.jsx)`;
29
30
  const SCOPE = `${PREFIX}${TS_PACKAGE}/${TS_FILE}`;
30
- const MANIFEST_SCOPE = `${PREFIX}${TS_PACKAGE}/package\\.json`;
31
+ const MANIFEST_SCOPE = `${PREFIX}${TS_PACKAGE}/${TS_DIRS}package\\.json`;
31
32
  /**
32
33
  * One expression per form the grammar defines, each anchored and each the whole of
33
34
  * a reference. Several forms share one expression because they differ in what the
@@ -5,7 +5,7 @@ import { destructuring, propertyReadsOf } from "./destructuring.js";
5
5
  import { specifierCandidates } from "./configuration-files.js";
6
6
  import { namespaceUses } from "./namespace-objects.js";
7
7
  import { globExpression } from "./glob.js";
8
- import { declarationsByName, nodeKey } from "./inventory.js";
8
+ import { declarationsByName, isParameterProperty, nodeKey, } from "./inventory.js";
9
9
  import { augmentationReferences } from "./augmentations.js";
10
10
  import { elementAccessReferences } from "./element-access.js";
11
11
  import { indexedAccessReferences } from "./indexed-access.js";
@@ -336,6 +336,7 @@ function sitesOf(file, declarations, fileId, unread) {
336
336
  const wholes = [];
337
337
  const urls = [];
338
338
  const decorated = [];
339
+ const parameterWrites = [];
339
340
  const namespaces = namespaceUses(file);
340
341
  const literals = [];
341
342
  const flows = [];
@@ -419,7 +420,16 @@ function sitesOf(file, declarations, fileId, unread) {
419
420
  }
420
421
  const held = declarations.get(nodeKey(file, node));
421
422
  const outer = enclosing;
422
- if (held !== undefined) {
423
+ // A parameter property's annotation, initializer and decorators stay with the
424
+ // parameter when the member goes, so they are written inside the constructor's class.
425
+ if (held !== undefined && isParameterProperty(node)) {
426
+ parameterWrites.push({
427
+ name: node.name,
428
+ from: outer,
429
+ member: held,
430
+ });
431
+ }
432
+ else if (held !== undefined) {
423
433
  enclosing = held;
424
434
  for (const modifier of node.modifiers ?? []) {
425
435
  if (isDecorator(modifier)) {
@@ -441,6 +451,7 @@ function sitesOf(file, declarations, fileId, unread) {
441
451
  wholes,
442
452
  urls,
443
453
  decorated,
454
+ parameterWrites,
444
455
  keyed,
445
456
  };
446
457
  }
@@ -523,15 +534,12 @@ function importedNames(file) {
523
534
  importedCache.set(file, names);
524
535
  return names;
525
536
  }
526
- /**
527
- * The name node of every import binding one file's top level declares, and whether it
528
- * binds a whole module.
529
- */
537
+ /** The name node of every import binding one file's top level declares. */
530
538
  function importBindings(file) {
531
539
  const found = [];
532
540
  for (const statement of file.statements) {
533
541
  if (isImportEqualsDeclaration(statement)) {
534
- found.push({ node: statement.name, whole: true });
542
+ found.push({ node: statement.name });
535
543
  continue;
536
544
  }
537
545
  const clause = isImportDeclaration(statement) ? statement.importClause : undefined;
@@ -539,17 +547,17 @@ function importBindings(file) {
539
547
  continue;
540
548
  }
541
549
  if (clause.name !== undefined) {
542
- found.push({ node: clause.name, whole: false });
550
+ found.push({ node: clause.name });
543
551
  }
544
552
  const bindings = clause.namedBindings;
545
553
  if (bindings === undefined) {
546
554
  continue;
547
555
  }
548
556
  if (bindings.kind === SyntaxKind.NamespaceImport) {
549
- found.push({ node: bindings.name, whole: true });
557
+ found.push({ node: bindings.name });
550
558
  }
551
559
  else {
552
- found.push(...bindings.elements.map((element) => ({ node: element.name, whole: false })));
560
+ found.push(...bindings.elements.map((element) => ({ node: element.name })));
553
561
  }
554
562
  }
555
563
  return found;
@@ -856,7 +864,7 @@ export function references(project, held, targetRoot, options) {
856
864
  }
857
865
  }
858
866
  const keyNames = computedKeyNames(held.symbols);
859
- for (const { file, fileId, uses, reads, links, evaluations, wholes, urls, decorated, keyed, } of walked) {
867
+ for (const { file, fileId, uses, reads, links, evaluations, wholes, urls, decorated, parameterWrites, keyed, } of walked) {
860
868
  const test = testFile.get(file) === true;
861
869
  // A component file's markup may use any binding its blocks declare at the top level.
862
870
  const markup = isComponentFile(file);
@@ -874,6 +882,17 @@ export function references(project, held, targetRoot, options) {
874
882
  test,
875
883
  });
876
884
  }
885
+ // The constructor stores its argument in the member a parameter property declares.
886
+ for (const { name, from, member } of parameterWrites) {
887
+ found.push({
888
+ from,
889
+ to: member,
890
+ position: renderPosition(file, root, name.getStart()),
891
+ use: "write",
892
+ resolution: "syntax",
893
+ test,
894
+ });
895
+ }
877
896
  // One batch per capped run of the file's name nodes, the re-exports' nodes after
878
897
  // the uses and the evaluated modules' specifiers last, each node once: a bare star
879
898
  // re-export's specifier is a link and an evaluation both. A shorthand is left out
@@ -1011,15 +1030,14 @@ export function references(project, held, targetRoot, options) {
1011
1030
  const chained = symbol === UNANSWERED
1012
1031
  ? named(binding.node).map((id) => ({ id, stepped: false, guessed: true }))
1013
1032
  : chains.chainOf(symbol);
1014
- // The markup may name any export of a module a binding holds whole.
1015
- const targets = binding.whole
1016
- ? chained.flatMap((target) => [
1017
- target,
1018
- ...declaredIn(target.id)
1019
- .filter((one) => one.exported)
1020
- .map((one) => ({ ...target, id: one.id })),
1021
- ])
1022
- : chained;
1033
+ // The markup may name any export of a module a binding's chain reaches, whether a
1034
+ // namespace import binds it or a named or default import of a namespace re-export.
1035
+ const targets = chained.flatMap((target) => [
1036
+ target,
1037
+ ...declaredIn(target.id)
1038
+ .filter((one) => one.exported)
1039
+ .map((one) => ({ ...target, id: one.id })),
1040
+ ]);
1023
1041
  for (const target of targets) {
1024
1042
  found.push({
1025
1043
  from: fileId,
@@ -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 = "8.1.0";
7
+ const SCHEMA_VERSION = "8.2.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. */
@@ -21,9 +21,9 @@ const PLATFORM_MEMBERS = ["os", "arch", "tags"];
21
21
  const PROJECT_MEMBER = "project";
22
22
  /**
23
23
  * One segment of a project path: not empty, not `.` or `..`, and holding no
24
- * solidus, backslash or line break.
24
+ * solidus or control character.
25
25
  */
26
- const PROJECT_SEGMENT = String.raw `(?:[^/\\.\r\n][^/\\\r\n]*|\.[^/\\.\r\n][^/\\\r\n]*|\.\.[^/\\\r\n]+)`;
26
+ const PROJECT_SEGMENT = String.raw `(?:[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.\.[^/\x00-\x1f\x7f]+)`;
27
27
  /**
28
28
  * A project path: segments joined by a solidus, so the path is relative, below
29
29
  * the root it is read against, and spelled one way on every machine.
@@ -151,6 +151,39 @@ function readStrings(parent, name, path, label, minItems, allowed) {
151
151
  }
152
152
  return value;
153
153
  }
154
+ /**
155
+ * The target kind, a string or an object naming every language. The walk has
156
+ * already refused an undeclared member, so an object is refused for the first
157
+ * language it leaves out, then for the first language whose value is refused,
158
+ * each in the vocabulary's language order.
159
+ */
160
+ function readTargetKind(target, label) {
161
+ const value = member(target, "kind");
162
+ const perLanguage = `an object naming the kind of each of ${spell(LANGUAGES)}`;
163
+ if (value === undefined) {
164
+ return undefined;
165
+ }
166
+ if (typeof value === "string") {
167
+ if (!TARGET_KINDS.includes(value)) {
168
+ throw malformed(label, "target.kind", `${JSON.stringify(value)} is not one of ${spell(TARGET_KINDS)}, nor ${perLanguage}`);
169
+ }
170
+ return value;
171
+ }
172
+ if (!isRecord(value)) {
173
+ throw malformed(label, "target.kind", `${JSON.stringify(value)} is neither one of ${spell(TARGET_KINDS)} nor ${perLanguage}`);
174
+ }
175
+ const missing = LANGUAGES.find((language) => !Object.hasOwn(value, language));
176
+ if (missing !== undefined) {
177
+ throw malformed(label, `target.kind.${missing}`, `is not set, and ${perLanguage} names each`);
178
+ }
179
+ for (const language of LANGUAGES) {
180
+ const kind = value[language];
181
+ if (typeof kind !== "string" || !TARGET_KINDS.includes(kind)) {
182
+ throw malformed(label, `target.kind.${language}`, `${JSON.stringify(kind)} is not one of ${spell(TARGET_KINDS)}`);
183
+ }
184
+ }
185
+ return { go: value["go"], ts: value["ts"] };
186
+ }
154
187
  /** A component file's extension: a full stop followed by letters and digits. */
155
188
  const COMPONENT_EXTENSION = /^\.[A-Za-z0-9]+$/u;
156
189
  function readComponentExtensions(ts, label) {
@@ -194,7 +227,7 @@ function readProject(entry, at, label) {
194
227
  const project = requireMember(readString(entry, PROJECT_MEMBER, path, label), path, label);
195
228
  if (!PROJECT_PATH.test(project)) {
196
229
  throw malformed(label, path, `${JSON.stringify(project)} is not a path below the target root: segments joined by /, ` +
197
- "none empty, . or .., and no backslash or line break");
230
+ "none empty, . or .., and no control character");
198
231
  }
199
232
  return { shape: "project", id, project };
200
233
  }
@@ -508,7 +541,7 @@ function readDoc(value, label) {
508
541
  }
509
542
  return {
510
543
  contractVersion,
511
- targetKind: readEnum(target, "kind", "target.kind", label, TARGET_KINDS),
544
+ targetKind: readTargetKind(target, label),
512
545
  languages: readStrings(analysis, "languages", "analysis.languages", label, 0, LANGUAGES),
513
546
  minConfidence: readEnum(analysis, "min_confidence", "analysis.min_confidence", label, CONFIDENCES),
514
547
  generatedFiles: readEnum(analysis, "generated_files", "analysis.generated_files", label, GENERATED_FILES),
@@ -15,7 +15,10 @@ export const SCHEMA_ROOT = {
15
15
  kind: "section",
16
16
  members: {
17
17
  contract_version: leaf,
18
- target: { kind: "section", members: { kind: leaf } },
18
+ target: {
19
+ kind: "section",
20
+ members: { kind: { kind: "object", members: { go: leaf, ts: leaf } } },
21
+ },
19
22
  analysis: {
20
23
  kind: "section",
21
24
  members: {
@@ -205,11 +205,12 @@ export function rowReason(provenance) {
205
205
  return `recorded by ${provenance.analyzer} ${provenance.version}`;
206
206
  }
207
207
  /**
208
- * The rows of the other language's scheme that the baseline document `text` holds, in
209
- * document order and as written, which a write keeps. A document that is not a baseline,
210
- * or a row in a shape no record takes, is refused rather than dropped.
208
+ * The rows of a scheme other than `own` that the baseline document `text` holds, in
209
+ * document order and as written, which a write of `own`'s rows keeps. A document that is
210
+ * not a baseline, or a row in a shape no record takes, of any scheme, is refused rather
211
+ * than dropped.
211
212
  */
212
- export function otherSchemeRows(text) {
213
+ export function otherSchemeRows(text, own = OWN_SCHEME) {
213
214
  const refuse = (detail) => {
214
215
  throw new Error(`${BASELINE_FILE} cannot be rewritten keeping the other language's rows: ${detail}`);
215
216
  };
@@ -233,7 +234,7 @@ export function otherSchemeRows(text) {
233
234
  }
234
235
  // readRecord has checked every member is a string and the code and symbol are present.
235
236
  const { code, symbol, path, reason } = value;
236
- if (code === undefined || symbol === undefined || symbol.startsWith(OWN_SCHEME)) {
237
+ if (code === undefined || symbol === undefined || symbol.startsWith(own)) {
237
238
  return [];
238
239
  }
239
240
  return [
@@ -246,10 +247,32 @@ export function otherSchemeRows(text) {
246
247
  ];
247
248
  });
248
249
  }
250
+ /** The reference schemes in the order a baseline holds their rows. */
251
+ const SCHEME_ORDER = ["go://", "ts://"];
252
+ function schemeRank(row) {
253
+ return SCHEME_ORDER.findIndex((scheme) => row.symbol.startsWith(scheme));
254
+ }
255
+ /** A surrogate half no other completes, which Go's encoding/json decodes as U+FFFD. */
256
+ const LONE_SURROGATE = /[\ud800-\udfff]/gu;
257
+ /** One string with every lone surrogate written as U+FFFD. */
258
+ function wellFormed(text) {
259
+ return text.replace(LONE_SURROGATE, "\ufffd");
260
+ }
249
261
  /**
250
- * A baseline holding one row per finding, in the order given, which is the report's, after
251
- * the other language's rows `kept`, so two analyzers writing one document in either order
252
- * write the same bytes. Every recorded row carries the provenance as its reason. A finding
262
+ * The pieces of a JSON document with U+2028 and U+2029 escaped, as Go's encoding/json
263
+ * writes them. Neither is JSON syntax, so each stands inside a string.
264
+ */
265
+ function* separatorsEscaped(pieces) {
266
+ for (const piece of pieces) {
267
+ yield piece.replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
268
+ }
269
+ }
270
+ /**
271
+ * A baseline holding one row per finding, in the order given, which is the report's, and
272
+ * the other language's rows `kept`, Go's rows ahead of TypeScript's and each scheme's in
273
+ * the order given, so two analyzers writing one document in either order write the same
274
+ * bytes, in the form grammar/suppression.md fixes. Every recorded row carries the
275
+ * provenance as its reason. A finding
253
276
  * missing a value a row needs, and an identity missing its name or version, are refused
254
277
  * rather than written, because the grammar requires a reason on every row.
255
278
  */
@@ -264,5 +287,16 @@ export function writeBaseline(findings, provenance, kept = []) {
264
287
  }
265
288
  return { code: found.code, symbol: found.symbol, path: found.path, reason };
266
289
  });
267
- return jsonChunks({ description: BASELINE_DESCRIPTION, baseline: [...kept, ...baseline] }, 2);
290
+ const rows = [...kept, ...baseline]
291
+ .sort((a, b) => schemeRank(a) - schemeRank(b))
292
+ .map((row) => {
293
+ const { code, symbol, path, reason } = row;
294
+ return {
295
+ code: wellFormed(code),
296
+ symbol: wellFormed(symbol),
297
+ ...(path === undefined ? {} : { path: wellFormed(path) }),
298
+ ...(reason === undefined ? {} : { reason: wellFormed(reason) }),
299
+ };
300
+ });
301
+ return separatorsEscaped(jsonChunks({ description: BASELINE_DESCRIPTION, baseline: rows }, 2));
268
302
  }
@@ -3,11 +3,11 @@
3
3
  * one or more codes and a reason, and binding to every declaration that begins on the line
4
4
  * below it, or on its own line where it trails a token there.
5
5
  */
6
- import { SyntaxKind } from "@typescript/native/ast";
7
- import { nodeKey } from "./inventory.js";
6
+ import { isConstructorDeclaration, SyntaxKind, } from "@typescript/native/ast";
7
+ import { isParameterProperty, nodeKey } from "./inventory.js";
8
8
  import { byPosition, positionKey, renderPosition } from "./position.js";
9
9
  import { signatureOf } from "./signatures.js";
10
- import { SUPPRESSION_WITHOUT_REASON, SuppressionError, siteText, } from "./suppress.js";
10
+ import { isPartKind, SUPPRESSION_WITHOUT_REASON, SuppressionError, siteText, } from "./suppress.js";
11
11
  /**
12
12
  * The three expressions the grammar applies to the text of each line comment, in this
13
13
  * order: a comment in the namespace, a well-formed directive, and a directive that lacks
@@ -132,37 +132,53 @@ function lineComments(file) {
132
132
  }
133
133
  return found;
134
134
  }
135
+ function bindingKey(one) {
136
+ return `${one.declared?.id ?? ""}\u0000${one.part.id}`;
137
+ }
135
138
  /**
136
139
  * The declarations of one file by the line each begins on, the line of its first token: a
137
140
  * decorator, a modifier or the keyword. A variable's first token is its statement's
138
141
  * keyword, and a variable written on a later line of the statement also begins there. A
139
- * function also begins, for a directive, on the line of each of its parameters.
142
+ * function also begins, for a directive, on the line of each of its parameters, and a
143
+ * class, for a part kind, on the lines of its constructor and its constructor's parameters.
140
144
  */
141
145
  function declarationLines(file, held, symbols) {
142
146
  const below = new Map();
143
147
  const add = (offset, one) => {
144
148
  const line = file.getLineAndCharacterOfPosition(tokenStart(file.text, offset)).line + 1;
145
149
  const at = below.get(line) ?? [];
146
- if (!at.some((known) => known.id === one.id)) {
150
+ if (!at.some((known) => bindingKey(known) === bindingKey(one))) {
147
151
  at.push(one);
148
152
  }
149
153
  below.set(line, at);
150
154
  };
151
- const visit = (node) => {
155
+ const declaredAt = (node) => {
152
156
  const id = held.declarations.get(nodeKey(file, node));
153
- const one = id === undefined ? undefined : symbols.get(id);
157
+ return id === undefined ? undefined : symbols.get(id);
158
+ };
159
+ const visit = (node) => {
160
+ const one = declaredAt(node);
161
+ const owner = isConstructorDeclaration(node) ? declaredAt(node.parent) : undefined;
162
+ if (owner !== undefined && isConstructorDeclaration(node)) {
163
+ for (const at of [node, ...node.parameters.filter((one) => !isParameterProperty(one))]) {
164
+ add(at.pos, { declared: undefined, part: owner });
165
+ }
166
+ }
167
+ const holder = one !== undefined && isParameterProperty(node) ? declaredAt(node.parent.parent) : one;
168
+ if (one !== undefined && holder !== undefined) {
169
+ add(node.pos, { declared: one, part: holder });
170
+ }
154
171
  if (one !== undefined) {
155
- add(node.pos, one);
156
172
  // A parameter on a later line of a wrapped signature takes its directive on the
157
173
  // line above it, inside the parameter list, and the directive binds the function.
158
174
  for (const parameter of signatureOf(node)?.parameters ?? []) {
159
- add(parameter.pos, one);
175
+ add(parameter.pos, { declared: one, part: one });
160
176
  }
161
177
  const list = node.parent;
162
178
  if (node.kind === SyntaxKind.VariableDeclaration &&
163
179
  list.kind === SyntaxKind.VariableDeclarationList &&
164
180
  list.parent.kind === SyntaxKind.VariableStatement) {
165
- add(list.parent.pos, one);
181
+ add(list.parent.pos, { declared: one, part: one });
166
182
  }
167
183
  }
168
184
  node.forEachChild(visit);
@@ -223,7 +239,7 @@ export function inlineSuppressions(perProject) {
223
239
  continue;
224
240
  }
225
241
  for (const one of written.bound) {
226
- if (!known.bound.some((other) => other.id === one.id)) {
242
+ if (!known.bound.some((other) => bindingKey(other) === bindingKey(one))) {
227
243
  known.bound.push(one);
228
244
  }
229
245
  }
@@ -251,8 +267,15 @@ export function inlineSuppressions(perProject) {
251
267
  }
252
268
  break;
253
269
  case "directive": {
254
- const declared = [...bound].sort((a, b) => byPosition(a.position, b.position));
255
270
  for (const code of directive.codes) {
271
+ const targets = new Map();
272
+ for (const one of bound) {
273
+ const target = isPartKind(code) ? one.part : one.declared;
274
+ if (target !== undefined) {
275
+ targets.set(target.id, target);
276
+ }
277
+ }
278
+ const declared = [...targets.values()].sort((a, b) => byPosition(a.position, b.position));
256
279
  const base = {
257
280
  code,
258
281
  path: site.path,
@@ -34,9 +34,10 @@ export function siteText(site) {
34
34
  export const CODE_FORM = /^DS[0-9]{4}$/u;
35
35
  /**
36
36
  * A path inside the target: relative to the target root, the solidus as separator, no
37
- * leading `./`, no trailing solidus, no `.` or `..` segment, never absolute.
37
+ * leading `./`, no trailing solidus, no `.` or `..` segment, never absolute, and no
38
+ * control character; a backslash is a character of a name.
38
39
  */
39
- export const PATH_EXPRESSION = String.raw `^(?:[^/\\.\r\n][^/\\\r\n]*|\.[^/\\.\r\n][^/\\\r\n]*|\.\.[^/\\\r\n]+)(?:/(?:[^/\\.\r\n][^/\\\r\n]*|\.[^/\\.\r\n][^/\\\r\n]*|\.\.[^/\\\r\n]+))*$`;
40
+ export const PATH_EXPRESSION = String.raw `^(?:[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.\.[^/\x00-\x1f\x7f]+)(?:/(?:[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.\.[^/\x00-\x1f\x7f]+))*$`;
40
41
  const PATH_FORM = new RegExp(PATH_EXPRESSION, "u");
41
42
  /** Whether one value is a path in the form an entry's `path` takes. */
42
43
  export function isEntryPath(value) {
@@ -84,7 +84,8 @@ function declaringManifest(host, dir, targetRoot, name) {
84
84
  }
85
85
  }
86
86
  }
87
- if (at === targetRoot || relativePath(targetRoot, at) === undefined || dirnamePath(at) === at) {
87
+ // The target root is not below itself, so the walk stops after reading its manifest.
88
+ if (relativePath(targetRoot, at) === undefined) {
88
89
  return undefined;
89
90
  }
90
91
  }
@@ -1,11 +1,11 @@
1
1
  import { runAnalysis, runBaselineWrite, unansweredCounts, } from "../analysis.js";
2
- import { ConfigError } from "../config.js";
2
+ import { ConfigError, kindOf } from "../config.js";
3
3
  import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
4
4
  import { moduleIdentity } from "../consumers.js";
5
5
  import { analyzerProvenance, verdictOf } from "../findings-pass.js";
6
6
  import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
7
7
  import { partialNotes } from "../partial.js";
8
- import { buildReport, capFindings, sortFindings, } from "../report.js";
8
+ import { buildReport, LANGUAGE, capFindings, sortFindings, } from "../report.js";
9
9
  import { json } from "../reporters/json.js";
10
10
  import { RENDERINGS } from "../reporters/reporters.js";
11
11
  import { parseTemplate, TemplateError } from "../reporters/template.js";
@@ -188,7 +188,11 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
188
188
  version,
189
189
  conformance: CONFORMANCE,
190
190
  declaredGaps: DECLARED_GAPS,
191
- target: { kind, root, identity: moduleIdentity(host, scoped.target) },
191
+ target: {
192
+ kind: kindOf(kind, LANGUAGE),
193
+ root,
194
+ identity: moduleIdentity(host, scoped.target),
195
+ },
192
196
  configurations: configurationsOf(run.projects, targetRoot),
193
197
  notBuilt: notBuiltOf(run.notBuilt, targetRoot),
194
198
  loaded: run.consumers.map((consumer) => ({
@@ -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 = "7.1.0";
7
+ export const CONTRACT_VERSION = "7.2.0";
@@ -19,7 +19,6 @@ const DEPENDENCY_MEMBERS = [
19
19
  "optionalDependencies",
20
20
  ];
21
21
  const SOURCE = /(?<!\.d)\.(?:[mc]?ts|tsx)$/u;
22
- const OUTPUT = /(?:\.d\.[mc]?ts|\.[mc]?js|\.jsx)$/u;
23
22
  function isRecord(value) {
24
23
  return typeof value === "object" && value !== null && !Array.isArray(value);
25
24
  }
@@ -39,7 +38,7 @@ export function packageNameOf(specifier) {
39
38
  return first;
40
39
  }
41
40
  function inside(dir, path) {
42
- return path === dir || relativePath(dir, path) !== undefined;
41
+ return relativePath(dir, path) !== undefined;
43
42
  }
44
43
  /** The `exports` entry one subpath selects, with what a pattern's `*` matched. */
45
44
  function exportsEntry(exportsField, subpath) {
@@ -199,12 +198,10 @@ export function workspaceResolver(host, workspace) {
199
198
  return found;
200
199
  }
201
200
  }
202
- if (OUTPUT.test(path)) {
203
- const beside = emittedFrom(path, { sourceDirs: [] });
204
- const found = beside.find((candidate) => host.kindOf(candidate) === "file");
205
- if (found !== undefined) {
206
- return found;
207
- }
201
+ const beside = emittedFrom(path, { sourceDirs: [] });
202
+ const found = beside.find((candidate) => host.kindOf(candidate) === "file");
203
+ if (found !== undefined) {
204
+ return found;
208
205
  }
209
206
  return compiled(member, path) && host.kindOf(path) === "file" ? path : undefined;
210
207
  };
@@ -198,14 +198,14 @@ function memberDirectories(host, root, patterns) {
198
198
  }
199
199
  return [...included].filter((dir) => {
200
200
  const below = relativePath(root, dir);
201
- const segments = below === undefined || below === "." || below === "" ? [] : below.split("/");
201
+ const segments = below === undefined ? [] : below.split("/");
202
202
  return dir === root || !excluded.some((pattern) => pathMatches(pattern, segments));
203
203
  });
204
204
  }
205
205
  /** Whether a workspace's patterns name one directory below its root, read without listing any. */
206
206
  function namesDirectory(root, patterns, dir) {
207
207
  const below = relativePath(root, dir);
208
- if (below === undefined || below === "" || below.startsWith("..")) {
208
+ if (below === undefined || below.startsWith("..")) {
209
209
  return false;
210
210
  }
211
211
  const path = below.split("/");
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "7.1.0",
3
+ "version": "7.2.0-dev.2",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "7.1.0",
3
+ "version": "7.2.0-dev.2",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
@@ -40,7 +40,7 @@
40
40
  "@stryker-mutator/core": "10.0.0",
41
41
  "@stryker-mutator/typescript-checker": "10.0.0",
42
42
  "@stryker-mutator/vitest-runner": "10.0.0",
43
- "@types/node": "24.19.1",
43
+ "@types/node": "24.19.2",
44
44
  "eslint": "10.12.0",
45
45
  "fast-check": "4.10.2",
46
46
  "prettier": "3.9.9",
@@ -14,7 +14,7 @@ import {
14
14
  isNamespaceImport,
15
15
  } from "@typescript/native/ast";
16
16
  import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/sync";
17
- import { declarationsByName, nodeKey, type Inventory } from "./inventory.ts";
17
+ import { declarationsByName, isParameterProperty, nodeKey, type Inventory } from "./inventory.ts";
18
18
  import { UNANSWERED, type Answer } from "./query.ts";
19
19
  import type { ProjectView } from "./session.ts";
20
20
 
@@ -76,7 +76,12 @@ export function aliasChains<Brand>(project: ProjectView<Brand>, held: Inventory)
76
76
  const node = project.declarationAt(handle)?.node;
77
77
  const id =
78
78
  node === undefined ? undefined : held.declarations.get(nodeKey(node.getSourceFile(), node));
79
- if (id !== undefined && !ids.includes(id)) {
79
+ // A parameter property's node declares the parameter too, whose reads are no member's.
80
+ const parameter =
81
+ node !== undefined &&
82
+ isParameterProperty(node) &&
83
+ (symbol.flags & SymbolFlags.Property) === 0;
84
+ if (id !== undefined && !parameter && !ids.includes(id)) {
80
85
  ids.push(id);
81
86
  }
82
87
  }
package/src/analysis.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { lineOf, readComponent } from "./component-files.ts";
10
- import type { Config, Provenance } from "./config.ts";
10
+ import { kindOf, type Config, type Provenance } from "./config.ts";
11
11
  import {
12
12
  consumerLoads,
13
13
  consumerReferences,
@@ -83,7 +83,7 @@ import {
83
83
  import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
84
84
  import type { UnansweredQuestion } from "./query.ts";
85
85
  import { byPosition, isComponentFile, type Position } from "./position.ts";
86
- import type { TypeErrorSkip, UnansweredCount } from "./report.ts";
86
+ import { LANGUAGE, type TypeErrorSkip, type UnansweredCount } from "./report.ts";
87
87
  import { SetupError, setupLine, type SetupFailure } from "./setup-failure.ts";
88
88
  import {
89
89
  pathsOf,
@@ -377,7 +377,7 @@ function readProjects<Answer>(
377
377
  conventions,
378
378
  testFiles: config.ts.testFiles,
379
379
  parse,
380
- publishedAPI: config.targetKind === "library",
380
+ publishedAPI: config.targetKind !== "" && kindOf(config.targetKind, LANGUAGE) === "library",
381
381
  publishedMembers: new Set(
382
382
  (workspace?.entries ?? []).filter((entry) => entry.published).map((entry) => entry.dir),
383
383
  ),
package/src/config.ts CHANGED
@@ -13,6 +13,14 @@ export const REPOSITORY_DOCUMENT = "deadset.json";
13
13
  */
14
14
  export type TargetKind = "application" | "library";
15
15
 
16
+ /** The target kind as a source writes it: one kind for every language, or one per language. */
17
+ export type TargetKindSetting = TargetKind | { readonly go: TargetKind; readonly ts: TargetKind };
18
+
19
+ /** The kind a target kind setting gives one language's declarations. */
20
+ export function kindOf(setting: TargetKindSetting, language: Language): TargetKind {
21
+ return typeof setting === "string" ? setting : setting[language];
22
+ }
23
+
16
24
  /** The lowest reachability class a finding must reach to be reported. */
17
25
  export type Confidence = "certain" | "probable" | "possible";
18
26
 
@@ -183,7 +191,7 @@ export interface TSSection {
183
191
  */
184
192
  export interface Config {
185
193
  readonly contractVersion: string;
186
- readonly targetKind: TargetKind | "";
194
+ readonly targetKind: TargetKindSetting | "";
187
195
  readonly analysis: Analysis;
188
196
  readonly consumersComplete: boolean;
189
197
  readonly rootPatterns: readonly string[];