@aoelite/refpact-ts 0.0.0-stage → 0.1.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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -2
  3. package/dist/baseline.d.ts +26 -0
  4. package/dist/baseline.js +65 -0
  5. package/dist/core/index.d.ts +79 -0
  6. package/dist/core/index.js +90 -0
  7. package/dist/core/model.d.ts +340 -0
  8. package/dist/core/model.js +1 -0
  9. package/dist/core/results.d.ts +237 -0
  10. package/dist/core/results.js +1 -0
  11. package/dist/core/wasm.d.ts +7 -0
  12. package/dist/core/wasm.js +67 -0
  13. package/dist/custom.d.ts +129 -0
  14. package/dist/custom.js +217 -0
  15. package/dist/filters.d.ts +73 -0
  16. package/dist/filters.js +39 -0
  17. package/dist/frontend/checks.d.ts +15 -0
  18. package/dist/frontend/checks.js +12 -0
  19. package/dist/frontend/classic.d.ts +3 -0
  20. package/dist/frontend/classic.js +105 -0
  21. package/dist/frontend/compiler.d.ts +93 -0
  22. package/dist/frontend/compiler.js +116 -0
  23. package/dist/frontend/components.d.ts +29 -0
  24. package/dist/frontend/components.js +111 -0
  25. package/dist/frontend/declarations.d.ts +45 -0
  26. package/dist/frontend/declarations.js +158 -0
  27. package/dist/frontend/dependencies.d.ts +53 -0
  28. package/dist/frontend/dependencies.js +337 -0
  29. package/dist/frontend/imports.d.ts +149 -0
  30. package/dist/frontend/imports.js +765 -0
  31. package/dist/frontend/native.d.ts +3 -0
  32. package/dist/frontend/native.js +165 -0
  33. package/dist/frontend/paths.d.ts +10 -0
  34. package/dist/frontend/paths.js +18 -0
  35. package/dist/frontend/project.d.ts +118 -0
  36. package/dist/frontend/project.js +775 -0
  37. package/dist/group.d.ts +74 -0
  38. package/dist/group.js +106 -0
  39. package/dist/index.d.ts +35 -0
  40. package/dist/index.js +27 -0
  41. package/dist/items.d.ts +133 -0
  42. package/dist/items.js +179 -0
  43. package/dist/layers.d.ts +112 -0
  44. package/dist/layers.js +215 -0
  45. package/dist/onion.d.ts +78 -0
  46. package/dist/onion.js +96 -0
  47. package/dist/patterns.d.ts +14 -0
  48. package/dist/patterns.js +15 -0
  49. package/dist/rules.d.ts +408 -0
  50. package/dist/rules.js +655 -0
  51. package/package.json +52 -4
  52. package/wasm/refpact_core.wasm +0 -0
@@ -0,0 +1,775 @@
1
+ /** Loads TypeScript projects through the compiler and turns their imports into facts. */
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { isBuiltin } from "node:module";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { checks } from "./checks.js";
7
+ import { COMPONENT_EXTENSIONS, componentFiles, componentOf } from "./components.js";
8
+ import { openProjects } from "./compiler.js";
9
+ import { DEPENDENCY_KINDS, dependencyFacts, PACKAGE_ITEM_KIND, PACKAGE_PROPERTIES } from "./dependencies.js";
10
+ import { collectDeclarations, ITEM_KINDS, MODIFIERS, RELATIONS, VISIBILITIES } from "./declarations.js";
11
+ import { collectAccesses, collectGlobalUses, collectMarkers, collectMemberUses, collectModuleUses, GLOBAL_KIND, KINDS, MARKERS, MEMBER_KIND } from "./imports.js";
12
+ import { ATTACHED_PREFIXES, exportSegment, globalSegment, HIDDEN_PREFIXES, nodeSegment, packageSegment } from "./paths.js";
13
+ /** One or more analysed TypeScript projects. */
14
+ export class Project {
15
+ /** The directory that file paths are relative to. */
16
+ root;
17
+ /** The tsconfig files that were loaded, also those found through `references`. */
18
+ configFiles;
19
+ /** The facts, for the core. */
20
+ facts;
21
+ /** @internal */
22
+ constructor(root, configFiles, facts) {
23
+ this.root = root;
24
+ this.configFiles = configFiles;
25
+ this.facts = facts;
26
+ }
27
+ /** The paths of all files, relative to the root, such as `src/app/main.ts`. */
28
+ get files() {
29
+ return this.facts.units.map((unit) => unit.join("/"));
30
+ }
31
+ }
32
+ /**
33
+ * Loads the project of a tsconfig file, and the projects it names in `references`. A
34
+ * directory stands for its `tsconfig.json`, or for its `jsconfig.json` when it has none. A
35
+ * directory with neither is read with default options: all its files below it except
36
+ * `node_modules`, JavaScript too, with the resolution of a bundler, which finds `./db` and
37
+ * `./db.js` alike. A JavaScript project is also read through a tsconfig file with `allowJs`.
38
+ *
39
+ * Imports are resolved by the installed TypeScript compiler, with the options of the project
40
+ * that owns the file: `paths`, package `exports` and `imports`, and project references.
41
+ */
42
+ export function loadProject(tsconfig, options = {}) {
43
+ const inputs = typeof tsconfig === "string" ? [tsconfig] : [...tsconfig];
44
+ // The directories of the configs that refpact writes for directories without one.
45
+ const written = [];
46
+ try {
47
+ const given = inputs.map((input) => configPath(input, written));
48
+ const first = given[0];
49
+ if (first === undefined) {
50
+ throw new Error("refpact: loadProject needs a tsconfig file");
51
+ }
52
+ const configFiles = withReferences(given.map(({ file }) => file));
53
+ const root = path.resolve(options.root ?? first.directory);
54
+ const extensions = options.components === true ? [...COMPONENT_EXTENSIONS] : options.components === false ? [] : [...(options.components ?? [])];
55
+ const session = openProjects(root, configFiles, extensions.length > 0 ? componentFiles(extensions) : undefined);
56
+ let project;
57
+ try {
58
+ const own = configFiles.filter((file) => !written.includes(path.dirname(file)));
59
+ const members = options.resolveMembers === true || options.followImplementations === true;
60
+ const facts = analyse(root, session.projects, members, options.followImplementations === true, extensions);
61
+ project = new Project(root, own, options.readDependencies === true ? withDependencies(root, facts) : facts);
62
+ }
63
+ finally {
64
+ session.close();
65
+ }
66
+ if (written.length > 0 && project.facts.units.length === 0) {
67
+ throw new Error(`refpact: ${root} has no tsconfig.json, no jsconfig.json and no JavaScript or TypeScript files.`);
68
+ }
69
+ return project;
70
+ }
71
+ finally {
72
+ for (const directory of written) {
73
+ rmSync(directory, { recursive: true, force: true });
74
+ }
75
+ }
76
+ }
77
+ /**
78
+ * The config file of a path: the file itself, or in a directory its `tsconfig.json`, else
79
+ * its `jsconfig.json`, which a JavaScript project has. For a directory with neither, a
80
+ * config with default options is written to a new directory, which is added to `written`.
81
+ * Without `written`, as for a project reference, such a directory is an error.
82
+ */
83
+ function configPath(file, written) {
84
+ const absolute = path.resolve(file);
85
+ if (!existsSync(absolute)) {
86
+ throw new Error(`refpact: ${absolute} does not exist`);
87
+ }
88
+ if (!statSync(absolute).isDirectory()) {
89
+ return { file: absolute, directory: path.dirname(absolute) };
90
+ }
91
+ const found = ["tsconfig.json", "jsconfig.json"].map((name) => path.join(absolute, name)).find((candidate) => existsSync(candidate));
92
+ if (found !== undefined) {
93
+ return { file: found, directory: absolute };
94
+ }
95
+ if (written === undefined) {
96
+ throw new Error(`refpact: ${absolute} has no tsconfig.json and no jsconfig.json.`);
97
+ }
98
+ const directory = mkdtempSync(path.join(tmpdir(), "refpact-config-"));
99
+ written.push(directory);
100
+ const glob = absolute.split(path.sep).join("/");
101
+ const config = {
102
+ compilerOptions: { allowJs: true, module: "preserve", noEmit: true, types: [] },
103
+ // A wildcard of the compiler leaves out `node_modules`.
104
+ include: [`${glob}/**/*`],
105
+ };
106
+ const configFile = path.join(directory, "jsconfig.json");
107
+ writeFileSync(configFile, JSON.stringify(config));
108
+ return { file: configFile, directory: absolute };
109
+ }
110
+ /** Adds the projects that the given tsconfig files name in `references`, at any depth. */
111
+ function withReferences(configFiles) {
112
+ const all = [];
113
+ const pending = [...configFiles];
114
+ while (pending.length > 0) {
115
+ const file = pending.shift();
116
+ if (all.includes(file)) {
117
+ continue;
118
+ }
119
+ all.push(file);
120
+ const config = parseJsonWithComments(readFileSync(file, "utf8"), file);
121
+ if (Array.isArray(config.references)) {
122
+ for (const reference of config.references) {
123
+ if (typeof reference.path === "string") {
124
+ pending.push(configPath(path.resolve(path.dirname(file), reference.path)).file);
125
+ }
126
+ }
127
+ }
128
+ }
129
+ return all;
130
+ }
131
+ /** Parses JSON that may have comments and trailing commas, as tsconfig files do. */
132
+ export function parseJsonWithComments(text, file) {
133
+ let plain = "";
134
+ let index = 0;
135
+ while (index < text.length) {
136
+ const char = text[index];
137
+ const next = text[index + 1];
138
+ if (char === '"') {
139
+ const start = index;
140
+ index += 1;
141
+ while (index < text.length && text[index] !== '"') {
142
+ index += text[index] === "\\" ? 2 : 1;
143
+ }
144
+ index += 1;
145
+ plain += text.slice(start, index);
146
+ }
147
+ else if (char === "/" && next === "/") {
148
+ while (index < text.length && text[index] !== "\n") {
149
+ index += 1;
150
+ }
151
+ }
152
+ else if (char === "/" && next === "*") {
153
+ const end = text.indexOf("*/", index + 2);
154
+ index = end === -1 ? text.length : end + 2;
155
+ }
156
+ else {
157
+ plain += char;
158
+ index += 1;
159
+ }
160
+ }
161
+ try {
162
+ return JSON.parse(plain.replace(/,(\s*[}\]])/g, "$1"));
163
+ }
164
+ catch (error) {
165
+ throw new Error(`refpact: cannot read ${file}: ${error.message}`);
166
+ }
167
+ }
168
+ /** The facts with the `package.json` files of the project as units, and what they name as references. */
169
+ function withDependencies(root, facts) {
170
+ const files = facts.units.map((unit) => path.resolve(root, ...unit));
171
+ const found = dependencyFacts(root, files);
172
+ // The units stay in the order of their paths, and each keeps its number of lines.
173
+ const lines = facts.unitLines ?? [];
174
+ const units = [...facts.units.map((unit, index) => ({ path: unit, lines: lines[index] ?? 0 })), ...found.units];
175
+ units.sort((a, b) => compare(a.path.join("/"), b.path.join("/")));
176
+ return {
177
+ ...facts,
178
+ units: units.map((unit) => unit.path),
179
+ unitLines: units.map((unit) => unit.lines),
180
+ items: [...facts.items, ...found.declarations.map((declaration) => declaration.path)],
181
+ kinds: [...facts.kinds, ...DEPENDENCY_KINDS],
182
+ facts: [...facts.facts, ...found.facts],
183
+ declarations: [...(facts.declarations ?? []), ...found.declarations],
184
+ itemKinds: [...(facts.itemKinds ?? []), PACKAGE_ITEM_KIND],
185
+ properties: [...PACKAGE_PROPERTIES],
186
+ };
187
+ }
188
+ function analyse(root, projects, members, implementations, components) {
189
+ // A file belongs to the first project that has it. Its imports are resolved there.
190
+ const units = new Map();
191
+ for (const project of projects) {
192
+ for (const file of project.ownFiles) {
193
+ // A component file is a unit under its own name, not under the name that the compiler has for it.
194
+ const component = components.length > 0 && !existsSync(file) ? componentOf(file, components) : undefined;
195
+ const relative = path.relative(root, component?.file ?? file).split(path.sep).join("/");
196
+ if (!units.has(file) && !relative.startsWith("../") && !path.isAbsolute(relative)) {
197
+ units.set(file, { path: relative.split("/"), file, project });
198
+ }
199
+ }
200
+ }
201
+ const ordered = [...units.values()].sort((a, b) => compare(a.path.join("/"), b.path.join("/")));
202
+ const lookup = new UnitLookup(root, ordered);
203
+ const facts = [];
204
+ const items = [];
205
+ const declarations = [];
206
+ const unitLines = [];
207
+ for (const unit of ordered) {
208
+ const sourceFile = unit.project.sourceFile(unit.file);
209
+ if (sourceFile === undefined) {
210
+ unitLines.push(0);
211
+ continue;
212
+ }
213
+ unitLines.push(countLines(sourceFile.text));
214
+ const found = factsOfFile(unit, sourceFile, lookup, members);
215
+ for (const name of found.exportNames) {
216
+ items.push([...unit.path, exportSegment(name)]);
217
+ }
218
+ found.facts.sort((a, b) => a.line - b.line || a.column - b.column);
219
+ facts.push(...found.facts);
220
+ declarations.push(...found.declarations);
221
+ // The client of the native compiler keeps each tree that it gave. A large project has
222
+ // more trees than fit in memory, and no file is read twice.
223
+ unit.project.forgetSourceFiles();
224
+ }
225
+ addAssignable(declarations);
226
+ if (implementations) {
227
+ addImplementations(facts, declarations);
228
+ }
229
+ return {
230
+ separator: "/",
231
+ units: ordered.map((unit) => unit.path),
232
+ unitLines,
233
+ items,
234
+ kinds: members ? [...KINDS, MEMBER_KIND] : [...KINDS],
235
+ markers: [...MARKERS],
236
+ tags: [],
237
+ facts,
238
+ declarations,
239
+ itemKinds: [...ITEM_KINDS],
240
+ visibilities: [...VISIBILITIES],
241
+ relations: [...RELATIONS],
242
+ modifiers: [...MODIFIERS],
243
+ attachedPrefixes: ATTACHED_PREFIXES,
244
+ hiddenPrefixes: HIDDEN_PREFIXES,
245
+ };
246
+ }
247
+ /**
248
+ * Gives each declaration the relation `assignable-to` to each type that it extends or
249
+ * implements, directly or through other declarations of the project: when `Order extends
250
+ * Entity` and `Entity implements Saved`, `Order` is assignable to both.
251
+ */
252
+ function addAssignable(declarations) {
253
+ const byPath = new Map(declarations.map((declaration) => [declaration.path.join("\0"), declaration]));
254
+ const supertypes = (declaration) => declaration.relations.filter((tie) => tie.relation === "extends" || tie.relation === "implements").map((tie) => tie.target);
255
+ for (const [index, declaration] of declarations.entries()) {
256
+ const found = [];
257
+ const seen = new Set([declaration.path.join("\0")]);
258
+ const pending = supertypes(declaration);
259
+ while (pending.length > 0) {
260
+ const target = pending.shift();
261
+ const key = target.join("\0");
262
+ if (seen.has(key)) {
263
+ continue;
264
+ }
265
+ seen.add(key);
266
+ found.push(target);
267
+ const next = byPath.get(key);
268
+ if (next !== undefined) {
269
+ pending.push(...supertypes(next));
270
+ }
271
+ }
272
+ if (found.length > 0) {
273
+ declarations[index] = { ...declaration, relations: [...declaration.relations, ...found.map((target) => ({ relation: "assignable-to", target }))] };
274
+ }
275
+ }
276
+ }
277
+ /**
278
+ * After each use of a member of an interface or of a class: a candidate use of the same
279
+ * member of each class of another file that is assignable to it.
280
+ */
281
+ function addImplementations(facts, declarations) {
282
+ const key = (target) => target.join("\0");
283
+ // The classes that are assignable to a declaration, by its path.
284
+ const below = new Map();
285
+ for (const declaration of declarations) {
286
+ if (declaration.kind !== "class") {
287
+ continue;
288
+ }
289
+ for (const tie of declaration.relations) {
290
+ if (tie.relation === "assignable-to") {
291
+ below.set(key(tie.target), [...(below.get(key(tie.target)) ?? []), declaration]);
292
+ }
293
+ }
294
+ }
295
+ if (below.size === 0) {
296
+ return;
297
+ }
298
+ const all = facts.splice(0);
299
+ for (const fact of all) {
300
+ facts.push(fact);
301
+ if (fact.what.type !== "reference" || fact.what.kind !== MEMBER_KIND) {
302
+ continue;
303
+ }
304
+ const target = fact.what.target;
305
+ const member = target[target.length - 1];
306
+ for (const implementer of below.get(key(target.slice(0, -1))) ?? []) {
307
+ if (key(implementer.unit) === key(fact.unit)) {
308
+ continue;
309
+ }
310
+ facts.push({
311
+ ...fact,
312
+ note: `may be called through ${[...target.slice(-2)].map((segment) => segment.replace(/^#/, "")).join(".")}`,
313
+ what: { type: "reference", target: [...implementer.path, member], aliases: [], kind: MEMBER_KIND, certainty: "candidate" },
314
+ });
315
+ }
316
+ }
317
+ }
318
+ /** The number of lines of a text. A line break at the end of the text starts no line. */
319
+ function countLines(text) {
320
+ if (text === "") {
321
+ return 0;
322
+ }
323
+ let lines = 1;
324
+ for (let index = text.indexOf("\n"); index !== -1 && index < text.length - 1; index = text.indexOf("\n", index + 1)) {
325
+ lines += 1;
326
+ }
327
+ return lines;
328
+ }
329
+ function compare(a, b) {
330
+ return a < b ? -1 : a > b ? 1 : 0;
331
+ }
332
+ class UnitLookup {
333
+ root;
334
+ byFile = new Map();
335
+ constructor(root, units) {
336
+ this.root = root;
337
+ for (const unit of units) {
338
+ this.byFile.set(unit.file, unit);
339
+ // The compiler names files by a canonical path, which is lower case on file systems
340
+ // that ignore case.
341
+ this.byFile.set(unit.file.toLowerCase(), unit);
342
+ }
343
+ }
344
+ unit(file) {
345
+ return file === undefined ? undefined : (this.byFile.get(file) ?? this.byFile.get(file.toLowerCase()));
346
+ }
347
+ /** The units whose file, as an absolute name with `/`, the test accepts, in the order of their paths. */
348
+ matching(test) {
349
+ const found = new Set();
350
+ for (const unit of this.byFile.values()) {
351
+ if (test.test(unit.file.split(path.sep).join("/"))) {
352
+ found.add(unit);
353
+ }
354
+ }
355
+ return [...found].sort((a, b) => compare(a.path.join("/"), b.path.join("/")));
356
+ }
357
+ }
358
+ /**
359
+ * The facts of one file, all names that the file exports, also through `export *`, and the
360
+ * items that it declares.
361
+ */
362
+ function factsOfFile(unit, sourceFile, lookup, resolveMembers) {
363
+ const project = unit.project;
364
+ // A TypeScript file is read without a visit of the nodes that cannot name a module.
365
+ const prune = /\.[cm]?tsx?$/.test(unit.file);
366
+ const uses = collectModuleUses(sourceFile, prune);
367
+ const bindings = uses.flatMap((use) => (use.namespace !== undefined ? [{ use, binding: use.namespace }] : []));
368
+ const members = collectMemberUses(sourceFile, new Set(bindings.map(({ binding }) => binding.local)), prune);
369
+ // One round trip to the compiler for every node of this file that needs a symbol.
370
+ const nodes = [];
371
+ const ask = (node) => nodes.push(node) - 1;
372
+ const own = ask(sourceFile);
373
+ const asked = uses.map((use) => ({
374
+ specifier: ask(use.specifierNode),
375
+ require: use.requireNode !== undefined ? ask(use.requireNode) : undefined,
376
+ names: use.names.map((name) => ask(name.node)),
377
+ namespace: use.namespace !== undefined ? ask(use.namespace.node) : undefined,
378
+ constants: (use.constants ?? []).map((constant) => ({ use: ask(constant.use), declaration: ask(constant.declaration) })),
379
+ }));
380
+ const askedMembers = members.map((member) => ({
381
+ object: ask(member.object),
382
+ member: ask(member.member),
383
+ alias: member.alias !== undefined ? { declaration: ask(member.alias.declaration), of: ask(member.alias.of) } : undefined,
384
+ }));
385
+ const accesses = resolveMembers ? collectAccesses(sourceFile) : [];
386
+ const askedAccesses = accesses.map(ask);
387
+ const globals = collectGlobalUses(sourceFile);
388
+ const askedGlobals = globals.map((use) => ({ name: use.shorthand !== undefined ? undefined : ask(use.node), through: use.through !== undefined ? ask(use.through) : undefined }));
389
+ const markers = collectMarkers(sourceFile, prune);
390
+ const askedMarkers = markers.map((use) => (use.object !== undefined ? ask(use.object) : undefined));
391
+ const declared = collectDeclarations(sourceFile);
392
+ const askedDeclared = declared.map((item) => ({ at: ask(item.at), ties: item.ties.map((tie) => ask(tie.node)) }));
393
+ const symbols = project.symbolsAt(nodes);
394
+ const symbol = (index) => (index === undefined ? undefined : symbols[index]);
395
+ const facts = [];
396
+ const add = (node, kind, target, aliases, note, certainty = "exact") => {
397
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
398
+ facts.push({
399
+ unit: unit.path,
400
+ file: unit.path.join("/"),
401
+ line: position.line + 1,
402
+ column: position.character + 1,
403
+ tags: [],
404
+ ...(note !== undefined ? { note } : {}),
405
+ what: {
406
+ type: "reference",
407
+ target,
408
+ aliases: aliases.filter((alias, index) => !samePath(alias, target) && aliases.findIndex((other) => samePath(other, alias)) === index),
409
+ kind,
410
+ certainty,
411
+ },
412
+ });
413
+ };
414
+ const mark = (node, marker, note) => {
415
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
416
+ facts.push({
417
+ unit: unit.path,
418
+ file: unit.path.join("/"),
419
+ line: position.line + 1,
420
+ column: position.character + 1,
421
+ tags: [],
422
+ ...(note !== undefined ? { note } : {}),
423
+ what: { type: "marker", marker },
424
+ });
425
+ };
426
+ // A global of the host: declared, but in no file of the project and not in the ECMAScript library.
427
+ globals.forEach((use, index) => {
428
+ const indexes = askedGlobals[index];
429
+ if (indexes.through !== undefined && isDeclaredInProject(symbol(indexes.through), project, lookup)) {
430
+ return;
431
+ }
432
+ const found = use.shorthand !== undefined ? project.shorthandValueAt(use.shorthand) : symbol(indexes.name);
433
+ const files = found === undefined ? [] : project.filesOf(found);
434
+ if (files.length === 0 || files.some((file) => lookup.unit(file) !== undefined || isLanguageLibrary(file))) {
435
+ return;
436
+ }
437
+ const target = [globalSegment(use.name), ...(use.member !== undefined ? [exportSegment(use.member)] : [])];
438
+ add(use.at, GLOBAL_KIND, target, [], undefined);
439
+ });
440
+ // `console` is a marker when the compiler finds no declaration of it in the project.
441
+ markers.forEach((use, index) => {
442
+ const asked = askedMarkers[index];
443
+ if (asked !== undefined && isDeclaredInProject(symbol(asked), project, lookup)) {
444
+ return;
445
+ }
446
+ const position = sourceFile.getLineAndCharacterOfPosition(use.at.getStart(sourceFile));
447
+ facts.push({
448
+ unit: unit.path,
449
+ file: unit.path.join("/"),
450
+ line: position.line + 1,
451
+ column: position.character + 1,
452
+ tags: [],
453
+ what: { type: "marker", marker: use.marker },
454
+ });
455
+ });
456
+ const resolved = new Map();
457
+ // The paths of each imported name, by its symbol, for the relations of the declarations.
458
+ const imported = [];
459
+ uses.forEach((use, index) => {
460
+ const indexes = asked[index];
461
+ // A `require` that is declared in the project is a local function, not Node's.
462
+ if (use.requireNode !== undefined && isDeclaredInProject(symbol(indexes.require), project, lookup)) {
463
+ return;
464
+ }
465
+ // A constant has the value that was read only when its name is the variable that was read.
466
+ const proved = indexes.constants.every((constant) => {
467
+ const [used, declared] = [symbol(constant.use), symbol(constant.declaration)];
468
+ return used !== undefined && declared !== undefined && project.same(used, declared);
469
+ });
470
+ const computed = proved ? use.computed : "unknown";
471
+ if (computed === "unknown" || computed === "pattern") {
472
+ // The code computes the specifier. What is known of it names the files that it can be.
473
+ mark(use.at, "computed-import", computed === "pattern" ? use.specifier : undefined);
474
+ if (computed === "pattern") {
475
+ const note = `the specifier is computed: ${use.specifier}`;
476
+ for (const target of patternTargets(use.specifier, unit, lookup, false)) {
477
+ add(use.at, use.kind, target, [], note, "candidate");
478
+ }
479
+ }
480
+ return;
481
+ }
482
+ if (computed === "glob") {
483
+ for (const target of patternTargets(use.specifier, unit, lookup, true)) {
484
+ add(use.at, use.kind, target, [], `matches ${use.specifier}`);
485
+ }
486
+ return;
487
+ }
488
+ // The compiler knows the module of a specifier that is one string in the place.
489
+ const known = computed === undefined ? declaredFile(symbol(indexes.specifier), project) : undefined;
490
+ const module = resolveModule(use.specifier, unit, known, lookup, use.asset === true);
491
+ resolved.set(use, module);
492
+ // The compiler resolves a specifier that is one string. For another one, and for the file
493
+ // of `new URL(..)`, which need not be a module, nothing says that it leads nowhere.
494
+ if (module.note === NOT_RESOLVED && computed === undefined && use.asset !== true) {
495
+ mark(use.specifierNode, "unresolved-import", use.specifier);
496
+ }
497
+ if (use.whole) {
498
+ const note = computed === "constant" && module.note === NOT_RESOLVED ? undefined : module.note;
499
+ add(use.at, use.kind, module.base, module.fileBase !== undefined ? [module.fileBase] : [], note);
500
+ }
501
+ use.names.forEach((name, nameIndex) => {
502
+ const target = [...module.base, exportSegment(name.exported)];
503
+ const aliases = module.fileBase !== undefined ? [[...module.fileBase, exportSegment(name.exported)]] : [];
504
+ const from = name.specifier === true ? { module: symbol(indexes.specifier), name: name.exported } : undefined;
505
+ const nameSymbol = symbol(indexes.names[nameIndex]);
506
+ aliases.push(...declarations(nameSymbol, project, lookup, false, from));
507
+ add(name.node, name.kind, target, aliases, module.note);
508
+ if (nameSymbol !== undefined) {
509
+ imported.push({ symbol: nameSymbol, paths: [target, ...aliases] });
510
+ }
511
+ });
512
+ });
513
+ // `object.name`, where the name is a member of a class, an interface or an enum that
514
+ // another file of the project declares.
515
+ accesses.forEach((name, index) => {
516
+ const member = symbol(askedAccesses[index]);
517
+ const owner = member === undefined ? undefined : project.ownerOf(member);
518
+ const declaredIn = owner === undefined ? undefined : lookup.unit(declaredFile(owner, project));
519
+ if (member === undefined || owner === undefined || declaredIn === undefined || declaredIn === unit) {
520
+ return;
521
+ }
522
+ add(name, MEMBER_KIND, [...declaredIn.path, exportSegment(owner.name), exportSegment(member.name)], [], undefined);
523
+ });
524
+ // `ns.member`, where `ns` is bound to a module by `import * as ns` or by `require`.
525
+ members.forEach((member, index) => {
526
+ const indexes = askedMembers[index];
527
+ let objectSymbol = symbol(indexes.object);
528
+ if (indexes.alias !== undefined) {
529
+ // The object is a `const` whose value is the binding: it must be that `const`.
530
+ const declared = symbol(indexes.alias.declaration);
531
+ const same = objectSymbol !== undefined && declared !== undefined && project.same(objectSymbol, declared);
532
+ objectSymbol = same ? symbol(indexes.alias.of) : undefined;
533
+ }
534
+ const owner = bindings.find(({ use, binding }) => {
535
+ const bindingSymbol = symbol(asked[uses.indexOf(use)]?.namespace);
536
+ return binding.local === member.objectName && objectSymbol !== undefined && bindingSymbol !== undefined && project.same(bindingSymbol, objectSymbol);
537
+ });
538
+ const module = owner !== undefined ? resolved.get(owner.use) : undefined;
539
+ if (owner === undefined || module === undefined) {
540
+ return;
541
+ }
542
+ const target = [...module.base, exportSegment(member.memberName)];
543
+ const aliases = module.fileBase !== undefined ? [[...module.fileBase, exportSegment(member.memberName)]] : [];
544
+ const memberSymbol = symbol(indexes.member);
545
+ aliases.push(...declarations(memberSymbol, project, lookup, memberSymbol !== undefined));
546
+ add(member.member, owner.binding.kind, target, aliases, module.note);
547
+ });
548
+ const module = symbol(own);
549
+ const exports = module === undefined ? new Map() : project.exportsOf(module);
550
+ // What the exports of the file stand for, asked only when a declaration has no `export`.
551
+ let exported;
552
+ const isExported = (own) => {
553
+ if (own === undefined || exports.size === 0) {
554
+ return false;
555
+ }
556
+ exported ??= [...exports.values()].flatMap((entry) => [entry, project.immediateAlias(entry) ?? entry]);
557
+ return exported.some((entry) => project.same(entry, own));
558
+ };
559
+ const items = declared.map((item, index) => {
560
+ const indexes = askedDeclared[index];
561
+ const relations = [];
562
+ item.ties.forEach((tie, tieIndex) => {
563
+ const tied = symbol(indexes.ties[tieIndex]);
564
+ // A type parameter is no type of its own: `T` in `first<T>(items: T[]): T`.
565
+ if (tied !== undefined && project.isTypeParameter(tied)) {
566
+ return;
567
+ }
568
+ const entry = tied === undefined ? undefined : imported.find((name) => project.same(name.symbol, tied));
569
+ const paths = entry !== undefined ? entry.paths : declarations(tied, project, lookup, true);
570
+ for (const target of paths) {
571
+ if (!relations.some((known) => known.relation === tie.relation && samePath(known.target, target))) {
572
+ relations.push({ relation: tie.relation, target });
573
+ }
574
+ }
575
+ });
576
+ const position = sourceFile.getLineAndCharacterOfPosition(item.at.getStart(sourceFile));
577
+ return {
578
+ path: [...unit.path, exportSegment(item.name)],
579
+ name: item.name,
580
+ unit: unit.path,
581
+ file: unit.path.join("/"),
582
+ line: position.line + 1,
583
+ column: position.character + 1,
584
+ endLine: sourceFile.getLineAndCharacterOfPosition(item.whole.end).line + 1,
585
+ tags: [],
586
+ kind: item.kind,
587
+ visibility: item.exported || isExported(symbol(indexes.at)) ? "exported" : "local",
588
+ relations,
589
+ modifiers: [...item.modifiers],
590
+ };
591
+ });
592
+ return { facts, exportNames: [...exports.keys()], declarations: items };
593
+ }
594
+ function samePath(a, b) {
595
+ return a.length === b.length && a.every((segment, index) => segment === b[index]);
596
+ }
597
+ function declaredFile(symbol, project) {
598
+ return symbol === undefined ? undefined : project.filesOf(symbol)[0];
599
+ }
600
+ /** Whether a file is a part of the ECMAScript library of the compiler: `lib.es5.d.ts`, `lib.es2015.promise.d.ts`. */
601
+ function isLanguageLibrary(file) {
602
+ return /^lib\.(es\d|esnext|decorators)/.test(path.basename(file));
603
+ }
604
+ function isDeclaredInProject(symbol, project, lookup) {
605
+ return symbol !== undefined && project.filesOf(symbol).some((file) => lookup.unit(file) !== undefined);
606
+ }
607
+ /**
608
+ * The places that a name leads to through re-exports, nearest first. Each place is a file
609
+ * of the project and the name it has there.
610
+ */
611
+ function declarations(symbol, project, lookup, includeFirst = false, from) {
612
+ const places = [];
613
+ let current = symbol;
614
+ for (let hop = 0; hop < 16 && current !== undefined; hop += 1) {
615
+ if (hop > 0 || includeFirst) {
616
+ const unit = lookup.unit(declaredFile(current, project));
617
+ if (unit !== undefined) {
618
+ places.push([...unit.path, exportSegment(current.name)]);
619
+ }
620
+ }
621
+ current = hop === 0 && from !== undefined ? firstStep(current, from, project) : project.immediateAlias(current);
622
+ }
623
+ return places;
624
+ }
625
+ /**
626
+ * One step from the name of an import or a re-export to the export that it stands for.
627
+ *
628
+ * The compiler can answer this for each name, but with the native compiler each answer is a
629
+ * round trip to another process, and a project has many more imported names than modules.
630
+ * So the answer is read from the exports of the module, which one round trip gives for all
631
+ * names. The compiler finds the export in the same table. The name `default` is the
632
+ * exception and is left to the compiler: it can stand for the module as a whole.
633
+ */
634
+ function firstStep(symbol, from, project) {
635
+ const found = from.module === undefined || from.name === "default" ? undefined : project.exportsOf(from.module).get(from.name);
636
+ if (found === undefined) {
637
+ return project.immediateAlias(symbol);
638
+ }
639
+ if (checks.aliases) {
640
+ const asked = project.immediateAlias(symbol);
641
+ if (asked === undefined || !project.same(asked, found)) {
642
+ throw new Error(`refpact: the export \`${from.name}\` of a module is not what the compiler gives for an import of it`);
643
+ }
644
+ }
645
+ return found;
646
+ }
647
+ /** The note of an import that leads to no file. */
648
+ const NOT_RESOLVED = "not resolved";
649
+ function isRelative(specifier) {
650
+ return specifier.startsWith("./") || specifier.startsWith("../") || specifier === "." || specifier === "..";
651
+ }
652
+ /**
653
+ * The paths that a specifier with parts that are not known can be: the files of the project
654
+ * that it matches, or the package that it names. With `glob`, the pattern is a glob of
655
+ * `import.meta.glob`: `*` stays in one directory and `**` does not. Otherwise each `*` is a
656
+ * part that the code computes, of any text.
657
+ */
658
+ function patternTargets(pattern, from, lookup, glob) {
659
+ if (isRelative(pattern) || path.isAbsolute(pattern)) {
660
+ const start = path.resolve(path.dirname(from.file), pattern).split(path.sep).join("/");
661
+ const literal = (text) => text.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
662
+ const body = glob
663
+ ? start
664
+ .split(/(\*\*\/|\*\*|\*)/)
665
+ .map((part) => (part === "**/" ? "(.*/)?" : part === "**" ? ".*" : part === "*" ? "[^/]*" : literal(part)))
666
+ .join("")
667
+ : start.split("*").map(literal).join(".*");
668
+ // A computed specifier can leave out the extension, or name a directory with an index file.
669
+ const endings = glob ? "" : `(/index)?(${EXTENSIONS.map(literal).join("|")})?`;
670
+ return lookup
671
+ .matching(new RegExp(`^${body}${endings}$`))
672
+ .filter((unit) => unit !== from)
673
+ .map((unit) => unit.path);
674
+ }
675
+ // Of a package, the name must be known: `pkg/${name}`, not `${name}/sub`.
676
+ const parts = pattern.split("/");
677
+ const nameLength = pattern.startsWith("@") ? 2 : 1;
678
+ const name = parts.slice(0, nameLength).join("/");
679
+ const baseUrl = from.project.options["baseUrl"];
680
+ const aliased = pattern.startsWith("#") ||
681
+ matchesPaths(name, from.project.options["paths"]) ||
682
+ matchesPaths(pattern, from.project.options["paths"]) ||
683
+ // With `baseUrl`, the name of a directory below it is no package.
684
+ (typeof baseUrl === "string" && existsSync(path.resolve(baseUrl, name)));
685
+ if (glob || aliased || parts.length <= nameLength || name.includes("*") || name === "") {
686
+ return [];
687
+ }
688
+ return [isBuiltin(name) ? [nodeSegment(name.replace(/^node:/, ""))] : [packageSegment(name)]];
689
+ }
690
+ function resolveModule(specifier, from, resolvedFile, lookup, asset = false) {
691
+ let target = lookup.unit(resolvedFile);
692
+ const relative = isRelative(specifier);
693
+ if (relative || path.isAbsolute(specifier)) {
694
+ if (target !== undefined) {
695
+ return { base: target.path };
696
+ }
697
+ // The compiler does not resolve `require("./x")` in a TypeScript file. The file system
698
+ // answers for relative paths.
699
+ const start = path.resolve(path.dirname(from.file), specifier);
700
+ // The file of `new URL(..)` is named in full.
701
+ target = lookup.unit(asset ? start : probe(start)) ?? lookup.unit(probe(start));
702
+ if (target !== undefined) {
703
+ return { base: target.path };
704
+ }
705
+ const outside = path.relative(lookup.root, resolvedFile ?? start).split(path.sep);
706
+ // A file of `new URL(..)` that is there is found, also when it is no module.
707
+ const found = resolvedFile !== undefined || (asset && existsSync(start));
708
+ return found ? { base: outside } : { base: outside, note: NOT_RESOLVED };
709
+ }
710
+ if (isBuiltin(specifier)) {
711
+ const [name, ...rest] = specifier.replace(/^node:/, "").split("/");
712
+ return { base: [nodeSegment(name), ...rest] };
713
+ }
714
+ // `#name` is an entry of `imports` in package.json; a `paths` pattern is an alias of the
715
+ // project; a name that TypeScript 6 finds below `baseUrl` is a path. All lead to files,
716
+ // not packages.
717
+ if (specifier.startsWith("#") || matchesPaths(specifier, from.project.options["paths"]) || isBelowBaseUrl(specifier, resolvedFile, from)) {
718
+ if (target !== undefined) {
719
+ return { base: target.path };
720
+ }
721
+ return resolvedFile !== undefined
722
+ ? { base: path.relative(lookup.root, resolvedFile).split(path.sep) }
723
+ : { base: specifier.split("/"), note: NOT_RESOLVED };
724
+ }
725
+ const parts = specifier.split("/");
726
+ const nameLength = specifier.startsWith("@") ? 2 : 1;
727
+ const base = [packageSegment(parts.slice(0, nameLength).join("/")), ...parts.slice(nameLength)];
728
+ // A package of the same workspace can resolve to a source file of the project.
729
+ return target !== undefined ? { base, fileBase: target.path } : { base };
730
+ }
731
+ /** True when the compiler found the specifier as a path from the `baseUrl` of the project. */
732
+ function isBelowBaseUrl(specifier, resolvedFile, from) {
733
+ const baseUrl = from.project.options["baseUrl"];
734
+ if (typeof baseUrl !== "string" || resolvedFile === undefined) {
735
+ return false;
736
+ }
737
+ const named = path.resolve(baseUrl, specifier);
738
+ return resolvedFile === named || resolvedFile.startsWith(`${named}.`) || resolvedFile.startsWith(named + path.sep);
739
+ }
740
+ const EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".d.ts", ".js", ".jsx", ".mjs", ".cjs", ".json"];
741
+ const SOURCE_OF_OUTPUT = {
742
+ ".js": [".ts", ".tsx"],
743
+ ".jsx": [".tsx"],
744
+ ".mjs": [".mts"],
745
+ ".cjs": [".cts"],
746
+ };
747
+ /** Finds the file of a relative specifier the way TypeScript names files. */
748
+ function probe(start) {
749
+ const candidates = [start];
750
+ const extension = path.extname(start);
751
+ for (const source of SOURCE_OF_OUTPUT[extension] ?? []) {
752
+ candidates.push(start.slice(0, -extension.length) + source);
753
+ }
754
+ for (const added of EXTENSIONS) {
755
+ candidates.push(start + added);
756
+ }
757
+ for (const added of EXTENSIONS) {
758
+ candidates.push(path.join(start, `index${added}`));
759
+ }
760
+ return candidates.find((candidate) => existsSync(candidate) && statSync(candidate).isFile());
761
+ }
762
+ function matchesPaths(specifier, paths) {
763
+ if (typeof paths !== "object" || paths === null) {
764
+ return false;
765
+ }
766
+ return Object.keys(paths).some((pattern) => {
767
+ const star = pattern.indexOf("*");
768
+ if (star === -1) {
769
+ return pattern === specifier;
770
+ }
771
+ const prefix = pattern.slice(0, star);
772
+ const suffix = pattern.slice(star + 1);
773
+ return specifier.length >= prefix.length + suffix.length && specifier.startsWith(prefix) && specifier.endsWith(suffix);
774
+ });
775
+ }