@cplieger/deadset-ts 8.0.0-dev.14 → 8.0.0-dev.15

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 (46) hide show
  1. package/dist/src/alias-chain.js +2 -2
  2. package/dist/src/analysis.js +2 -1
  3. package/dist/src/conformance-record.js +1 -1
  4. package/dist/src/consumers.js +1 -1
  5. package/dist/src/contract/contract.generated.js +1 -0
  6. package/dist/src/dependencies.js +2 -1
  7. package/dist/src/entry-points.js +26 -95
  8. package/dist/src/file-facts.js +1 -1
  9. package/dist/src/graph.js +0 -1
  10. package/dist/src/inventory/commonjs.js +462 -0
  11. package/dist/src/inventory/enumerate.js +166 -0
  12. package/dist/src/inventory/syntax.js +236 -0
  13. package/dist/src/inventory.js +305 -705
  14. package/dist/src/load/discover.js +1 -1
  15. package/dist/src/load/package-scope.js +56 -0
  16. package/dist/src/own-body.js +28 -0
  17. package/dist/src/position.js +41 -0
  18. package/dist/src/ref.js +11 -20
  19. package/dist/src/references.js +91 -30
  20. package/dist/src/session.js +3 -0
  21. package/dist/src/suppress-inline.js +33 -81
  22. package/dist/src/unseen-loads.js +106 -17
  23. package/jsr.json +1 -1
  24. package/package.json +2 -2
  25. package/src/alias-chain.ts +2 -2
  26. package/src/analysis.ts +2 -1
  27. package/src/conformance-record.ts +1 -1
  28. package/src/consumers.ts +1 -1
  29. package/src/contract/contract.generated.ts +1 -0
  30. package/src/dependencies.ts +2 -1
  31. package/src/entry-points.ts +30 -107
  32. package/src/file-facts.ts +1 -1
  33. package/src/graph.ts +0 -1
  34. package/src/inventory/commonjs.ts +611 -0
  35. package/src/inventory/enumerate.ts +202 -0
  36. package/src/inventory/syntax.ts +311 -0
  37. package/src/inventory.ts +424 -878
  38. package/src/load/discover.ts +1 -1
  39. package/src/load/package-scope.ts +74 -0
  40. package/src/own-body.ts +42 -0
  41. package/src/position.ts +42 -0
  42. package/src/ref.ts +11 -20
  43. package/src/references.ts +116 -53
  44. package/src/session.ts +17 -1
  45. package/src/suppress-inline.ts +53 -89
  46. package/src/unseen-loads.ts +166 -18
@@ -1,6 +1,6 @@
1
1
  import { LANGUAGE_ROWS } from "../contract/contract.generated.js";
2
2
  import { filesBelow, filesIn } from "../fs/tree.js";
3
- import { packageScope } from "../inventory.js";
3
+ import { packageScope } from "./package-scope.js";
4
4
  import { dirnamePath, joinPath, relativePath, resolvePath } from "../paths.js";
5
5
  import { LANGUAGE, message } from "../product.js";
6
6
  import { globExpression } from "../text/glob.js";
@@ -0,0 +1,56 @@
1
+ import { manifestAt } from "../fs/manifest.js";
2
+ import { dirnamePath, joinPath } from "../paths.js";
3
+ /**
4
+ * Resolves each file's package scope: the name of the nearest manifest at or above its
5
+ * directory up to the target root, and the file's path below that manifest. Where no
6
+ * manifest up to the root carries a name, the package is a single dot and the path is read
7
+ * from the root, which cannot collide, because a package name never starts with a dot. One
8
+ * directory is read at most once.
9
+ */
10
+ export function packageScope(host, root) {
11
+ const named = new Map();
12
+ const manifestName = (dir) => {
13
+ const name = manifestAt(host, joinPath(root, dir))?.["name"];
14
+ return typeof name === "string" && name !== "" ? name : undefined;
15
+ };
16
+ const nearest = (dir) => {
17
+ if (named.has(dir)) {
18
+ return named.get(dir);
19
+ }
20
+ const name = manifestName(dir);
21
+ const found = name !== undefined ? { name, at: dir } : dir === "" ? undefined : nearest(parentOf(dir));
22
+ named.set(dir, found);
23
+ return found;
24
+ };
25
+ return (path) => {
26
+ const found = nearest(parentOf(path));
27
+ if (found === undefined) {
28
+ return { package: ".", path };
29
+ }
30
+ return { package: found.name, path: found.at === "" ? path : path.slice(found.at.length + 1) };
31
+ };
32
+ }
33
+ /**
34
+ * Whether Node reads each file's `.js` siblings as ECMAScript modules: the `type` of the
35
+ * nearest manifest at or above its directory up to the target root is `module`
36
+ * (https://nodejs.org/api/packages.html#type). One directory is read at most once.
37
+ */
38
+ export function packageTypeIsModule(host, root) {
39
+ const typed = new Map();
40
+ const nearest = (dir) => {
41
+ const known = typed.get(dir);
42
+ if (known !== undefined) {
43
+ return known;
44
+ }
45
+ const manifest = manifestAt(host, joinPath(root, dir));
46
+ const found = manifest !== undefined ? manifest["type"] === "module" : dir !== "" && nearest(parentOf(dir));
47
+ typed.set(dir, found);
48
+ return found;
49
+ };
50
+ return (path) => nearest(parentOf(path));
51
+ }
52
+ /** The directory one path below the root sits in, and the root itself as the empty path. */
53
+ function parentOf(path) {
54
+ const dir = dirnamePath(path);
55
+ return dir === "." ? "" : dir;
56
+ }
@@ -0,0 +1,28 @@
1
+ import { isArrowFunction, isClassDeclaration, isClassExpression, isFunctionDeclaration, isFunctionExpression, isVariableDeclaration, } from "@typescript/native/ast";
2
+ import { SymbolFlags } from "@typescript/native/sync";
3
+ import { isAmbient } from "./inventory.js";
4
+ import { unwrapped } from "./unwrap.js";
5
+ /**
6
+ * Whether `symbol`, through its aliases, names a function or class the target writes with a
7
+ * body, which a call or construction runs in place of the platform's `require` or `URL`. Any
8
+ * other answer, an unresolved one included, is the platform's, whose call loads a file.
9
+ */
10
+ export function hasOwnBody(project, symbol) {
11
+ const named = (symbol.flags & SymbolFlags.Alias) === 0 ? symbol : project.queries.aliased(symbol);
12
+ const own = project.ownPaths();
13
+ return (typeof named === "object" &&
14
+ named.declarations.some((handle) => {
15
+ const node = own.has(handle.path) ? project.declarationAt(handle)?.node : undefined;
16
+ const value = node !== undefined && isVariableDeclaration(node) && node.initializer !== undefined
17
+ ? unwrapped(node.initializer)
18
+ : node;
19
+ return (value !== undefined &&
20
+ (isFunctionDeclaration(value)
21
+ ? value.body !== undefined
22
+ : isFunctionExpression(value) ||
23
+ isArrowFunction(value) ||
24
+ isClassDeclaration(value) ||
25
+ isClassExpression(value)) &&
26
+ !isAmbient(value.getSourceFile(), value));
27
+ }));
28
+ }
@@ -25,6 +25,47 @@ export function isComponentFile(file) {
25
25
  export function writtenLength(file) {
26
26
  return isComponentFile(file) ? file.originalText.length : file.text.length;
27
27
  }
28
+ /** Whether one character ends a line, under the language's line terminators. */
29
+ function endsLine(char) {
30
+ return char === "\n" || char === "\r" || char === "\u2028" || char === "\u2029";
31
+ }
32
+ /** The offset where the line holding `from` ends. */
33
+ export function lineEnd(text, from) {
34
+ let at = from;
35
+ while (at < text.length && !endsLine(text[at])) {
36
+ at += 1;
37
+ }
38
+ return at;
39
+ }
40
+ /**
41
+ * The offset of the first token at or after `from`: whitespace, comments and a leading
42
+ * hashbang are trivia.
43
+ */
44
+ export function tokenStart(text, from) {
45
+ let at = from;
46
+ if (at === 0 && text.startsWith("#!")) {
47
+ at = lineEnd(text, 0);
48
+ }
49
+ for (;;) {
50
+ if (/\s/u.test(text[at] ?? "")) {
51
+ at += 1;
52
+ }
53
+ else if (text.startsWith("//", at)) {
54
+ at = lineEnd(text, at);
55
+ }
56
+ else if (text.startsWith("/*", at)) {
57
+ const close = text.indexOf("*/", at + 2);
58
+ at = close < 0 ? text.length : close + 2;
59
+ }
60
+ else {
61
+ return at;
62
+ }
63
+ }
64
+ }
65
+ /** The line, counted from one, of the first token at or after one offset of one file. */
66
+ export function startLine(file, offset) {
67
+ return file.getLineAndCharacterOfPosition(tokenStart(file.text, offset)).line + 1;
68
+ }
28
69
  /**
29
70
  * The position of one offset of one file, rendered against the target root.
30
71
  *
package/dist/src/ref.js CHANGED
@@ -1,13 +1,9 @@
1
1
  /**
2
- * The stable symbol reference: the identifier that names one declaration across
3
- * runs, across reports, in an ignore entry, in a configured root and in a
4
- * cross-language edge. It is built from names and containers only, so an edit above
5
- * a declaration leaves it unchanged, and it is compared as bytes, so there is one
6
- * canonical spelling per declaration and this module is where that spelling is
7
- * written.
8
- *
9
- * The position key of {@link ./position.ts} is the other identifier and has the
10
- * other job: it locates a finding inside one run, and the two never mix.
2
+ * The stable symbol reference: the identifier that names one declaration across runs,
3
+ * reports, ignore entries, configured roots and cross-language edges. It is built from
4
+ * names and containers only, so an edit above a declaration leaves it unchanged, and it
5
+ * is compared as bytes, so this module writes the one canonical spelling. The position
6
+ * key of {@link ./position.ts} locates a finding inside one run; the two never mix.
11
7
  */
12
8
  /** The language prefix, which namespaces every reference this analyzer emits. */
13
9
  const PREFIX = "ts://";
@@ -30,16 +26,10 @@ const TS_FILE = String.raw `${TS_DIRS}[^/#\r\n]+(?:(?:\.d)?\.[mc]?ts|\.tsx|\.[mc
30
26
  const SCOPE = `${PREFIX}${TS_PACKAGE}/${TS_FILE}`;
31
27
  const MANIFEST_SCOPE = `${PREFIX}${TS_PACKAGE}/${TS_DIRS}package\\.json`;
32
28
  /**
33
- * One expression per form the grammar defines, each anchored and each the whole of
34
- * a reference. Several forms share one expression because they differ in what the
35
- * symbol is rather than in how the reference is spelled, so the list is the set of
36
- * spellings and not the set of subjects. A reference is valid when it matches one.
37
- *
38
- * They are written here rather than read from the Contract at run time: the Contract
39
- * is data a reader outside this program consults, and a product that read it while
40
- * running would need it installed beside itself. A test compares this list against
41
- * the release this analyzer is written against, so a grammar that grows a form
42
- * arrives here as a failing test.
29
+ * One anchored expression per spelling the grammar defines; a reference is valid when it
30
+ * matches one. They are written here rather than read from the Contract at run time, so
31
+ * the product needs no Contract installed beside it; a test compares this list against
32
+ * the release this analyzer implements, so a grammar that grows a form fails it.
43
33
  */
44
34
  export const REF_EXPRESSIONS = [
45
35
  `^${SCOPE}#$`,
@@ -102,7 +92,8 @@ export function aliasFragment(name, block) {
102
92
  const head = block === undefined ? "" : `${quoted(block)}.`;
103
93
  return `${head}${nameComponent(name)}:alias`;
104
94
  }
105
- function spell(component) {
95
+ /** The canonical spelling of one component: a name, or a computed key. */
96
+ export function spell(component) {
106
97
  return component.computed ? computedComponent(component.text) : nameComponent(component.text);
107
98
  }
108
99
  /**
@@ -1,11 +1,11 @@
1
- import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExpressionStatement, isExternalModuleReference, isForInStatement, isForOfStatement, isForStatement, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isInterfaceDeclaration, isMetaProperty, isModuleBlock, isModuleDeclaration, isNewExpression, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVoidExpression, SyntaxKind, } from "@typescript/native/ast";
1
+ import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExpressionStatement, isExternalModuleReference, isForInStatement, isForOfStatement, isForStatement, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isInterfaceDeclaration, isMetaProperty, isModuleBlock, isModuleDeclaration, isNewExpression, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isStringLiteralLikeNode, isVoidExpression, SyntaxKind, } from "@typescript/native/ast";
2
2
  import { parenthesesRemoved } from "./unwrap.js";
3
3
  import { aliasChains } from "./alias-chain.js";
4
4
  import { contextualUses } from "./contextual-uses.js";
5
5
  import { destructuring, propertyReadsOf } from "./destructuring.js";
6
6
  import { specifierCandidates } from "./configuration-files.js";
7
7
  import { namespaceUses } from "./namespace-objects.js";
8
- import { declarationsByName, isParameterProperty, nodeKey, } from "./inventory.js";
8
+ import { isParameterProperty, nodeKey } from "./inventory.js";
9
9
  import { ambientGlobalInterfaces, augmentationReferences } from "./augmentations.js";
10
10
  import { elementAccessReferences } from "./element-access.js";
11
11
  import { indexedAccessReferences } from "./indexed-access.js";
@@ -18,6 +18,8 @@ import { byPosition, isComponentFile, renderPosition } from "./position.js";
18
18
  import { DEFAULT_BATCH_CAP, propertyTables, UNANSWERED } from "./query.js";
19
19
  import { typeParameterReads } from "./type-parameter-reads.js";
20
20
  import { typeQueryAliases } from "./type-query-alias.js";
21
+ import { hasOwnBody } from "./own-body.js";
22
+ import { computedLoadsOf, isLoadCall } from "./unseen-loads.js";
21
23
  import { computedKeyNames, iterationReferences, keyNameOf, keyedUses, } from "./symbol-keys.js";
22
24
  import { handedOn, valuesOf } from "./value-flow.js";
23
25
  import { bytewise } from "./text/compare.js";
@@ -211,6 +213,12 @@ function moduleURLOf(node) {
211
213
  }
212
214
  return undefined;
213
215
  }
216
+ /** The file of the target a relative specifier written in `file` names, by its source or output name. */
217
+ function fileNamed(file, specifier, fileIdByName) {
218
+ return specifierCandidates(dirnamePath(file.fileName), specifier)
219
+ .map((path) => fileIdByName.get(path))
220
+ .find((id) => id !== undefined);
221
+ }
214
222
  /** Whether one node is a string literal or a template literal with no substitution. */
215
223
  function isLiteralText(node) {
216
224
  return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node));
@@ -330,6 +338,7 @@ function sitesOf(file, declarations, fileId, unread) {
330
338
  const evaluations = [];
331
339
  const wholes = [];
332
340
  const urls = [];
341
+ const loadCalls = [];
333
342
  const decorated = [];
334
343
  const parameterWrites = [];
335
344
  const namespaces = namespaceUses(file);
@@ -411,7 +420,10 @@ function sitesOf(file, declarations, fileId, unread) {
411
420
  }
412
421
  const url = moduleURLOf(node);
413
422
  if (url !== undefined) {
414
- urls.push(url);
423
+ urls.push({ ...url, from: enclosing });
424
+ }
425
+ if (isLoadCall(node)) {
426
+ loadCalls.push({ call: node, from: enclosing });
415
427
  }
416
428
  const held = declarations.get(nodeKey(file, node));
417
429
  const outer = enclosing;
@@ -445,6 +457,7 @@ function sitesOf(file, declarations, fileId, unread) {
445
457
  evaluations,
446
458
  wholes,
447
459
  urls,
460
+ loadCalls,
448
461
  decorated,
449
462
  parameterWrites,
450
463
  keyed,
@@ -611,20 +624,12 @@ function aloneIn(file, uses, writes, found) {
611
624
  return result;
612
625
  }
613
626
  /**
614
- * Every reference one project's own files make to the project's own declarations, in
615
- * position order, and the files the test-file rules classified.
616
- *
617
- * `held` is the inventory of the same project, whose declaration map is what a resolved
618
- * symbol is looked up in; `targetRoot` is the absolute path every position is rendered
619
- * against, which is the root the inventory was read against. A pass over a consumer
620
- * walks the consumer's files instead and renders against its root, as
621
- * {@link ReferenceOptions.consumer} states.
622
- *
623
- * The order is by the referencing position, then by the declaration named, the
624
- * declaration referencing it and the use. That is a total order because one name
625
- * references one declaration once from one declaration, while a chain of aliases reaches
626
- * several declarations at one position and a bare star re-export's specifier both reads
627
- * and evaluates the module it names.
627
+ * Every reference one project's own files make to its own declarations, and the files the
628
+ * test-file rules classified. `held` is the same project's inventory, where a resolved symbol
629
+ * is looked up; positions render against `targetRoot`, or a consumer's root as
630
+ * {@link ReferenceOptions.consumer} states. The order is by position, then the declaration
631
+ * named, the referencing declaration and the use: an alias chain or a bare star re-export
632
+ * names several at one position, and one name references one declaration once.
628
633
  */
629
634
  export function references(project, held, targetRoot, options) {
630
635
  const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
@@ -716,7 +721,7 @@ export function references(project, held, targetRoot, options) {
716
721
  * live. A name a namespace import binds may be a use of any file.
717
722
  */
718
723
  const named = (node) => {
719
- const byName = declarationsByName(held);
724
+ const byName = held.byName;
720
725
  const text = node.getText();
721
726
  const imported = importedNames(node.getSourceFile()).get(text) ?? [];
722
727
  return [
@@ -740,7 +745,7 @@ export function references(project, held, targetRoot, options) {
740
745
  return;
741
746
  }
742
747
  aliased.set(id, symbol === UNANSWERED
743
- ? (declarationsByName(held).get(alias.name) ?? []).map((one) => ({
748
+ ? (held.byName.get(alias.name) ?? []).map((one) => ({
744
749
  id: one,
745
750
  stepped: true,
746
751
  guessed: true,
@@ -783,6 +788,9 @@ export function references(project, held, targetRoot, options) {
783
788
  return { file, fileId, ...sitesOf(file, declarations, fileId, unread) };
784
789
  });
785
790
  const fileIdByName = new Map(walked.filter((one) => fileIds.has(one.fileId)).map((one) => [one.file.fileName, one.fileId]));
791
+ const loadable = walked
792
+ .filter((one) => fileIds.has(one.fileId))
793
+ .map(({ file, fileId }) => ({ file, fileId }));
786
794
  const contextual = contextualUses(project, held, chains, walked.flatMap((one) => one.literals), walked.flatMap((one) => one.flows), cap, tables);
787
795
  const writtenBy = new Map();
788
796
  for (const write of contextual.writes) {
@@ -862,8 +870,9 @@ export function references(project, held, targetRoot, options) {
862
870
  }
863
871
  }
864
872
  const keyNames = computedKeyNames(held.symbols);
865
- for (const { file, fileId, uses, reads, links, evaluations, wholes, urls, decorated, parameterWrites, keyed, } of walked) {
873
+ for (const { file, fileId, uses, reads, links, evaluations, wholes, urls, loadCalls, decorated, parameterWrites, keyed, } of walked) {
866
874
  const test = testFile.get(file) === true;
875
+ const loads = computedLoadsOf(file, loadCalls, loadable);
867
876
  // A component file's markup may use any binding its blocks declare at the top level.
868
877
  const markup = isComponentFile(file);
869
878
  const bindings = markup ? importBindings(file) : [];
@@ -904,6 +913,10 @@ export function references(project, held, targetRoot, options) {
904
913
  ...wholes.map((whole) => whole.specifier),
905
914
  ...urls.flatMap((url) => url.constructor ?? []),
906
915
  ...bindings.map((binding) => binding.node),
916
+ ...loads.flatMap((load) => [
917
+ ...(load.require === undefined ? [] : [load.require]),
918
+ ...(load.asset ? [] : load.files.map((loaded) => loaded.file)),
919
+ ]),
907
920
  ]),
908
921
  ];
909
922
  const answered = new Map();
@@ -939,6 +952,18 @@ export function references(project, held, targetRoot, options) {
939
952
  });
940
953
  }
941
954
  }
955
+ // A script declares no module, so the checker names nothing for a specifier of one;
956
+ // the compiler's module resolution still names its file.
957
+ const unnamed = evaluations.flatMap(({ node }) => answered.get(node) === undefined &&
958
+ isStringLiteralLikeNode(node) &&
959
+ queriedFile(file, node, fileIdByName) === undefined
960
+ ? [node]
961
+ : []);
962
+ const resolved = project.resolvedFileNames(unnamed);
963
+ const scripts = new Map(unnamed.map((node, at) => {
964
+ const name = resolved[at];
965
+ return [node, name === undefined ? undefined : fileIdByName.get(name)];
966
+ }));
942
967
  for (const evaluation of evaluations) {
943
968
  const queried = queriedFile(file, evaluation.node, fileIdByName);
944
969
  if (queried !== undefined) {
@@ -952,6 +977,17 @@ export function references(project, held, targetRoot, options) {
952
977
  });
953
978
  }
954
979
  const module = answered.get(evaluation.node);
980
+ const script = scripts.get(evaluation.node);
981
+ if (script !== undefined) {
982
+ found.push({
983
+ from: evaluation.from,
984
+ to: script,
985
+ position: renderPosition(file, root, evaluation.node.getStart()),
986
+ use: "evaluation",
987
+ resolution: "syntax",
988
+ test,
989
+ });
990
+ }
955
991
  if (module === undefined) {
956
992
  continue;
957
993
  }
@@ -973,19 +1009,13 @@ export function references(project, held, targetRoot, options) {
973
1009
  }
974
1010
  for (const url of urls) {
975
1011
  const constructor = url.constructor === undefined ? undefined : answered.get(url.constructor);
976
- // An augmentation of the global `URL` merges into it, which keeps the
977
- // library's declaration; a `URL` the target declares has no other.
978
- if (typeof constructor === "object" &&
979
- constructor.declarations.length > 0 &&
980
- constructor.declarations.every((handle) => ownPaths.has(handle.path))) {
1012
+ if (typeof constructor === "object" && hasOwnBody(project, constructor)) {
981
1013
  continue;
982
1014
  }
983
- const loaded = specifierCandidates(dirnamePath(file.fileName), url.specifier)
984
- .map((path) => fileIdByName.get(path))
985
- .find((id) => id !== undefined);
1015
+ const loaded = fileNamed(file, url.specifier, fileIdByName);
986
1016
  if (loaded !== undefined) {
987
1017
  found.push({
988
- from: fileId,
1018
+ from: url.from,
989
1019
  to: loaded,
990
1020
  position: renderPosition(file, root, url.at.getStart()),
991
1021
  use: "evaluation",
@@ -994,6 +1024,37 @@ export function references(project, held, targetRoot, options) {
994
1024
  });
995
1025
  }
996
1026
  }
1027
+ // A computed load imports each file it loads where the call is written, reading what
1028
+ // the file exports unless it reads the file as text or a URL; hasOwnBody owns which
1029
+ // `require` loads nothing.
1030
+ for (const load of loads) {
1031
+ const callee = load.require === undefined ? undefined : answered.get(load.require);
1032
+ if (typeof callee === "object" && hasOwnBody(project, callee)) {
1033
+ continue;
1034
+ }
1035
+ const position = renderPosition(file, root, load.call.getStart());
1036
+ for (const loaded of load.files) {
1037
+ found.push({
1038
+ from: load.eager ? fileId : load.from,
1039
+ to: loaded.fileId,
1040
+ position,
1041
+ use: "evaluation",
1042
+ resolution: "syntax",
1043
+ test,
1044
+ });
1045
+ const module = load.asset ? undefined : answered.get(loaded.file);
1046
+ for (const target of module === undefined ? [] : exportsRead(module)) {
1047
+ found.push({
1048
+ from: load.from,
1049
+ to: target.id,
1050
+ position,
1051
+ use: "read",
1052
+ resolution: target.guessed ? "by-name" : "batch",
1053
+ test,
1054
+ });
1055
+ }
1056
+ }
1057
+ }
997
1058
  for (const whole of wholes) {
998
1059
  const module = answered.get(whole.specifier);
999
1060
  if (module === undefined) {
@@ -1060,7 +1121,7 @@ export function references(project, held, targetRoot, options) {
1060
1121
  }
1061
1122
  const position = renderPosition(file, root, read.key.getStart());
1062
1123
  const targets = symbol === UNANSWERED
1063
- ? (declarationsByName(held).get(read.name) ?? []).map((id) => ({
1124
+ ? (held.byName.get(read.name) ?? []).map((id) => ({
1064
1125
  id,
1065
1126
  stepped: false,
1066
1127
  guessed: true,
@@ -138,6 +138,9 @@ function viewOf(project, configFile, engine, log) {
138
138
  ownKeys ??= new Set(ownSourceFiles().map((file) => file.path));
139
139
  return ownKeys;
140
140
  },
141
+ resolvedFileNames: (specifiers) => batched(engine, specifiers, (node) => project.program.getResolvedModuleFromModuleSpecifier.gen(node, node.getSourceFile().fileName)).map((resolved) => resolved === undefined || resolved.resolvedFileName === ""
142
+ ? undefined
143
+ : resolved.resolvedFileName),
141
144
  handle: (node) => ({ node }),
142
145
  symbolsAt: (handles, cap) => queries.symbolsAt(handles.map(({ node }) => node), cap),
143
146
  resolvedSymbolAt: ({ node }) => queries.resolvedSymbol(node),
@@ -4,12 +4,12 @@
4
4
  * below it, or on its own line where it trails a token there.
5
5
  */
6
6
  import { CODE } from "./contract/contract.generated.js";
7
- import { isConstructorDeclaration, SyntaxKind, } from "@typescript/native/ast";
7
+ import { SyntaxKind } from "@typescript/native/ast";
8
8
  import { importBindings } from "./import-bindings.js";
9
- import { isParameterProperty, nodeKey } from "./inventory.js";
10
- import { byPosition, positionKey, renderPosition } from "./position.js";
9
+ import { nodeKey } from "./inventory.js";
10
+ import { relativePath } from "./paths.js";
11
+ import { byPosition, lineEnd, positionKey, renderPosition, startLine, tokenStart, } from "./position.js";
11
12
  import { importRef } from "./ref.js";
12
- import { signatureOf } from "./signatures.js";
13
13
  import { isPartKind, SuppressionError, siteText, } from "./suppress.js";
14
14
  /**
15
15
  * The three expressions the grammar applies to the text of each line comment, in this
@@ -55,43 +55,6 @@ const LITERALS = new Set([
55
55
  SyntaxKind.RegularExpressionLiteral,
56
56
  SyntaxKind.JsxText,
57
57
  ]);
58
- /** Whether one character ends a line, under the language's line terminators. */
59
- function endsLine(char) {
60
- return char === "\n" || char === "\r" || char === "\u2028" || char === "\u2029";
61
- }
62
- /** The offset where the line holding `from` ends. */
63
- function lineEnd(text, from) {
64
- let at = from;
65
- while (at < text.length && !endsLine(text[at])) {
66
- at += 1;
67
- }
68
- return at;
69
- }
70
- /**
71
- * The offset of the first token at or after `from`: whitespace, comments and a leading
72
- * hashbang are trivia.
73
- */
74
- export function tokenStart(text, from) {
75
- let at = from;
76
- if (at === 0 && text.startsWith("#!")) {
77
- at = lineEnd(text, 0);
78
- }
79
- for (;;) {
80
- if (/\s/u.test(text[at] ?? "")) {
81
- at += 1;
82
- }
83
- else if (text.startsWith("//", at)) {
84
- at = lineEnd(text, at);
85
- }
86
- else if (text.startsWith("/*", at)) {
87
- const close = text.indexOf("*/", at + 2);
88
- at = close < 0 ? text.length : close + 2;
89
- }
90
- else {
91
- return at;
92
- }
93
- }
94
- }
95
58
  /**
96
59
  * Every `//` line comment of one file, in file order. The text between tokens is read as
97
60
  * the language reads it: a block comment holds no line comment, and the literal tokens the
@@ -140,55 +103,42 @@ function lineComments(file) {
140
103
  function bindingKey(one) {
141
104
  return `${one.declared?.id ?? ""}\u0000${one.part.id}`;
142
105
  }
106
+ /** The placed declarations of a project by the path of the file each is in. */
107
+ function placedByPath(held, symbols) {
108
+ const byPath = new Map();
109
+ for (const [id, placed] of held.placement) {
110
+ const one = symbols.get(id);
111
+ const holder = symbols.get(placed.holder);
112
+ if (one === undefined || holder === undefined) {
113
+ continue;
114
+ }
115
+ const at = byPath.get(one.position.path) ?? [];
116
+ at.push({ one, holder, startLines: placed.startLines, partLines: placed.partLines });
117
+ byPath.set(one.position.path, at);
118
+ }
119
+ return byPath;
120
+ }
143
121
  /**
144
- * The declarations of one file by the line each begins on, the line of its first token: a
145
- * decorator, a modifier or the keyword. A variable's first token is its statement's
146
- * keyword, and a variable written on a later line of the statement also begins there. A
147
- * function also begins, for a directive, on the line of each of its parameters, and a
148
- * class, for a part kind, on the lines of its constructor and its constructor's parameters.
122
+ * The declarations of one file by the line a directive binds each on, as the inventory
123
+ * places them, and each import binding of the file on its statement's lines.
149
124
  */
150
- function declarationLines(file, held, symbols, targetRoot) {
125
+ function declarationLines(file, placed, held, targetRoot) {
151
126
  const below = new Map();
152
- const add = (offset, one) => {
153
- const line = file.getLineAndCharacterOfPosition(tokenStart(file.text, offset)).line + 1;
127
+ const add = (line, one) => {
154
128
  const at = below.get(line) ?? [];
155
129
  if (!at.some((known) => bindingKey(known) === bindingKey(one))) {
156
130
  at.push(one);
157
131
  }
158
132
  below.set(line, at);
159
133
  };
160
- const declaredAt = (node) => {
161
- const id = held.declarations.get(nodeKey(file, node));
162
- return id === undefined ? undefined : symbols.get(id);
163
- };
164
- const visit = (node) => {
165
- const one = declaredAt(node);
166
- const owner = isConstructorDeclaration(node) ? declaredAt(node.parent) : undefined;
167
- if (owner !== undefined && isConstructorDeclaration(node)) {
168
- for (const at of [node, ...node.parameters.filter((one) => !isParameterProperty(one))]) {
169
- add(at.pos, { declared: undefined, part: owner });
170
- }
171
- }
172
- const holder = one !== undefined && isParameterProperty(node) ? declaredAt(node.parent.parent) : one;
173
- if (one !== undefined && holder !== undefined) {
174
- add(node.pos, { declared: one, part: holder });
134
+ for (const { one, holder, startLines, partLines } of placed) {
135
+ for (const line of startLines) {
136
+ add(line, { declared: one, part: holder });
175
137
  }
176
- if (one !== undefined) {
177
- // A parameter on a later line of a wrapped signature takes its directive on the
178
- // line above it, inside the parameter list, and the directive binds the function.
179
- for (const parameter of signatureOf(node)?.parameters ?? []) {
180
- add(parameter.pos, { declared: one, part: one });
181
- }
182
- const list = node.parent;
183
- if (node.kind === SyntaxKind.VariableDeclaration &&
184
- list.kind === SyntaxKind.VariableDeclarationList &&
185
- list.parent.kind === SyntaxKind.VariableStatement) {
186
- add(list.parent.pos, { declared: one, part: one });
187
- }
138
+ for (const line of partLines) {
139
+ add(line, { declared: undefined, part: one });
188
140
  }
189
- node.forEachChild(visit);
190
- };
191
- file.forEachChild(visit);
141
+ }
192
142
  // An import binding is a subject of its own, bound on its line as a declaration is.
193
143
  const fileId = held.declarations.get(nodeKey(file, file));
194
144
  const moduleRef = held.symbols.find((symbol) => symbol.id === fileId)?.ref ?? "";
@@ -200,7 +150,7 @@ function declarationLines(file, held, symbols, targetRoot) {
200
150
  statement = statement.parent;
201
151
  }
202
152
  for (const at of [statement.pos, name.getStart()]) {
203
- add(at, { declared: one, part: one });
153
+ add(startLine(file, at), { declared: one, part: one });
204
154
  }
205
155
  }
206
156
  return below;
@@ -221,13 +171,15 @@ export function inlineDirectives(project, held, targetRoot) {
221
171
  const symbols = new Map(held.symbols
222
172
  .filter((symbol) => symbol.kind !== "file")
223
173
  .map((symbol) => [symbol.id, { id: symbol.id, ref: symbol.ref, position: symbol.position }]));
174
+ const placed = placedByPath(held, symbols);
224
175
  const found = [];
225
176
  for (const file of project.ownSourceFiles()) {
226
177
  const comments = lineComments(file).filter((comment) => CANDIDATE.test(comment.text));
227
178
  if (comments.length === 0) {
228
179
  continue;
229
180
  }
230
- const below = declarationLines(file, held, symbols, targetRoot);
181
+ const path = relativePath(targetRoot, file.fileName) ?? "";
182
+ const below = declarationLines(file, placed.get(path) ?? [], held, targetRoot);
231
183
  for (const comment of comments) {
232
184
  const site = renderPosition(file, targetRoot, comment.offset);
233
185
  const target = trails(file, comment.offset) ? site.line : site.line + 1;