@cplieger/deadset-ts 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/dist/src/configured-declarations.js +7 -5
  3. package/dist/src/conformance-record.js +2 -2
  4. package/dist/src/convention-rows.js +67 -1
  5. package/dist/src/conventions.js +51 -9
  6. package/dist/src/edges.js +19 -16
  7. package/dist/src/findings/completion.js +9 -4
  8. package/dist/src/findings/emitters.js +35 -34
  9. package/dist/src/findings/reads-and-writes.js +3 -2
  10. package/dist/src/findings/visibility-narrowing.js +2 -1
  11. package/dist/src/inventory.js +6 -1
  12. package/dist/src/module-declarations.js +3 -3
  13. package/dist/src/outside-tests.js +5 -11
  14. package/dist/src/overrides.js +22 -10
  15. package/dist/src/ref.js +18 -2
  16. package/dist/src/reporters/sarif.js +3 -1
  17. package/dist/src/suppress-file.js +3 -5
  18. package/dist/src/version.js +1 -1
  19. package/dist/src/workspace-programs.js +6 -31
  20. package/jsr.json +1 -1
  21. package/package.json +2 -2
  22. package/src/configured-declarations.ts +7 -5
  23. package/src/conformance-record.ts +2 -2
  24. package/src/convention-rows.ts +84 -1
  25. package/src/conventions.ts +77 -9
  26. package/src/edges.ts +20 -17
  27. package/src/findings/completion.ts +11 -4
  28. package/src/findings/emitters.ts +38 -51
  29. package/src/findings/reads-and-writes.ts +7 -2
  30. package/src/findings/visibility-narrowing.ts +2 -1
  31. package/src/inventory.ts +6 -1
  32. package/src/module-declarations.ts +3 -3
  33. package/src/outside-tests.ts +5 -11
  34. package/src/overrides.ts +26 -11
  35. package/src/ref.ts +27 -4
  36. package/src/reporters/sarif.ts +5 -1
  37. package/src/suppress-file.ts +3 -6
  38. package/src/version.ts +1 -1
  39. package/src/workspace-programs.ts +6 -35
@@ -1,15 +1,17 @@
1
1
  /**
2
2
  * The overrides of the target's class members. A call through the base class names the
3
3
  * base's member and runs the override of whichever subclass the value is, so the base
4
- * member's declaration references every override. A method overriding a member of a class
5
- * declared outside the target, at any depth of its `extends` chain, is called by outside
6
- * code holding the instance through that class, so its own class references it.
4
+ * member's declaration references every override. A method, an accessor or a property,
5
+ * static or not, overriding a member of a class declared outside the target, at any depth
6
+ * of its `extends` chain, is used by outside code holding the instance or the class
7
+ * through that class, so its own class references it.
7
8
  */
8
9
  import { isClassDeclaration, isClassExpression, isExpressionWithTypeArguments, isPropertyAccessExpression, isQualifiedName, SyntaxKind, } from "@typescript/native/unstable/ast";
9
10
  import { memberComponent } from "./class-members.js";
10
11
  import { nodeKey } from "./inventory.js";
11
12
  import { SymbolFlags } from "@typescript/native/unstable/sync";
12
13
  import { UNANSWERED } from "./query.js";
14
+ import { nameComponent } from "./ref.js";
13
15
  /** The member kinds a subclass overrides. */
14
16
  const OVERRIDABLE = new Set(["method", "class-member"]);
15
17
  /** The node naming the class one `extends` entry names. */
@@ -40,7 +42,7 @@ function extending(project, held) {
40
42
  }
41
43
  /**
42
44
  * The references each base class member makes to its overrides, for the classes of one
43
- * project that extend another class of the target, and each class makes to its methods
45
+ * project that extend another class of the target, and each class makes to its members
44
46
  * that override a member of a class declared outside the target. A member a nearer base
45
47
  * already overrides is reached through that override. A private member is not
46
48
  * overridden. Each reference is placed at the override and made by its file.
@@ -102,7 +104,7 @@ export function overrideReferences(project, held, chains, testFiles) {
102
104
  const outsideNames = outsideMemberNames(project, bases, outsideBases);
103
105
  for (const [classId, names] of outsideNames) {
104
106
  for (const [component, member] of members.get(classId) ?? []) {
105
- if (member.kind === "method" && names.has(component)) {
107
+ if (names.has(component)) {
106
108
  found.push({
107
109
  from: classId,
108
110
  to: member.id,
@@ -133,15 +135,20 @@ export function overrideReferences(project, held, chains, testFiles) {
133
135
  }
134
136
  /**
135
137
  * Per class of the target with a class declared outside the target in its `extends`
136
- * chain, the names of the members that outside class's instances carry.
138
+ * chain, the member components that outside class's instances and constructor carry, a
139
+ * static one spelled as a static member's component is.
137
140
  */
138
141
  function outsideMemberNames(project, bases, outsideBases) {
139
142
  const direct = new Map();
140
143
  for (const [classId, base] of outsideBases) {
141
- const type = project.queries.declaredTypeOf(base);
142
- const properties = type === UNANSWERED ? UNANSWERED : project.queries.propertiesOf(type);
143
- if (properties !== UNANSWERED) {
144
- direct.set(classId, new Set(properties.map((property) => property.escapedName)));
144
+ const instance = project.queries.declaredTypeOf(base);
145
+ const statics = project.queries.typeOfSymbol(base);
146
+ const components = [
147
+ ...namesOf(project, instance === UNANSWERED ? undefined : instance).map(nameComponent),
148
+ ...namesOf(project, statics === UNANSWERED ? undefined : statics).map((name) => `${nameComponent(name)}:static`),
149
+ ];
150
+ if (components.length > 0) {
151
+ direct.set(classId, new Set(components));
145
152
  }
146
153
  }
147
154
  const names = new Map();
@@ -163,3 +170,8 @@ function outsideMemberNames(project, bases, outsideBases) {
163
170
  }
164
171
  return names;
165
172
  }
173
+ /** The names of a type's properties; none where the type or its properties are unanswered. */
174
+ function namesOf(project, type) {
175
+ const properties = type === undefined ? UNANSWERED : project.queries.propertiesOf(type);
176
+ return properties === UNANSWERED ? [] : properties.map((property) => property.name);
177
+ }
package/dist/src/ref.js CHANGED
@@ -43,7 +43,7 @@ const MANIFEST_SCOPE = `${PREFIX}${TS_PACKAGE}/package\\.json`;
43
43
  export const REF_EXPRESSIONS = [
44
44
  `^${SCOPE}#$`,
45
45
  `^${SCOPE}#${TS_HEAD}$`,
46
- `^${SCOPE}#${TS_HEAD}:alias$`,
46
+ `^${SCOPE}#(?:${TS_QUOTED}\\.)?${TS_HEAD}:alias$`,
47
47
  `^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})+$`,
48
48
  `^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})+:static$`,
49
49
  `^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})*(?::static)?<${TS_IDENT}>$`,
@@ -66,6 +66,9 @@ export function nameComponent(name) {
66
66
  if (IDENTIFIER.test(name) || PRIVATE_NAME.test(name)) {
67
67
  return name;
68
68
  }
69
+ return quoted(name);
70
+ }
71
+ function quoted(name) {
69
72
  const escaped = name
70
73
  .replaceAll("\\", "\\\\")
71
74
  .replaceAll("'", "\\'")
@@ -82,6 +85,11 @@ export function nameComponent(name) {
82
85
  export function computedComponent(text) {
83
86
  return `[${text.replaceAll(ASCII_WHITESPACE, "")}]`;
84
87
  }
88
+ /** The fragment of an export alias, inside the `declare module` block named, if one is. */
89
+ export function aliasFragment(name, block) {
90
+ const head = block === undefined ? "" : `${quoted(block)}.`;
91
+ return `${head}${nameComponent(name)}:alias`;
92
+ }
85
93
  function spell(component) {
86
94
  return component.computed ? computedComponent(component.text) : nameComponent(component.text);
87
95
  }
@@ -96,7 +104,7 @@ export function renderRef(module, fragment) {
96
104
  case "module":
97
105
  return `${scope}#`;
98
106
  case "alias":
99
- return `${scope}#${nameComponent(fragment.name)}:alias`;
107
+ return `${scope}#${aliasFragment(fragment.name, fragment.block)}`;
100
108
  case "dependency":
101
109
  return `${scope}#${fragment.name}:${fragment.section}`;
102
110
  case "declaration": {
@@ -107,6 +115,14 @@ export function renderRef(module, fragment) {
107
115
  }
108
116
  }
109
117
  }
118
+ /**
119
+ * One declaration's key among a run's: its reference and the file that declares it. Two
120
+ * packages of one name spell one reference for two declarations, so the reference alone
121
+ * keys neither.
122
+ */
123
+ export function declarationKey(ref, path) {
124
+ return `${ref}\u0000${path}`;
125
+ }
110
126
  /**
111
127
  * Whether one string is a reference of this language's grammar, in its canonical
112
128
  * spelling. A string that is not, whether misspelled or spelled in a form the
@@ -1,5 +1,6 @@
1
1
  import { jsonChunks } from "../json-chunks.js";
2
2
  import { KINDS } from "../kinds.js";
3
+ import { declarationKey } from "../ref.js";
3
4
  import { lineHashes, symbolFingerprint } from "./fingerprint.js";
4
5
  import { RenderError } from "./reporter.js";
5
6
  import { utf8Bytes } from "./sha256.js";
@@ -122,7 +123,8 @@ function relatedOf(found) {
122
123
  related.push({ label: "write", at, endLine: at.end_line });
123
124
  }
124
125
  for (const one of found.component.members ?? []) {
125
- if (one.ref !== found.symbol.ref) {
126
+ if (declarationKey(one.ref, one.position.path) !==
127
+ declarationKey(found.symbol.ref, found.position.path)) {
126
128
  related.push({ label: "member", at: one.position, endLine: one.position.end_line });
127
129
  }
128
130
  }
@@ -7,6 +7,7 @@
7
7
  import { jsonChunks } from "./json-chunks.js";
8
8
  import { joinPath } from "./paths.js";
9
9
  import { positionAt, walkDocument } from "./json-document.js";
10
+ import { declarationKey } from "./ref.js";
10
11
  import { CODE_FORM, SUPPRESSION_WITHOUT_REASON, SuppressionError, UNSCOPED_ENTRY, blankReason, isEntryPath, isSymbolRef, siteText, } from "./suppress.js";
11
12
  /** The name and the location the grammar fixes for the ignore file. */
12
13
  export const IGNORE_FILE = "deadset-ignore.json";
@@ -38,9 +39,6 @@ function documentSite(shape) {
38
39
  function isObject(value) {
39
40
  return typeof value === "object" && value !== null && !Array.isArray(value);
40
41
  }
41
- function referenceKey(ref, path) {
42
- return `${ref}\u0000${path}`;
43
- }
44
42
  /**
45
43
  * Reads one entry or row: the records it binds, the refusals it carries, or the
46
44
  * malformed value that ends the run.
@@ -100,7 +98,7 @@ function readRecord(value, site, byReference, shape) {
100
98
  if (refusals.length > 0) {
101
99
  return { records: [], refusals };
102
100
  }
103
- const named = byReference.get(referenceKey(symbol, written.path)) ?? [];
101
+ const named = byReference.get(declarationKey(symbol, written.path)) ?? [];
104
102
  if (named.length === 0) {
105
103
  return { records: [{ ...written, bound: "" }], refusals: [] };
106
104
  }
@@ -109,7 +107,7 @@ function readRecord(value, site, byReference, shape) {
109
107
  function byReferenceOf(symbols) {
110
108
  const byReference = new Map();
111
109
  for (const symbol of symbols) {
112
- const key = referenceKey(symbol.ref, symbol.position.path);
110
+ const key = declarationKey(symbol.ref, symbol.position.path);
113
111
  byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
114
112
  }
115
113
  return byReference;
@@ -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 = "6.0.0";
7
+ export const CONTRACT_VERSION = "6.1.0";
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { ModuleKind, ModuleResolutionKind } from "@typescript/native/unstable/sync";
9
9
  import { isStringLiteralLikeNode } from "@typescript/native/unstable/ast";
10
- import { dirnamePath, joinPath, resolvePath } from "./paths.js";
10
+ import { dirnamePath, resolvePath } from "./paths.js";
11
11
  /** The most questions one round trip carries. */
12
12
  const BATCH = 2048;
13
13
  /**
@@ -20,31 +20,6 @@ function analysisOptions(parsed) {
20
20
  const { composite, rootDir: _rootDir, outDir: _outDir, declarationDir: _declarationDir, tsBuildInfoFile: _tsBuildInfoFile, ...kept } = parsed.options;
21
21
  return composite === true ? { ...kept, declaration: true } : kept;
22
22
  }
23
- /**
24
- * The references a built program can carry: only where every referenced
25
- * configuration reads and is composite with output, because the compiler reports a
26
- * broken reference at syntax a built program does not have.
27
- */
28
- function carriedReferences(engine, parsed) {
29
- const references = parsed.projectReferences ?? [];
30
- for (const reference of references) {
31
- // A reference that names a directory names the `tsconfig.json` in it.
32
- const configFile = reference.path.endsWith(".json")
33
- ? reference.path
34
- : joinPath(reference.path, "tsconfig.json");
35
- let options;
36
- try {
37
- options = engine.parseConfigFile(configFile).options;
38
- }
39
- catch {
40
- return [];
41
- }
42
- if (options.composite !== true || options.noEmit === true) {
43
- return [];
44
- }
45
- }
46
- return references;
47
- }
48
23
  /** The conditions an `exports` lookup under one configuration and one resolution mode reads. */
49
24
  function conditionsOf(parsed, mode) {
50
25
  const resolution = parsed.options.moduleResolution;
@@ -94,7 +69,7 @@ function configurationOf(engine, host, reading) {
94
69
  if (reason !== undefined) {
95
70
  return { kind: "unread", reason };
96
71
  }
97
- const { options } = reading.parsed;
72
+ const { options, projectReferences: references = [] } = reading.parsed;
98
73
  const raw = options["pathsBasePath"];
99
74
  const base = typeof raw === "string" ? raw : dirnamePath(reading.configFile);
100
75
  const paths = {};
@@ -117,8 +92,8 @@ function configurationOf(engine, host, reading) {
117
92
  ...(options.composite === true ? { composite: false, declaration: true } : {}),
118
93
  paths,
119
94
  },
120
- ...(reading.references.length > 0
121
- ? { references: reading.references.map((reference) => ({ path: reference.path })) }
95
+ ...(references.length > 0
96
+ ? { references: references.map((reference) => ({ path: reference.path })) }
122
97
  : {}),
123
98
  ...(exclude === undefined ? {} : { exclude }),
124
99
  });
@@ -150,6 +125,7 @@ function configurationOf(engine, host, reading) {
150
125
  /** The program one configuration is built as, under the answers it holds so far. */
151
126
  function programOf(engine, reading) {
152
127
  const compilerOptions = analysisOptions(reading.parsed);
128
+ const references = reading.parsed.projectReferences ?? [];
153
129
  const resolver = engine.createModuleResolver(compilerOptions, reading.entries);
154
130
  return {
155
131
  resolver,
@@ -159,7 +135,7 @@ function programOf(engine, reading) {
159
135
  options: {
160
136
  moduleResolver: resolver,
161
137
  configFileParsingDiagnostics: reading.parsed.errors,
162
- ...(reading.references.length > 0 ? { projectReferences: reading.references } : {}),
138
+ ...(references.length > 0 ? { projectReferences: references } : {}),
163
139
  },
164
140
  },
165
141
  };
@@ -292,7 +268,6 @@ export function workspacePrograms(engine, host, configFiles, resolver) {
292
268
  {
293
269
  configFile,
294
270
  parsed,
295
- references: carriedReferences(engine, parsed),
296
271
  entries: [],
297
272
  seen: new Set(),
298
273
  bound: new Map(),
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
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": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
@@ -29,7 +29,7 @@
29
29
  "lint:fix": "eslint . --fix && prettier --write ."
30
30
  },
31
31
  "dependencies": {
32
- "@typescript/native": "npm:typescript@7.1.0-dev.20261006.1"
32
+ "@typescript/native": "npm:typescript@7.1.0-dev.20261007.1"
33
33
  },
34
34
  "overrides": {
35
35
  "qs": "^6.15.2"
@@ -207,8 +207,8 @@ function modulesNamed<Brand>(
207
207
  }
208
208
 
209
209
  /**
210
- * The declaration each entry names in one project. A symbol entry names the
211
- * inventory's declaration whose reference it spells exactly; a module entry names the
210
+ * The declaration each entry names in one project. A symbol entry names every
211
+ * declaration of the inventory whose reference it spells exactly; a module entry names the
212
212
  * declaration its path reaches from the exports of the module its specifier resolves
213
213
  * to; a global entry names the declaration its path reaches from the global scope.
214
214
  * Every alias along a path is followed to the declaration it stands for.
@@ -221,15 +221,17 @@ export function resolveEntries<Brand>(
221
221
  if (entries.length === 0) {
222
222
  return [];
223
223
  }
224
- const byRef = new Map(held.symbols.map((symbol) => [symbol.ref, symbol.id]));
224
+ const byRef = new Map<string, string[]>();
225
+ for (const symbol of held.symbols) {
226
+ byRef.set(symbol.ref, [...(byRef.get(symbol.ref) ?? []), symbol.id]);
227
+ }
225
228
  const modules = modulesNamed(
226
229
  project,
227
230
  new Set(entries.flatMap((entry) => (entry.shape === "module" ? [entry.module] : []))),
228
231
  );
229
232
  return entries.map((entry) => {
230
233
  if (entry.shape === "symbol") {
231
- const id = byRef.get(entry.symbol);
232
- return { entry, handles: new Set<string>(), ids: new Set(id === undefined ? [] : [id]) };
234
+ return { entry, handles: new Set<string>(), ids: new Set(byRef.get(entry.symbol) ?? []) };
233
235
  }
234
236
  const { components, static: isStatic } = componentsOf(
235
237
  entry.shape === "module" ? entry.name : entry.global,
@@ -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: "4.0.0",
5
+ corpusVersion: "4.1.0",
6
6
  result: "pass",
7
- digest: "sha256:8250b61deb71467139bf4cbcaf10802cdabb1a2c2261a11a571fc1ff55bfc3dc",
7
+ digest: "sha256:167f770cdff2776fa7ab28b7ff6121c4d7bdb01d83c5ba0057d9303281614008",
8
8
  },
9
9
  gaps: [],
10
10
  } as const;
@@ -31,6 +31,20 @@ export interface ShortNameReading extends MoveReading {
31
31
  readonly package: string;
32
32
  }
33
33
 
34
+ /**
35
+ * One configuration property whose value makes the tool's output import a runtime helper
36
+ * package, so the package is used where the property holds that value.
37
+ */
38
+ export interface HelperReading extends MoveReading {
39
+ /**
40
+ * The value: `true`, or a string the property holds, alone, as an element of an array,
41
+ * or as the first element of an array that is such an element.
42
+ */
43
+ readonly value: true | string;
44
+ /** The package the output imports its helpers from. */
45
+ readonly package: string;
46
+ }
47
+
34
48
  /** One directory a row's globs name that a configuration property moves. */
35
49
  export interface DirectoryMove {
36
50
  /** The placeholder the row's globs and later moves write as `<id>`. */
@@ -71,6 +85,8 @@ export interface ConventionRow {
71
85
  readonly selections?: readonly MoveReading[];
72
86
  /** The members of a `package.json` the tool reads its configuration from. */
73
87
  readonly manifestKeys?: readonly string[];
88
+ /** The settings that make the tool's output import a runtime helper package. */
89
+ readonly helpers?: readonly HelperReading[];
74
90
  }
75
91
 
76
92
  const SCRIPT = "{js,jsx,ts,tsx}";
@@ -105,6 +121,12 @@ const SOLIDSTART_PLUGIN = { module: "@solidjs/start/config", export: "solidStart
105
121
 
106
122
  const NUXT_CONFIG = [`nuxt.config.${ANY_MODULE}`];
107
123
 
124
+ const BABEL_CONFIG = [
125
+ "babel.config.{json,js,cjs,mjs,cts,mts,ts}",
126
+ ".babelrc",
127
+ ".babelrc.{json,js,cjs,mjs,cts}",
128
+ ];
129
+
108
130
  const STRYKER_CONFIG = "{stryker,.stryker}.{conf,config}.{json,js,mjs,cjs}";
109
131
  const STRYKER_MODULE = "{stryker,.stryker}.{conf,config}.{js,mjs,cjs}";
110
132
  const VITEST_CONFIG = `{vitest,vite}.config.${ANY_MODULE}`;
@@ -132,6 +154,19 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
132
154
  },
133
155
  ],
134
156
  },
157
+ {
158
+ name: "babel",
159
+ package: "@babel/core",
160
+ range: ">=7.0.0 <9.0.0",
161
+ entries: [],
162
+ moves: [],
163
+ helpers: ["@babel/plugin-transform-runtime", "@babel/transform-runtime"].map((value) => ({
164
+ property: "plugins",
165
+ files: BABEL_CONFIG,
166
+ value,
167
+ package: "@babel/runtime",
168
+ })),
169
+ },
135
170
  {
136
171
  name: "eslint",
137
172
  package: "eslint",
@@ -304,6 +339,21 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
304
339
  },
305
340
  ],
306
341
  },
342
+ {
343
+ name: "rolldown",
344
+ package: "rolldown",
345
+ range: ">=1.0.0 <2.0.0",
346
+ entries: [],
347
+ moves: [],
348
+ helpers: [
349
+ {
350
+ property: "transform.helpers.mode",
351
+ files: [`rolldown.config.${ANY_MODULE}`],
352
+ value: "Runtime",
353
+ package: "@oxc-project/runtime",
354
+ },
355
+ ],
356
+ },
307
357
  {
308
358
  name: "solidstart",
309
359
  package: "@solidjs/start",
@@ -350,7 +400,10 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
350
400
  range: ">=7.0.0 <11.0.0",
351
401
  entries: [STRYKER_MODULE],
352
402
  moves: [],
353
- selections: [{ property: "mutate", files: [STRYKER_CONFIG] }],
403
+ selections: [
404
+ { property: "mutate", files: [STRYKER_CONFIG] },
405
+ { property: "ignorePatterns", files: [STRYKER_CONFIG] },
406
+ ],
354
407
  shortNames: [
355
408
  {
356
409
  property: "testRunner",
@@ -481,6 +534,36 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
481
534
  },
482
535
  ],
483
536
  },
537
+ {
538
+ name: "swc",
539
+ package: "@swc/core",
540
+ range: ">=1.0.0 <2.0.0",
541
+ entries: [],
542
+ moves: [],
543
+ helpers: [
544
+ {
545
+ property: "jsc.externalHelpers",
546
+ files: [".swcrc"],
547
+ value: true,
548
+ package: "@swc/helpers",
549
+ },
550
+ ],
551
+ },
552
+ {
553
+ name: "vite",
554
+ package: "vite",
555
+ range: ">=8.0.0 <9.0.0",
556
+ entries: [],
557
+ moves: [],
558
+ helpers: [
559
+ {
560
+ property: "oxc.helpers.mode",
561
+ files: [VITE_CONFIG],
562
+ value: "Runtime",
563
+ package: "@oxc-project/runtime",
564
+ },
565
+ ],
566
+ },
484
567
  {
485
568
  name: "vitest",
486
569
  package: "vitest",
@@ -50,6 +50,7 @@ import {
50
50
  CONVENTION_ROWS,
51
51
  type ConventionRow,
52
52
  type DirectoryMove,
53
+ type HelperReading,
53
54
  type MoveReading,
54
55
  type OptionsCall,
55
56
  type ShortNameReading,
@@ -110,7 +111,10 @@ export interface Conventions {
110
111
  readonly globs: readonly ConventionGlob[];
111
112
  readonly generated: readonly GeneratedDirectory[];
112
113
  readonly failures: readonly ConventionFailure[];
113
- /** The dependencies the applied rows' short names and manifest keys use, by name, each once. */
114
+ /**
115
+ * The dependencies the applied rows' short names, manifest keys and helper settings use,
116
+ * by name, each once.
117
+ */
114
118
  readonly uses: readonly string[];
115
119
  /** Per configuration file, absolute, the strings its applied rows' selection keys hold. */
116
120
  readonly selected: ReadonlyMap<string, Selected>;
@@ -136,6 +140,9 @@ type ParseSource = (fileName: string, text: string) => SourceFile;
136
140
 
137
141
  const JSON_EXTENSION = ".json";
138
142
 
143
+ /** A configuration file with no extension that its tool reads as a JSON document. */
144
+ const JSON_RC = /^\.[\w-]+rc$/u;
145
+
139
146
  /** The sections an installed package is required by, so its absence fails the run. */
140
147
  const REQUIRED_SECTIONS = ["dependencies", "devDependencies"] as const;
141
148
 
@@ -533,11 +540,10 @@ function readPath(
533
540
  }
534
541
 
535
542
  /**
536
- * Every string literal one file writes at one property's path inside the objects it is
537
- * read from, as {@link readProperty} reads them: the value itself, or each element of an
538
- * array value, whatever else the path holds.
543
+ * Every value one file writes at one property's path inside the objects it is read from,
544
+ * as {@link readProperty} reads them, whatever else the path holds.
539
545
  */
540
- function stringsAt(file: SourceFile, property: string, call?: OptionsCall): readonly Node[] {
546
+ function valuesAt(file: SourceFile, property: string, call?: OptionsCall): readonly Node[] {
541
547
  const declared = declaredValues(file);
542
548
  const objects =
543
549
  call === undefined
@@ -558,10 +564,8 @@ function stringsAt(file: SourceFile, property: string, call?: OptionsCall): read
558
564
  const held = unwrapped(member.initializer);
559
565
  if (rest.length > 0) {
560
566
  visit(held, rest);
561
- } else if (literalText(held) !== undefined) {
567
+ } else {
562
568
  found.push(held);
563
- } else if (isArrayLiteralExpression(held)) {
564
- found.push(...held.elements.map(unwrapped).filter((one) => literalText(one) !== undefined));
565
569
  }
566
570
  }
567
571
  };
@@ -573,6 +577,36 @@ function stringsAt(file: SourceFile, property: string, call?: OptionsCall): read
573
577
  return found;
574
578
  }
575
579
 
580
+ /** The string literals among values: each value itself, or each element of an array value. */
581
+ function stringsAt(file: SourceFile, property: string, call?: OptionsCall): readonly Node[] {
582
+ return valuesAt(file, property, call).flatMap((held) => {
583
+ if (literalText(held) !== undefined) {
584
+ return [held];
585
+ }
586
+ return isArrayLiteralExpression(held)
587
+ ? held.elements.map(unwrapped).filter((one) => literalText(one) !== undefined)
588
+ : [];
589
+ });
590
+ }
591
+
592
+ /** Whether one value holds a helper setting's value, as {@link HelperReading} states it. */
593
+ function holdsValue(held: Node, value: true | string): boolean {
594
+ if (value === true) {
595
+ return held.kind === SyntaxKind.TrueKeyword;
596
+ }
597
+ if (literalText(held) === value) {
598
+ return true;
599
+ }
600
+ return (
601
+ isArrayLiteralExpression(held) &&
602
+ held.elements.some((element) => {
603
+ const one = unwrapped(element);
604
+ const first = isArrayLiteralExpression(one) ? one.elements[0] : one;
605
+ return first !== undefined && literalText(first) === value;
606
+ })
607
+ );
608
+ }
609
+
576
610
  /** The strings an applied row's selection keys hold, added per configuration file to `into`. */
577
611
  function selectedStrings(
578
612
  host: Host,
@@ -760,7 +794,11 @@ function readingFiles(
760
794
  } catch {
761
795
  continue;
762
796
  }
763
- found.push({ name, text, file: parse(path, text) });
797
+ found.push({
798
+ name,
799
+ text,
800
+ file: parse(JSON_RC.test(name) ? `${path}${JSON_EXTENSION}` : path, text),
801
+ });
764
802
  }
765
803
  }
766
804
  return found;
@@ -808,6 +846,33 @@ function shortNameUses(
808
846
  return [...used].filter((name) => declared.has(name)).sort(compare);
809
847
  }
810
848
 
849
+ /** The declared dependencies an applied row's helper settings use. */
850
+ function helperUses(
851
+ host: Host,
852
+ parse: ParseSource,
853
+ dir: string,
854
+ manifest: unknown,
855
+ readings: readonly HelperReading[],
856
+ ): readonly string[] {
857
+ const declared = new Set(
858
+ DEPENDENCY_SECTIONS.flatMap((section) => {
859
+ const held = isRecord(manifest) ? manifest[section] : undefined;
860
+ return isRecord(held) ? Object.keys(held) : [];
861
+ }),
862
+ );
863
+ return readings
864
+ .filter(
865
+ (reading) =>
866
+ declared.has(reading.package) &&
867
+ readingFiles(host, parse, dir, reading.files).some(({ file }) =>
868
+ valuesAt(file, reading.property, reading.call).some((held) =>
869
+ holdsValue(held, reading.value),
870
+ ),
871
+ ),
872
+ )
873
+ .map((reading) => reading.package);
874
+ }
875
+
811
876
  /** Two strings ordered bytewise. */
812
877
  function compare(a: string, b: string): number {
813
878
  if (a === b) {
@@ -897,6 +962,9 @@ export function readConventions(
897
962
  for (const name of manifestKeyUses(declared, row.manifestKeys ?? [])) {
898
963
  uses.add(name);
899
964
  }
965
+ for (const name of helperUses(host, parse, manifest.dir, declared, row.helpers ?? [])) {
966
+ uses.add(name);
967
+ }
900
968
  selectedStrings(host, parse, manifest.dir, row.selections ?? [], selected);
901
969
  const excludes = [
902
970
  ...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),