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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/src/alias-chain.js +6 -2
  2. package/dist/src/analysis.js +3 -1
  3. package/dist/src/config.js +4 -0
  4. package/dist/src/conformance-record.js +2 -2
  5. package/dist/src/decorator.js +3 -2
  6. package/dist/src/findings/unused-declarations.js +3 -3
  7. package/dist/src/injection-container.js +28 -25
  8. package/dist/src/intra-function-parts.js +72 -11
  9. package/dist/src/inventory.js +18 -3
  10. package/dist/src/kinds.js +2 -2
  11. package/dist/src/ref.js +3 -2
  12. package/dist/src/references.js +38 -20
  13. package/dist/src/report.js +1 -1
  14. package/dist/src/resolve.js +37 -4
  15. package/dist/src/schema.js +4 -1
  16. package/dist/src/suppress-file.js +43 -9
  17. package/dist/src/suppress-inline.js +35 -12
  18. package/dist/src/suppress.js +3 -2
  19. package/dist/src/verbs/analyze.js +7 -3
  20. package/dist/src/version.js +1 -1
  21. package/jsr.json +1 -1
  22. package/package.json +1 -1
  23. package/src/alias-chain.ts +7 -2
  24. package/src/analysis.ts +3 -3
  25. package/src/config.ts +9 -1
  26. package/src/conformance-record.ts +2 -2
  27. package/src/decorator.ts +8 -2
  28. package/src/findings/unused-declarations.ts +3 -3
  29. package/src/injection-container.ts +31 -26
  30. package/src/intra-function-parts.ts +95 -11
  31. package/src/inventory.ts +26 -2
  32. package/src/kinds.ts +2 -2
  33. package/src/ref.ts +3 -2
  34. package/src/references.ts +55 -21
  35. package/src/report.ts +1 -1
  36. package/src/resolve.ts +56 -6
  37. package/src/schema.ts +10 -14
  38. package/src/signatures.ts +6 -1
  39. package/src/suppress-file.ts +48 -9
  40. package/src/suppress-inline.ts +57 -15
  41. package/src/suppress.ts +3 -2
  42. package/src/verbs/analyze.ts +7 -2
  43. package/src/version.ts +1 -1
@@ -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) {
@@ -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";
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "7.1.0-dev.2",
3
+ "version": "7.2.0-dev.1",
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-dev.2",
3
+ "version": "7.2.0-dev.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
@@ -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[];
@@ -2,9 +2,9 @@
2
2
  // Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
3
3
  export const RECORD = {
4
4
  conformance: {
5
- corpusVersion: "5.1.0",
5
+ corpusVersion: "5.2.0",
6
6
  result: "pass",
7
- digest: "sha256:befd5e68f8d68e859b2a0920464a5470a157d5d40a881d9cca28716a4575751d",
7
+ digest: "sha256:4050d1b67fb757bc12a60f8ece7317a3e8485d3383ec52919cd24b9dfd5509dc",
8
8
  },
9
9
  gaps: [],
10
10
  } as const;
package/src/decorator.ts CHANGED
@@ -14,7 +14,12 @@
14
14
  import type { Node } from "@typescript/native/ast";
15
15
  import { decoratorsOf, spelledDecorator } from "./calls.ts";
16
16
  import type { DetectorInput, Evidence } from "./exempt.ts";
17
- import { nodeKey, type InventorySymbol, type SymbolKind } from "./inventory.ts";
17
+ import {
18
+ isParameterProperty,
19
+ nodeKey,
20
+ type InventorySymbol,
21
+ type SymbolKind,
22
+ } from "./inventory.ts";
18
23
  import { renderPosition } from "./position.ts";
19
24
 
20
25
  /** The kinds of declaration a decorator is attached to and holds back. */
@@ -44,7 +49,8 @@ export function decorator<Brand>(input: DetectorInput<Brand>): readonly Evidence
44
49
  const visit = (node: Node): void => {
45
50
  const id = held.declarations.get(nodeKey(file, node));
46
51
  const symbol = id === undefined ? undefined : byId.get(id);
47
- if (symbol !== undefined && DECORATED.has(symbol.kind)) {
52
+ // A parameter property's decorator is attached to its parameter, and to no member.
53
+ if (symbol !== undefined && DECORATED.has(symbol.kind) && !isParameterProperty(node)) {
48
54
  for (const attached of decoratorsOf(node)) {
49
55
  const detail = `decorated by ${spelledDecorator(attached)}`;
50
56
  const site = renderPosition(file, targetRoot, attached.getStart());
@@ -78,13 +78,13 @@ function codeOf(judged: Judged): string | undefined {
78
78
  ) {
79
79
  return undefined;
80
80
  }
81
- if (candidate.unreferencedTest) {
82
- return unreferencedCode(symbol);
83
- }
84
81
  const referenced = candidate.productionRefs + candidate.testRefs > 0;
85
82
  if (symbol.kind === "enum-member" && !referenced) {
86
83
  return undefined;
87
84
  }
85
+ if (candidate.unreferencedTest) {
86
+ return unreferencedCode(symbol);
87
+ }
88
88
  if (candidate.heldByTests === candidate.id) {
89
89
  return "DS1004";
90
90
  }
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * The injection-container class: a container constructs a class passed to a call
3
3
  * `ts.injection_registrations` names, or one a parameter of whose constructor carries a
4
- * decorator, and populates each of its properties and accessors that carries one, a base
5
- * class's included. A constructor and its parameters are no declarations of the
6
- * inventory, so the class stands for them and the registration referencing it keeps it.
4
+ * decorator, passes every argument of its constructor, and populates each of its
5
+ * properties and accessors that carries a decorator, a base class's included. So each
6
+ * parameter property is injected, decorated or not. A constructor is no declaration of
7
+ * the inventory, so the class stands for it and the registration referencing it keeps it.
7
8
  */
8
9
 
9
10
  import {
@@ -14,6 +15,7 @@ import {
14
15
  isPropertyDeclaration,
15
16
  isSetAccessorDeclaration,
16
17
  type CallExpression,
18
+ type Decorator,
17
19
  type Node,
18
20
  } from "@typescript/native/ast";
19
21
  import { aliasChains } from "./alias-chain.ts";
@@ -27,7 +29,7 @@ import {
27
29
  type ResolvedEntry,
28
30
  } from "./configured-declarations.ts";
29
31
  import type { DetectorInput, Evidence } from "./exempt.ts";
30
- import { nodeKey } from "./inventory.ts";
32
+ import { isParameterProperty, nodeKey } from "./inventory.ts";
31
33
  import { renderPosition } from "./position.ts";
32
34
 
33
35
  /** One class a container constructs, by the registration that says so. */
@@ -37,13 +39,14 @@ interface Registration {
37
39
  readonly detail: string;
38
40
  }
39
41
 
40
- /** Whether one member declaration is a property or an accessor carrying a decorator. */
42
+ /** Whether one member declaration is a parameter property, or a property or accessor carrying a decorator. */
41
43
  function isInjected(node: Node): boolean {
42
44
  return (
43
- (isPropertyDeclaration(node) ||
45
+ isParameterProperty(node) ||
46
+ ((isPropertyDeclaration(node) ||
44
47
  isGetAccessorDeclaration(node) ||
45
48
  isSetAccessorDeclaration(node)) &&
46
- decoratorsOf(node).length > 0
49
+ decoratorsOf(node).length > 0)
47
50
  );
48
51
  }
49
52
 
@@ -77,21 +80,24 @@ function registrations<Brand>(input: DetectorInput<Brand>): readonly Registratio
77
80
  });
78
81
  }
79
82
 
80
- /** The classes of the target a parameter of whose constructor carries a decorator. */
81
- function constructedForParameters<Brand>(input: DetectorInput<Brand>): ReadonlySet<string> {
83
+ /**
84
+ * The classes of the target a parameter of whose constructor carries a decorator, each
85
+ * with the first such decorator.
86
+ */
87
+ function constructedForParameters<Brand>(
88
+ input: DetectorInput<Brand>,
89
+ ): ReadonlyMap<string, Decorator> {
82
90
  const { project, held } = input;
83
- const found = new Set<string>();
91
+ const found = new Map<string, Decorator>();
84
92
  for (const file of project.ownSourceFiles()) {
85
93
  const visit = (node: Node): void => {
86
94
  if (isClassDeclaration(node) || isClassExpression(node)) {
87
95
  const id = held.declarations.get(nodeKey(file, node));
88
- const asks = node.members.some(
89
- (member) =>
90
- isConstructorDeclaration(member) &&
91
- member.parameters.some((parameter) => decoratorsOf(parameter).length > 0),
96
+ const [asks] = node.members.flatMap((member) =>
97
+ isConstructorDeclaration(member) ? member.parameters.flatMap(decoratorsOf) : [],
92
98
  );
93
- if (id !== undefined && asks) {
94
- found.add(id);
99
+ if (id !== undefined && asks !== undefined) {
100
+ found.set(id, asks);
95
101
  }
96
102
  }
97
103
  node.forEachChild(visit);
@@ -110,7 +116,7 @@ export function injectionContainer<Brand>(input: DetectorInput<Brand>): readonly
110
116
  const { project, held, targetRoot } = input;
111
117
  const registered = registrations(input);
112
118
  const constructed = constructedForParameters(input);
113
- const classes = new Set([...registered.map((one) => one.id), ...constructed]);
119
+ const classes = new Set([...registered.map((one) => one.id), ...constructed.keys()]);
114
120
  if (classes.size === 0) {
115
121
  return [];
116
122
  }
@@ -125,16 +131,15 @@ export function injectionContainer<Brand>(input: DetectorInput<Brand>): readonly
125
131
  found.push({ id: member.id, detail: registration.detail, site: at(registration.site) });
126
132
  }
127
133
  }
128
- for (const id of constructed) {
134
+ for (const [id, asks] of constructed) {
129
135
  for (const member of injected(id)) {
130
- const [decorator] = decoratorsOf(member.node);
131
- if (decorator !== undefined) {
132
- found.push({
133
- id: member.id,
134
- detail: `injected by ${spelledDecorator(decorator)}`,
135
- site: at(decorator),
136
- });
137
- }
136
+ // An undecorated parameter property is passed because a sibling parameter asks.
137
+ const [decorator = asks] = decoratorsOf(member.node);
138
+ found.push({
139
+ id: member.id,
140
+ detail: `injected by ${spelledDecorator(decorator)}`,
141
+ site: at(decorator),
142
+ });
138
143
  }
139
144
  }
140
145
  return found;