@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
package/src/matrix.ts CHANGED
@@ -94,14 +94,6 @@ export interface SweepResult {
94
94
  readonly components: readonly Component[];
95
95
  }
96
96
 
97
- /** Two strings ordered bytewise. */
98
- function compare(a: string, b: string): number {
99
- if (a === b) {
100
- return 0;
101
- }
102
- return a < b ? -1 : 1;
103
- }
104
-
105
97
  /**
106
98
  * What makes one reference one reference, whichever configuration made it. A consumer's
107
99
  * position is rendered against its own root, so the consumer is part of what it names.
@@ -163,7 +155,7 @@ export function matrixOf(per: readonly Configured[]): Matrix {
163
155
  references.push(reference);
164
156
  }
165
157
  });
166
- symbols.sort((a, b) => byPosition(a.position, b.position) || compare(a.ref, b.ref));
158
+ symbols.sort((a, b) => byPosition(a.position, b.position));
167
159
 
168
160
  return {
169
161
  configurations: per.map((one) => one.configuration),
@@ -113,9 +113,6 @@ interface Named {
113
113
  function namedBy(
114
114
  held: Inventory,
115
115
  ): (specifier: string, names: readonly string[]) => readonly Named[] {
116
- if (held.modules.size === 0) {
117
- return () => [];
118
- }
119
116
  const refs = new Map<string, string>();
120
117
  const children = new Map<string, InventorySymbol[]>();
121
118
  for (const symbol of held.symbols) {
package/src/ref.ts CHANGED
@@ -27,10 +27,11 @@ const TS_COMPUTED = String.raw`\[[^\]\r\n\t ]+\]`;
27
27
  const TS_HEAD = `(?:${TS_IDENT}|${TS_QUOTED})`;
28
28
  const TS_MEMBER = `(?:${TS_IDENT}|#${TS_IDENT}|${TS_QUOTED}|${TS_COMPUTED})`;
29
29
  const TS_PACKAGE = String.raw`(?:@[A-Za-z0-9._-]+/)?[A-Za-z0-9._-]+`;
30
- const TS_FILE = String.raw`(?:[^/#\r\n]+/)*[^/#\r\n]+(?:(?:\.d)?\.[mc]?ts|\.tsx|\.[mc]?js|\.jsx)`;
30
+ const TS_DIRS = String.raw`(?:[^/#\r\n]+/)*`;
31
+ const TS_FILE = String.raw`${TS_DIRS}[^/#\r\n]+(?:(?:\.d)?\.[mc]?ts|\.tsx|\.[mc]?js|\.jsx)`;
31
32
 
32
33
  const SCOPE = `${PREFIX}${TS_PACKAGE}/${TS_FILE}`;
33
- const MANIFEST_SCOPE = `${PREFIX}${TS_PACKAGE}/package\\.json`;
34
+ const MANIFEST_SCOPE = `${PREFIX}${TS_PACKAGE}/${TS_DIRS}package\\.json`;
34
35
 
35
36
  /**
36
37
  * One expression per form the grammar defines, each anchored and each the whole of
package/src/references.ts CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  isVoidExpression,
33
33
  SyntaxKind,
34
34
  type Node,
35
+ type ParameterDeclaration,
35
36
  type SourceFile,
36
37
  } from "@typescript/native/ast";
37
38
  import type { Symbol as TSSymbol } from "@typescript/native/sync";
@@ -41,7 +42,13 @@ import { destructuring, propertyReadsOf, type PropertyRead } from "./destructuri
41
42
  import { specifierCandidates } from "./configuration-files.ts";
42
43
  import { namespaceUses } from "./namespace-objects.ts";
43
44
  import { globExpression } from "./glob.ts";
44
- import { declarationsByName, nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
45
+ import {
46
+ declarationsByName,
47
+ isParameterProperty,
48
+ nodeKey,
49
+ type Inventory,
50
+ type InventorySymbol,
51
+ } from "./inventory.ts";
45
52
  import { augmentationReferences } from "./augmentations.ts";
46
53
  import { elementAccessReferences } from "./element-access.ts";
47
54
  import { indexedAccessReferences } from "./indexed-access.ts";
@@ -478,6 +485,13 @@ interface Decorated {
478
485
  readonly declaration: string;
479
486
  }
480
487
 
488
+ /** One parameter property, which its constructor writes, and the declaration that writes it. */
489
+ interface ParameterWrite {
490
+ readonly name: Node;
491
+ readonly from: string;
492
+ readonly member: string;
493
+ }
494
+
481
495
  /** One property a pattern reads, and the declaration the pattern is written inside. */
482
496
  interface Read extends PropertyRead {
483
497
  readonly from: string;
@@ -494,6 +508,7 @@ interface FileSites {
494
508
  readonly wholes: readonly WholeRead[];
495
509
  readonly urls: readonly ModuleURL[];
496
510
  readonly decorated: readonly Decorated[];
511
+ readonly parameterWrites: readonly ParameterWrite[];
497
512
  readonly keyed: readonly KeyedAccess[];
498
513
  }
499
514
 
@@ -682,6 +697,7 @@ function sitesOf(
682
697
  const wholes: WholeRead[] = [];
683
698
  const urls: ModuleURL[] = [];
684
699
  const decorated: Decorated[] = [];
700
+ const parameterWrites: ParameterWrite[] = [];
685
701
  const namespaces = namespaceUses(file);
686
702
  const literals: LiteralAt[] = [];
687
703
  const flows: FlowAt[] = [];
@@ -766,7 +782,15 @@ function sitesOf(
766
782
  }
767
783
  const held = declarations.get(nodeKey(file, node));
768
784
  const outer = enclosing;
769
- if (held !== undefined) {
785
+ // A parameter property's annotation, initializer and decorators stay with the
786
+ // parameter when the member goes, so they are written inside the constructor's class.
787
+ if (held !== undefined && isParameterProperty(node)) {
788
+ parameterWrites.push({
789
+ name: (node as ParameterDeclaration).name,
790
+ from: outer,
791
+ member: held,
792
+ });
793
+ } else if (held !== undefined) {
770
794
  enclosing = held;
771
795
  for (const modifier of (node as { readonly modifiers?: readonly Node[] }).modifiers ?? []) {
772
796
  if (isDecorator(modifier)) {
@@ -789,6 +813,7 @@ function sitesOf(
789
813
  wholes,
790
814
  urls,
791
815
  decorated,
816
+ parameterWrites,
792
817
  keyed,
793
818
  };
794
819
  }
@@ -884,15 +909,12 @@ function importedNames(file: SourceFile): ReadonlyMap<string, readonly (string |
884
909
  return names;
885
910
  }
886
911
 
887
- /**
888
- * The name node of every import binding one file's top level declares, and whether it
889
- * binds a whole module.
890
- */
891
- function importBindings(file: SourceFile): readonly { node: Node; whole: boolean }[] {
892
- const found: { node: Node; whole: boolean }[] = [];
912
+ /** The name node of every import binding one file's top level declares. */
913
+ function importBindings(file: SourceFile): readonly { node: Node }[] {
914
+ const found: { node: Node }[] = [];
893
915
  for (const statement of file.statements) {
894
916
  if (isImportEqualsDeclaration(statement)) {
895
- found.push({ node: statement.name, whole: true });
917
+ found.push({ node: statement.name });
896
918
  continue;
897
919
  }
898
920
  const clause = isImportDeclaration(statement) ? statement.importClause : undefined;
@@ -900,16 +922,16 @@ function importBindings(file: SourceFile): readonly { node: Node; whole: boolean
900
922
  continue;
901
923
  }
902
924
  if (clause.name !== undefined) {
903
- found.push({ node: clause.name, whole: false });
925
+ found.push({ node: clause.name });
904
926
  }
905
927
  const bindings = clause.namedBindings;
906
928
  if (bindings === undefined) {
907
929
  continue;
908
930
  }
909
931
  if (bindings.kind === SyntaxKind.NamespaceImport) {
910
- found.push({ node: bindings.name, whole: true });
932
+ found.push({ node: bindings.name });
911
933
  } else {
912
- found.push(...bindings.elements.map((element) => ({ node: element.name, whole: false })));
934
+ found.push(...bindings.elements.map((element) => ({ node: element.name })));
913
935
  }
914
936
  }
915
937
  return found;
@@ -1286,6 +1308,7 @@ export function references<Brand>(
1286
1308
  wholes,
1287
1309
  urls,
1288
1310
  decorated,
1311
+ parameterWrites,
1289
1312
  keyed,
1290
1313
  } of walked) {
1291
1314
  const test = testFile.get(file) === true;
@@ -1307,6 +1330,18 @@ export function references<Brand>(
1307
1330
  });
1308
1331
  }
1309
1332
 
1333
+ // The constructor stores its argument in the member a parameter property declares.
1334
+ for (const { name, from, member } of parameterWrites) {
1335
+ found.push({
1336
+ from,
1337
+ to: member,
1338
+ position: renderPosition(file, root, name.getStart()),
1339
+ use: "write",
1340
+ resolution: "syntax",
1341
+ test,
1342
+ });
1343
+ }
1344
+
1310
1345
  // One batch per capped run of the file's name nodes, the re-exports' nodes after
1311
1346
  // the uses and the evaluated modules' specifiers last, each node once: a bare star
1312
1347
  // re-export's specifier is a link and an evaluation both. A shorthand is left out
@@ -1456,15 +1491,14 @@ export function references<Brand>(
1456
1491
  symbol === UNANSWERED
1457
1492
  ? named(binding.node).map((id) => ({ id, stepped: false, guessed: true }))
1458
1493
  : chains.chainOf(symbol);
1459
- // The markup may name any export of a module a binding holds whole.
1460
- const targets = binding.whole
1461
- ? chained.flatMap((target) => [
1462
- target,
1463
- ...declaredIn(target.id)
1464
- .filter((one) => one.exported)
1465
- .map((one) => ({ ...target, id: one.id })),
1466
- ])
1467
- : chained;
1494
+ // The markup may name any export of a module a binding's chain reaches, whether a
1495
+ // namespace import binds it or a named or default import of a namespace re-export.
1496
+ const targets = chained.flatMap((target) => [
1497
+ target,
1498
+ ...declaredIn(target.id)
1499
+ .filter((one) => one.exported)
1500
+ .map((one) => ({ ...target, id: one.id })),
1501
+ ]);
1468
1502
  for (const target of targets) {
1469
1503
  found.push({
1470
1504
  from: fileId,
package/src/report.ts CHANGED
@@ -19,7 +19,7 @@ import type { StaleSuppression } from "./findings/self-check.ts";
19
19
  import type { TestFileRule } from "./references.ts";
20
20
 
21
21
  /** The version of the report schema this analyzer writes a report to. */
22
- const SCHEMA_VERSION = "8.1.0";
22
+ const SCHEMA_VERSION = "8.2.0";
23
23
 
24
24
  /** Every report schema version this analyzer reads: the one the Contract it implements admits. */
25
25
  export const SCHEMA_VERSIONS_ACCEPTED: readonly string[] = [SCHEMA_VERSION];
package/src/resolve.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  type Sort,
20
20
  type Source,
21
21
  type TargetKind,
22
+ type TargetKindSetting,
22
23
  type TemplateDelimiters,
23
24
  } from "./config.ts";
24
25
  import { CONVENTION_ROWS } from "./convention-rows.ts";
@@ -57,9 +58,9 @@ const PROJECT_MEMBER = "project";
57
58
 
58
59
  /**
59
60
  * One segment of a project path: not empty, not `.` or `..`, and holding no
60
- * solidus, backslash or line break.
61
+ * solidus or control character.
61
62
  */
62
- const PROJECT_SEGMENT = String.raw`(?:[^/\\.\r\n][^/\\\r\n]*|\.[^/\\.\r\n][^/\\\r\n]*|\.\.[^/\\\r\n]+)`;
63
+ const PROJECT_SEGMENT = String.raw`(?:[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.\.[^/\x00-\x1f\x7f]+)`;
63
64
 
64
65
  /**
65
66
  * A project path: segments joined by a solidus, so the path is relative, below
@@ -85,7 +86,7 @@ const CASCADES: readonly Cascade[] = ["roots", "full"];
85
86
  */
86
87
  interface Doc {
87
88
  readonly contractVersion: string | undefined;
88
- readonly targetKind: TargetKind | undefined;
89
+ readonly targetKind: TargetKindSetting | undefined;
89
90
  readonly languages: readonly Language[] | undefined;
90
91
  readonly minConfidence: Confidence | undefined;
91
92
  readonly generatedFiles: GeneratedFiles | undefined;
@@ -281,6 +282,55 @@ function readStrings<T extends string>(
281
282
  return value as readonly T[];
282
283
  }
283
284
 
285
+ /**
286
+ * The target kind, a string or an object naming every language. The walk has
287
+ * already refused an undeclared member, so an object is refused for the first
288
+ * language it leaves out, then for the first language whose value is refused,
289
+ * each in the vocabulary's language order.
290
+ */
291
+ function readTargetKind(
292
+ target: Record<string, unknown> | undefined,
293
+ label: string,
294
+ ): TargetKindSetting | undefined {
295
+ const value = member(target, "kind");
296
+ const perLanguage = `an object naming the kind of each of ${spell(LANGUAGES)}`;
297
+ if (value === undefined) {
298
+ return undefined;
299
+ }
300
+ if (typeof value === "string") {
301
+ if (!(TARGET_KINDS as readonly string[]).includes(value)) {
302
+ throw malformed(
303
+ label,
304
+ "target.kind",
305
+ `${JSON.stringify(value)} is not one of ${spell(TARGET_KINDS)}, nor ${perLanguage}`,
306
+ );
307
+ }
308
+ return value as TargetKind;
309
+ }
310
+ if (!isRecord(value)) {
311
+ throw malformed(
312
+ label,
313
+ "target.kind",
314
+ `${JSON.stringify(value)} is neither one of ${spell(TARGET_KINDS)} nor ${perLanguage}`,
315
+ );
316
+ }
317
+ const missing = LANGUAGES.find((language) => !Object.hasOwn(value, language));
318
+ if (missing !== undefined) {
319
+ throw malformed(label, `target.kind.${missing}`, `is not set, and ${perLanguage} names each`);
320
+ }
321
+ for (const language of LANGUAGES) {
322
+ const kind = value[language];
323
+ if (typeof kind !== "string" || !(TARGET_KINDS as readonly string[]).includes(kind)) {
324
+ throw malformed(
325
+ label,
326
+ `target.kind.${language}`,
327
+ `${JSON.stringify(kind)} is not one of ${spell(TARGET_KINDS)}`,
328
+ );
329
+ }
330
+ }
331
+ return { go: value["go"] as TargetKind, ts: value["ts"] as TargetKind };
332
+ }
333
+
284
334
  /** A component file's extension: a full stop followed by letters and digits. */
285
335
  const COMPONENT_EXTENSION = /^\.[A-Za-z0-9]+$/u;
286
336
 
@@ -355,7 +405,7 @@ function readProject(
355
405
  label,
356
406
  path,
357
407
  `${JSON.stringify(project)} is not a path below the target root: segments joined by /, ` +
358
- "none empty, . or .., and no backslash or line break",
408
+ "none empty, . or .., and no control character",
359
409
  );
360
410
  }
361
411
  return { shape: "project", id, project };
@@ -795,7 +845,7 @@ function readDoc(value: unknown, label: string): Doc {
795
845
 
796
846
  return {
797
847
  contractVersion,
798
- targetKind: readEnum(target, "kind", "target.kind", label, TARGET_KINDS),
848
+ targetKind: readTargetKind(target, label),
799
849
  languages: readStrings(analysis, "languages", "analysis.languages", label, 0, LANGUAGES),
800
850
  minConfidence: readEnum(
801
851
  analysis,
@@ -1052,7 +1102,7 @@ function settleAll(inputs: Inputs): { config: Config; provenance: Map<string, Or
1052
1102
 
1053
1103
  const config: Config = {
1054
1104
  contractVersion: at("contract_version", defaults.contractVersion, (doc) => doc.contractVersion),
1055
- targetKind: at<TargetKind | "">("target.kind", "", (doc) => doc.targetKind),
1105
+ targetKind: at<TargetKindSetting | "">("target.kind", "", (doc) => doc.targetKind),
1056
1106
  analysis: {
1057
1107
  languages: at("analysis.languages", defaults.analysis.languages, (doc) => doc.languages),
1058
1108
  minConfidence: at(
package/src/schema.ts CHANGED
@@ -1,19 +1,12 @@
1
1
  import { ConfigError } from "./config.ts";
2
2
 
3
3
  /**
4
- * How one node of the closed key list is written.
5
- *
6
- * - `leaf` is a setting: a scalar, or an array of scalars.
7
- * - `section` is an object whose member names the key list declares and which
8
- * holds no value of its own.
9
- * - `map` is an object whose member names the key list leaves open, its values
10
- * scalars: the severity and provenance objects.
11
- * - `list` is an array whose entries are objects with declared members. Where
12
- * the key list admits an entry in more than one shape, the members are every
13
- * shape's together, and which of them one entry may hold is the decoder's
14
- * refusal, not the walk's.
15
- * - `object` is one setting written as an object: its member names are declared
16
- * and a source supplies the object whole.
4
+ * How one node of the closed key list is written: `leaf`, a scalar or an array of
5
+ * scalars; `section`, an object of declared members and no value of its own; `map`, an
6
+ * object of open member names and scalar values (severity, provenance); `list`, an
7
+ * array of objects holding every admitted shape's members; `object`, one setting
8
+ * written as an object of declared members and supplied whole. Which shape an entry or
9
+ * a value takes, where the key list admits several, is the decoder's refusal.
17
10
  */
18
11
  type KeyKind = "leaf" | "section" | "map" | "list" | "object";
19
12
 
@@ -46,7 +39,10 @@ export const SCHEMA_ROOT: KeyNode = {
46
39
  kind: "section",
47
40
  members: {
48
41
  contract_version: leaf,
49
- target: { kind: "section", members: { kind: leaf } },
42
+ target: {
43
+ kind: "section",
44
+ members: { kind: { kind: "object", members: { go: leaf, ts: leaf } } },
45
+ },
50
46
  analysis: {
51
47
  kind: "section",
52
48
  members: {
package/src/signatures.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  isPropertyDeclaration,
14
14
  isVariableDeclaration,
15
15
  type ArrowFunction,
16
+ type ConstructorDeclaration,
16
17
  type FunctionDeclaration,
17
18
  type FunctionExpression,
18
19
  type MethodDeclaration,
@@ -21,7 +22,11 @@ import {
21
22
 
22
23
  /** A function-like node that carries a body of its own. */
23
24
  export type SignatureNode =
24
- FunctionDeclaration | MethodDeclaration | ArrowFunction | FunctionExpression;
25
+ | FunctionDeclaration
26
+ | MethodDeclaration
27
+ | ArrowFunction
28
+ | FunctionExpression
29
+ | ConstructorDeclaration;
25
30
 
26
31
  /** Whether one node is a function value written in place: an arrow or a function expression. */
27
32
  function isFunctionValue(node: Node | undefined): node is ArrowFunction | FunctionExpression {
@@ -303,11 +303,12 @@ interface KeptRow {
303
303
  }
304
304
 
305
305
  /**
306
- * The rows of the other language's scheme that the baseline document `text` holds, in
307
- * document order and as written, which a write keeps. A document that is not a baseline,
308
- * or a row in a shape no record takes, is refused rather than dropped.
306
+ * The rows of a scheme other than `own` that the baseline document `text` holds, in
307
+ * document order and as written, which a write of `own`'s rows keeps. A document that is
308
+ * not a baseline, or a row in a shape no record takes, of any scheme, is refused rather
309
+ * than dropped.
309
310
  */
310
- export function otherSchemeRows(text: string): readonly KeptRow[] {
311
+ export function otherSchemeRows(text: string, own: string = OWN_SCHEME): readonly KeptRow[] {
311
312
  const refuse = (detail: string): never => {
312
313
  throw new Error(
313
314
  `${BASELINE_FILE} cannot be rewritten keeping the other language's rows: ${detail}`,
@@ -331,7 +332,7 @@ export function otherSchemeRows(text: string): readonly KeptRow[] {
331
332
  }
332
333
  // readRecord has checked every member is a string and the code and symbol are present.
333
334
  const { code, symbol, path, reason } = value as Readonly<Record<string, string>>;
334
- if (code === undefined || symbol === undefined || symbol.startsWith(OWN_SCHEME)) {
335
+ if (code === undefined || symbol === undefined || symbol.startsWith(own)) {
335
336
  return [];
336
337
  }
337
338
  return [
@@ -345,10 +346,37 @@ export function otherSchemeRows(text: string): readonly KeptRow[] {
345
346
  });
346
347
  }
347
348
 
349
+ /** The reference schemes in the order a baseline holds their rows. */
350
+ const SCHEME_ORDER: readonly string[] = ["go://", "ts://"];
351
+
352
+ function schemeRank(row: { readonly symbol: string }): number {
353
+ return SCHEME_ORDER.findIndex((scheme) => row.symbol.startsWith(scheme));
354
+ }
355
+
356
+ /** A surrogate half no other completes, which Go's encoding/json decodes as U+FFFD. */
357
+ const LONE_SURROGATE = /[\ud800-\udfff]/gu;
358
+
359
+ /** One string with every lone surrogate written as U+FFFD. */
360
+ function wellFormed(text: string): string {
361
+ return text.replace(LONE_SURROGATE, "\ufffd");
362
+ }
363
+
364
+ /**
365
+ * The pieces of a JSON document with U+2028 and U+2029 escaped, as Go's encoding/json
366
+ * writes them. Neither is JSON syntax, so each stands inside a string.
367
+ */
368
+ function* separatorsEscaped(pieces: Iterable<string>): Generator<string, void, undefined> {
369
+ for (const piece of pieces) {
370
+ yield piece.replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
371
+ }
372
+ }
373
+
348
374
  /**
349
- * A baseline holding one row per finding, in the order given, which is the report's, after
350
- * the other language's rows `kept`, so two analyzers writing one document in either order
351
- * write the same bytes. Every recorded row carries the provenance as its reason. A finding
375
+ * A baseline holding one row per finding, in the order given, which is the report's, and
376
+ * the other language's rows `kept`, Go's rows ahead of TypeScript's and each scheme's in
377
+ * the order given, so two analyzers writing one document in either order write the same
378
+ * bytes, in the form grammar/suppression.md fixes. Every recorded row carries the
379
+ * provenance as its reason. A finding
352
380
  * missing a value a row needs, and an identity missing its name or version, are refused
353
381
  * rather than written, because the grammar requires a reason on every row.
354
382
  */
@@ -371,5 +399,16 @@ export function writeBaseline(
371
399
  }
372
400
  return { code: found.code, symbol: found.symbol, path: found.path, reason };
373
401
  });
374
- return jsonChunks({ description: BASELINE_DESCRIPTION, baseline: [...kept, ...baseline] }, 2);
402
+ const rows = [...kept, ...baseline]
403
+ .sort((a, b) => schemeRank(a) - schemeRank(b))
404
+ .map((row: KeptRow): KeptRow => {
405
+ const { code, symbol, path, reason } = row;
406
+ return {
407
+ code: wellFormed(code),
408
+ symbol: wellFormed(symbol),
409
+ ...(path === undefined ? {} : { path: wellFormed(path) }),
410
+ ...(reason === undefined ? {} : { reason: wellFormed(reason) }),
411
+ };
412
+ });
413
+ return separatorsEscaped(jsonChunks({ description: BASELINE_DESCRIPTION, baseline: rows }, 2));
375
414
  }
@@ -4,12 +4,18 @@
4
4
  * below it, or on its own line where it trails a token there.
5
5
  */
6
6
 
7
- import { SyntaxKind, type Node, type SourceFile } from "@typescript/native/ast";
8
- import { nodeKey, type Inventory } from "./inventory.ts";
7
+ import {
8
+ isConstructorDeclaration,
9
+ SyntaxKind,
10
+ type Node,
11
+ type SourceFile,
12
+ } from "@typescript/native/ast";
13
+ import { isParameterProperty, nodeKey, type Inventory } from "./inventory.ts";
9
14
  import { byPosition, positionKey, renderPosition, type Position } from "./position.ts";
10
15
  import type { ProjectView } from "./session.ts";
11
16
  import { signatureOf } from "./signatures.ts";
12
17
  import {
18
+ isPartKind,
13
19
  SUPPRESSION_WITHOUT_REASON,
14
20
  SuppressionError,
15
21
  siteText,
@@ -166,35 +172,64 @@ interface Declared {
166
172
  readonly position: Position;
167
173
  }
168
174
 
175
+ /**
176
+ * What one placement binds: the declaration a record for a code that reports a
177
+ * declaration binds, and the one a record for a part kind binds, which holds the part.
178
+ * A parameter property's field is the first and its class the second; a constructor's
179
+ * plain parameter binds only the second, because a constructor is no declaration.
180
+ */
181
+ interface Binding {
182
+ readonly declared: Declared | undefined;
183
+ readonly part: Declared;
184
+ }
185
+
186
+ function bindingKey(one: Binding): string {
187
+ return `${one.declared?.id ?? ""}\u0000${one.part.id}`;
188
+ }
189
+
169
190
  /**
170
191
  * The declarations of one file by the line each begins on, the line of its first token: a
171
192
  * decorator, a modifier or the keyword. A variable's first token is its statement's
172
193
  * keyword, and a variable written on a later line of the statement also begins there. A
173
- * function also begins, for a directive, on the line of each of its parameters.
194
+ * function also begins, for a directive, on the line of each of its parameters, and a
195
+ * class, for a part kind, on the lines of its constructor and its constructor's parameters.
174
196
  */
175
197
  function declarationLines(
176
198
  file: SourceFile,
177
199
  held: Inventory,
178
200
  symbols: ReadonlyMap<string, Declared>,
179
- ): ReadonlyMap<number, readonly Declared[]> {
180
- const below = new Map<number, Declared[]>();
181
- const add = (offset: number, one: Declared): void => {
201
+ ): ReadonlyMap<number, readonly Binding[]> {
202
+ const below = new Map<number, Binding[]>();
203
+ const add = (offset: number, one: Binding): void => {
182
204
  const line = file.getLineAndCharacterOfPosition(tokenStart(file.text, offset)).line + 1;
183
205
  const at = below.get(line) ?? [];
184
- if (!at.some((known) => known.id === one.id)) {
206
+ if (!at.some((known) => bindingKey(known) === bindingKey(one))) {
185
207
  at.push(one);
186
208
  }
187
209
  below.set(line, at);
188
210
  };
189
- const visit = (node: Node): void => {
211
+ const declaredAt = (node: Node): Declared | undefined => {
190
212
  const id = held.declarations.get(nodeKey(file, node));
191
- const one = id === undefined ? undefined : symbols.get(id);
213
+ return id === undefined ? undefined : symbols.get(id);
214
+ };
215
+ const visit = (node: Node): void => {
216
+ const one = declaredAt(node);
217
+ const owner = isConstructorDeclaration(node) ? declaredAt(node.parent) : undefined;
218
+ if (owner !== undefined && isConstructorDeclaration(node)) {
219
+ for (const at of [node, ...node.parameters.filter((one) => !isParameterProperty(one))]) {
220
+ add(at.pos, { declared: undefined, part: owner });
221
+ }
222
+ }
223
+ const holder =
224
+ one !== undefined && isParameterProperty(node) ? declaredAt(node.parent.parent) : one;
225
+ if (one !== undefined && holder !== undefined) {
226
+ add(node.pos, { declared: one, part: holder });
227
+ }
192
228
  if (one !== undefined) {
193
- add(node.pos, one);
194
229
  // A parameter on a later line of a wrapped signature takes its directive on the
195
230
  // line above it, inside the parameter list, and the directive binds the function.
196
231
  for (const parameter of signatureOf(node)?.parameters ?? []) {
197
- add(parameter.pos, one);
232
+ add(parameter.pos, { declared: one, part: one });
198
233
  }
199
234
  const list = node.parent;
200
235
  if (
@@ -202,7 +237,7 @@ function declarationLines(
202
237
  list.kind === SyntaxKind.VariableDeclarationList &&
203
238
  list.parent.kind === SyntaxKind.VariableStatement
204
239
  ) {
205
- add(list.parent.pos, one);
240
+ add(list.parent.pos, { declared: one, part: one });
206
241
  }
207
242
  }
208
243
  node.forEachChild(visit);
@@ -225,7 +260,7 @@ export interface InlineDirective {
225
260
  readonly site: Position;
226
261
  readonly text: string;
227
262
  readonly directive: Directive;
228
- readonly bound: Declared[];
263
+ readonly bound: Binding[];
229
264
  }
230
265
 
231
266
  /**
@@ -282,7 +317,7 @@ export function inlineSuppressions(
282
317
  continue;
283
318
  }
284
319
  for (const one of written.bound) {
285
- if (!known.bound.some((other) => other.id === one.id)) {
320
+ if (!known.bound.some((other) => bindingKey(other) === bindingKey(one))) {
286
321
  known.bound.push(one);
287
322
  }
288
323
  }
@@ -311,8 +346,15 @@ export function inlineSuppressions(
311
346
  }
312
347
  break;
313
348
  case "directive": {
314
- const declared = [...bound].sort((a, b) => byPosition(a.position, b.position));
315
349
  for (const code of directive.codes) {
350
+ const targets = new Map<string, Declared>();
351
+ for (const one of bound) {
352
+ const target = isPartKind(code) ? one.part : one.declared;
353
+ if (target !== undefined) {
354
+ targets.set(target.id, target);
355
+ }
356
+ }
357
+ const declared = [...targets.values()].sort((a, b) => byPosition(a.position, b.position));
316
358
  const base = {
317
359
  code,
318
360
  path: site.path,
package/src/suppress.ts CHANGED
@@ -88,9 +88,10 @@ export const CODE_FORM = /^DS[0-9]{4}$/u;
88
88
 
89
89
  /**
90
90
  * A path inside the target: relative to the target root, the solidus as separator, no
91
- * leading `./`, no trailing solidus, no `.` or `..` segment, never absolute.
91
+ * leading `./`, no trailing solidus, no `.` or `..` segment, never absolute, and no
92
+ * control character; a backslash is a character of a name.
92
93
  */
93
- 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]+))*$`;
94
+ 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]+))*$`;
94
95
 
95
96
  const PATH_FORM = new RegExp(PATH_EXPRESSION, "u");
96
97
 
@@ -151,7 +151,8 @@ function declaringManifest(
151
151
  }
152
152
  }
153
153
  }
154
- if (at === targetRoot || relativePath(targetRoot, at) === undefined || dirnamePath(at) === at) {
154
+ // The target root is not below itself, so the walk stops after reading its manifest.
155
+ if (relativePath(targetRoot, at) === undefined) {
155
156
  return undefined;
156
157
  }
157
158
  }