fallow-type-aware 3.34.0 → 3.35.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.
- package/README.md +3 -0
- package/package.json +1 -1
- package/src/protocol.mjs +30 -2
- package/src/reference-names.mjs +75 -0
- package/src/semantic.mjs +27 -68
- package/src/symbol-targets.mjs +68 -0
package/README.md
CHANGED
|
@@ -14,6 +14,9 @@ zero-based UTF-8 byte column, and optional owner. Results keep their semantic
|
|
|
14
14
|
assertion separate from `complete`, `partial`, or `unavailable` status. Evidence
|
|
15
15
|
and every operation-specific array are deterministic and bounded, with totals,
|
|
16
16
|
omissions, reason codes, actions, and truncation reported explicitly.
|
|
17
|
+
The request `evidence_limit` is at most 40. A `type-coupling` query can set its
|
|
18
|
+
own `evidence_limit` of at most 10,000, because its result is one report for
|
|
19
|
+
the whole project.
|
|
17
20
|
|
|
18
21
|
Structural diagnostics,
|
|
19
22
|
unknown identities, missing projects, unsupported syntax, dynamic behavior,
|
package/package.json
CHANGED
package/src/protocol.mjs
CHANGED
|
@@ -56,7 +56,13 @@ const API_SURFACE_QUERY_KEYS = new Set([
|
|
|
56
56
|
"include_cycles",
|
|
57
57
|
"private_leak_candidates",
|
|
58
58
|
]);
|
|
59
|
-
const TYPE_COUPLING_QUERY_KEYS = new Set([
|
|
59
|
+
const TYPE_COUPLING_QUERY_KEYS = new Set([
|
|
60
|
+
"id",
|
|
61
|
+
"operation",
|
|
62
|
+
"entry_points",
|
|
63
|
+
"include_cycles",
|
|
64
|
+
"evidence_limit",
|
|
65
|
+
]);
|
|
60
66
|
const PRIVATE_LEAK_CANDIDATE_KEYS = new Set(["id", "path", "export_name", "type_name"]);
|
|
61
67
|
const SYMBOL_KEYS = new Set([
|
|
62
68
|
"path",
|
|
@@ -77,6 +83,10 @@ const MAX_QUERIES = 25_000;
|
|
|
77
83
|
const MAX_GRAPH_QUERIES = 256;
|
|
78
84
|
const MAX_PRIVATE_LEAK_CANDIDATES = 25_000;
|
|
79
85
|
const MAX_EVIDENCE_PER_RESULT = 40;
|
|
86
|
+
// A type-coupling result is one project-wide report, so it can carry more
|
|
87
|
+
// evidence than a per-symbol result. This bound keeps the response well below
|
|
88
|
+
// the native client's semantic response byte limit.
|
|
89
|
+
const MAX_TYPE_COUPLING_EVIDENCE = 10_000;
|
|
80
90
|
const MAX_STRING_CHARS = 4_096;
|
|
81
91
|
|
|
82
92
|
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -257,13 +267,31 @@ const parseFrameworkContracts = (value, field) =>
|
|
|
257
267
|
const parseIncludeCycles = (value, field) =>
|
|
258
268
|
value === undefined ? false : requireBoolean(value, field);
|
|
259
269
|
|
|
270
|
+
const parseTypeCouplingEvidenceLimit = (value, field) => {
|
|
271
|
+
const limit = requireInteger(value, field, 1);
|
|
272
|
+
if (limit > MAX_TYPE_COUPLING_EVIDENCE) {
|
|
273
|
+
throw new Error(`${field} exceeds the ${MAX_TYPE_COUPLING_EVIDENCE} item limit`);
|
|
274
|
+
}
|
|
275
|
+
return limit;
|
|
276
|
+
};
|
|
277
|
+
|
|
260
278
|
const parseGraphQuery = (value, field, root, query) => {
|
|
261
279
|
const graphQuery = {
|
|
262
280
|
...query,
|
|
263
281
|
entryPoints: parseEntryPoints(value.entry_points, `${field}.entry_points`, root),
|
|
264
282
|
includeCycles: parseIncludeCycles(value.include_cycles, `${field}.include_cycles`),
|
|
265
283
|
};
|
|
266
|
-
if (query.operation
|
|
284
|
+
if (query.operation === "type-coupling") {
|
|
285
|
+
return value.evidence_limit === undefined
|
|
286
|
+
? graphQuery
|
|
287
|
+
: {
|
|
288
|
+
...graphQuery,
|
|
289
|
+
evidenceLimit: parseTypeCouplingEvidenceLimit(
|
|
290
|
+
value.evidence_limit,
|
|
291
|
+
`${field}.evidence_limit`,
|
|
292
|
+
),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
267
295
|
return {
|
|
268
296
|
...graphQuery,
|
|
269
297
|
privateLeakCandidates: parsePrivateLeakCandidates(
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isExportDeclaration,
|
|
3
|
+
isImportDeclaration,
|
|
4
|
+
isImportEqualsDeclaration,
|
|
5
|
+
isModuleBlock,
|
|
6
|
+
isModuleDeclaration,
|
|
7
|
+
isNamedExports,
|
|
8
|
+
isNamespaceExportDeclaration,
|
|
9
|
+
isNamespaceImport,
|
|
10
|
+
} from "typescript/unstable/ast/is";
|
|
11
|
+
|
|
12
|
+
const JAVASCRIPT_FILE = /\.[cm]?jsx?$/iu;
|
|
13
|
+
|
|
14
|
+
// In JavaScript, require bindings and export assignments also create aliases.
|
|
15
|
+
// The scan keeps every identifier of these files.
|
|
16
|
+
export const isJavaScriptFile = (sourceFile) => JAVASCRIPT_FILE.test(sourceFile.fileName);
|
|
17
|
+
|
|
18
|
+
const importNames = ({ importClause }) => {
|
|
19
|
+
const bindings = importClause?.namedBindings;
|
|
20
|
+
if (!bindings) return [importClause?.name];
|
|
21
|
+
if (isNamespaceImport(bindings)) return [importClause.name, bindings.name];
|
|
22
|
+
return [importClause.name, ...bindings.elements.map((element) => element.name)];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const exportNames = ({ exportClause }) => {
|
|
26
|
+
if (!exportClause) return [];
|
|
27
|
+
if (isNamedExports(exportClause)) return exportClause.elements.map((element) => element.name);
|
|
28
|
+
return [exportClause.name];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const moduleBodyNames = (body) => {
|
|
32
|
+
if (body && isModuleDeclaration(body)) return moduleBodyNames(body.body);
|
|
33
|
+
return body && isModuleBlock(body) ? body.statements.flatMap(statementAliasNames) : [];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const statementAliasNames = (statement) => {
|
|
37
|
+
if (isImportDeclaration(statement)) return importNames(statement);
|
|
38
|
+
if (isImportEqualsDeclaration(statement) || isNamespaceExportDeclaration(statement)) {
|
|
39
|
+
return [statement.name];
|
|
40
|
+
}
|
|
41
|
+
if (isExportDeclaration(statement)) return exportNames(statement);
|
|
42
|
+
if (isModuleDeclaration(statement)) return moduleBodyNames(statement.body);
|
|
43
|
+
return [];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Default-library and external-library files sit below node_modules. The
|
|
47
|
+
// path check skips them before the source file and its metadata, which each
|
|
48
|
+
// cost a checker round trip.
|
|
49
|
+
const NODE_MODULES_SEGMENT = /[\\/]node_modules[\\/]/u;
|
|
50
|
+
|
|
51
|
+
const isOwnSourceFile = (project, sourceFile) =>
|
|
52
|
+
Boolean(sourceFile) &&
|
|
53
|
+
!project.program.isSourceFileDefaultLibrary(sourceFile) &&
|
|
54
|
+
!project.program.isSourceFileFromExternalLibrary(sourceFile);
|
|
55
|
+
|
|
56
|
+
const programAliasNames = (project) =>
|
|
57
|
+
project.program
|
|
58
|
+
.getSourceFileNames()
|
|
59
|
+
.filter((fileName) => !NODE_MODULES_SEGMENT.test(fileName))
|
|
60
|
+
.map((fileName) => project.program.getSourceFile(fileName))
|
|
61
|
+
.filter((sourceFile) => isOwnSourceFile(project, sourceFile))
|
|
62
|
+
.flatMap((sourceFile) => sourceFile.statements.flatMap(statementAliasNames))
|
|
63
|
+
.map((name) => name?.text)
|
|
64
|
+
.filter((text) => typeof text === "string");
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns the identifier names that can reach a symbol-use target.
|
|
68
|
+
*
|
|
69
|
+
* An identifier resolves to a declaration with its own name, or to an alias
|
|
70
|
+
* that an import or export declares under that name. So the scan needs only
|
|
71
|
+
* the target names and the names that the alias declarations of the project
|
|
72
|
+
* files declare, project declaration files included.
|
|
73
|
+
*/
|
|
74
|
+
export const referenceNames = (projects, targetNames) =>
|
|
75
|
+
new Set([...targetNames, "default", ...projects.flatMap(programAliasNames)]);
|
package/src/semantic.mjs
CHANGED
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
isGetAccessorDeclaration,
|
|
16
16
|
isIdentifier,
|
|
17
17
|
isImportDeclaration,
|
|
18
|
-
isImportClause,
|
|
19
18
|
isImportSpecifier,
|
|
20
19
|
isImportTypeNode,
|
|
21
20
|
isNamespaceImport,
|
|
@@ -51,6 +50,8 @@ import {
|
|
|
51
50
|
symbolForDeclaration,
|
|
52
51
|
} from "./semantic-identity.mjs";
|
|
53
52
|
import { analyzeSymbolImpact } from "./symbol-impact.mjs";
|
|
53
|
+
import { isJavaScriptFile, referenceNames } from "./reference-names.mjs";
|
|
54
|
+
import { matchingAliasEntries, matchingSymbolEntries, targetName } from "./symbol-targets.mjs";
|
|
54
55
|
import { analyzeTypeCoupling } from "./type-coupling.mjs";
|
|
55
56
|
|
|
56
57
|
const ATTACHED_COMMENT_PATTERN = /\/\/|\/\*/u;
|
|
@@ -352,15 +353,10 @@ const semanticUse = (root, node, namespace) => {
|
|
|
352
353
|
};
|
|
353
354
|
};
|
|
354
355
|
|
|
355
|
-
const isDefaultImportReference = (node) => isImportClause(node.parent) && node.parent.name === node;
|
|
356
|
-
|
|
357
356
|
const candidateNameMatches = (candidateNames, name) => !candidateNames || candidateNames.has(name);
|
|
358
357
|
|
|
359
|
-
const isIdentifierReference = (node,
|
|
360
|
-
|
|
361
|
-
if (candidateNameMatches(candidateNames, node.text)) return true;
|
|
362
|
-
return includeDefaultImports && isDefaultImportReference(node);
|
|
363
|
-
};
|
|
358
|
+
const isIdentifierReference = (node, names) =>
|
|
359
|
+
isIdentifier(node) && candidateNameMatches(names, node.text);
|
|
364
360
|
|
|
365
361
|
const isElementNameNode = (node) =>
|
|
366
362
|
isStringLiteralLikeNode(node) &&
|
|
@@ -396,8 +392,11 @@ const symbolUseBatchState = (resolvedQueries, evidenceLimit) => {
|
|
|
396
392
|
uncertaintiesByQuery: new Map(resolvedQueries.map(({ query }) => [query.id, new Set()])),
|
|
397
393
|
declarationLocationsByQuery: new Map(resolvedQueries.map(({ query }) => [query.id, new Set()])),
|
|
398
394
|
targetsByKey: new Map(),
|
|
395
|
+
targetNames: new Set(),
|
|
396
|
+
aliasEntriesByProject: new Map(),
|
|
399
397
|
exportEntriesByModule: new Map(),
|
|
400
398
|
exportEntriesByProject: new Map(),
|
|
399
|
+
exportedNames: new Set(),
|
|
401
400
|
candidateNames: new Set(),
|
|
402
401
|
classMemberEntries,
|
|
403
402
|
classMemberEntriesByName: entriesByName(classMemberEntries),
|
|
@@ -412,6 +411,7 @@ const addSymbolTarget = (root, state, entry, declaration, namespace) => {
|
|
|
412
411
|
state.declarationLocationsByQuery
|
|
413
412
|
.get(entry.query.id)
|
|
414
413
|
.add(locationKey(location(root, declaration.name ?? declaration)));
|
|
414
|
+
state.targetNames.add(targetName(declaration));
|
|
415
415
|
const key = stableDeclarationKey(declaration, namespace);
|
|
416
416
|
const targets = state.targetsByKey.get(key) ?? [];
|
|
417
417
|
targets.push(entry);
|
|
@@ -422,6 +422,7 @@ const registerSymbolTargets = (root, state, entry) => {
|
|
|
422
422
|
state.candidateNames.add(entry.query.symbol.localName);
|
|
423
423
|
state.candidateNames.add(entry.query.symbol.exportedName);
|
|
424
424
|
if (entry.query.symbol.declarationKind === "export") {
|
|
425
|
+
state.exportedNames.add(entry.query.symbol.exportedName);
|
|
425
426
|
const moduleIdentity = canonicalFileIdentity(entry.query.symbol.absolutePath);
|
|
426
427
|
const entries = state.exportEntriesByModule.get(moduleIdentity) ?? new Set();
|
|
427
428
|
entries.add(entry);
|
|
@@ -447,45 +448,6 @@ const registerSymbolTargets = (root, state, entry) => {
|
|
|
447
448
|
state.contractRelationsByQuery.set(entry.query.id, entry.resolved.contractRelations);
|
|
448
449
|
};
|
|
449
450
|
|
|
450
|
-
const matchingSymbolEntries = (project, symbol, namespace, targetsByKey) => {
|
|
451
|
-
const declarations = declarationsForSymbol(project, symbol);
|
|
452
|
-
const matchingLane = declarations.filter((declaration) =>
|
|
453
|
-
declarationNamespaces(declaration).has(namespace),
|
|
454
|
-
);
|
|
455
|
-
if (matchingLane.length > 0) {
|
|
456
|
-
return new Set(
|
|
457
|
-
matchingLane.flatMap(
|
|
458
|
-
(declaration) => targetsByKey.get(stableDeclarationKey(declaration, namespace)) ?? [],
|
|
459
|
-
),
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
return new Set(
|
|
463
|
-
declarations.flatMap((declaration) =>
|
|
464
|
-
[...declarationNamespaces(declaration)].flatMap(
|
|
465
|
-
(declaredNamespace) =>
|
|
466
|
-
targetsByKey.get(stableDeclarationKey(declaration, declaredNamespace)) ?? [],
|
|
467
|
-
),
|
|
468
|
-
),
|
|
469
|
-
);
|
|
470
|
-
};
|
|
471
|
-
|
|
472
|
-
const matchingAliasEntries = (project, symbol, namespace, targetsByKey) => {
|
|
473
|
-
const entries = new Set();
|
|
474
|
-
const seen = new Set();
|
|
475
|
-
let current = symbol;
|
|
476
|
-
while (current && !seen.has(current)) {
|
|
477
|
-
seen.add(current);
|
|
478
|
-
matchingSymbolEntries(project, current, namespace, targetsByKey).forEach((entry) =>
|
|
479
|
-
entries.add(entry),
|
|
480
|
-
);
|
|
481
|
-
if ((current.flags & SymbolFlags.Alias) === 0) break;
|
|
482
|
-
const aliased = project.checker.getAliasedSymbol(current);
|
|
483
|
-
if (project.checker.isUnknownSymbol(aliased) || aliased === current) break;
|
|
484
|
-
current = aliased;
|
|
485
|
-
}
|
|
486
|
-
return entries;
|
|
487
|
-
};
|
|
488
|
-
|
|
489
451
|
const isDeclarationReference = (root, state, entry, node) => {
|
|
490
452
|
const locations = state.declarationLocationsByQuery.get(entry.query.id);
|
|
491
453
|
if (locations.has(locationKey(location(root, node)))) return true;
|
|
@@ -683,9 +645,7 @@ const recordComputedMemberAccess = (state, project, entries, node) => {
|
|
|
683
645
|
const matched = new Set(
|
|
684
646
|
receiverProperties
|
|
685
647
|
.filter((property) => state.candidateNames.has(property.name))
|
|
686
|
-
.flatMap((property) => [
|
|
687
|
-
...matchingSymbolEntries(project, property, "value", state.targetsByKey),
|
|
688
|
-
]),
|
|
648
|
+
.flatMap((property) => [...matchingSymbolEntries(project, property, "value", state)]),
|
|
689
649
|
);
|
|
690
650
|
matched.forEach((entry) => recordSymbolUncertainty(state, entry, "dynamic-member-access"));
|
|
691
651
|
};
|
|
@@ -706,22 +666,24 @@ const recordDynamicImportUncertainty = (state, project, node) => {
|
|
|
706
666
|
);
|
|
707
667
|
};
|
|
708
668
|
|
|
709
|
-
const referenceNamespaces = (project, node,
|
|
669
|
+
const referenceNamespaces = (project, node, rawSymbol) => {
|
|
710
670
|
const alias = aliasNode(node);
|
|
711
671
|
if (!alias || !isExportSpecifier(alias)) {
|
|
712
672
|
return [isTypePosition(node) ? "type" : "value"];
|
|
713
673
|
}
|
|
714
674
|
if (alias.isTypeOnly || alias.parent?.parent?.isTypeOnly) return ["type"];
|
|
715
675
|
const namespaces = new Set(
|
|
716
|
-
declarationsForSymbol(project,
|
|
717
|
-
...declarationNamespaces(declaration),
|
|
718
|
-
|
|
676
|
+
declarationsForSymbol(project, resolveAlias(project.checker, rawSymbol)).flatMap(
|
|
677
|
+
(declaration) => [...declarationNamespaces(declaration)],
|
|
678
|
+
),
|
|
719
679
|
);
|
|
720
680
|
return namespaces.size > 0 ? [...namespaces] : [isTypePosition(node) ? "type" : "value"];
|
|
721
681
|
};
|
|
722
682
|
|
|
723
683
|
const surfaceEntriesForNode = (state, project, node, namespace) => {
|
|
724
|
-
|
|
684
|
+
// Resolving the referenced module costs checker round trips, so skip every
|
|
685
|
+
// identifier that cannot name an export query.
|
|
686
|
+
if (!state.exportedNames.has(node.text) || moduleEdgeDeclaration(node)) return new Set();
|
|
725
687
|
const moduleIdentity = referencedModuleIdentity(project, node);
|
|
726
688
|
if (!moduleIdentity) return new Set();
|
|
727
689
|
const direct = [...(state.exportEntriesByModule.get(moduleIdentity) ?? [])].filter(
|
|
@@ -732,7 +694,7 @@ const surfaceEntriesForNode = (state, project, node, namespace) => {
|
|
|
732
694
|
for (const entry of direct) {
|
|
733
695
|
for (const { declaration } of entry.resolved.ownerContexts) {
|
|
734
696
|
const symbol = resolveAlias(project.checker, symbolForDeclaration(project, declaration));
|
|
735
|
-
matchingSymbolEntries(project, symbol, namespace, state
|
|
697
|
+
matchingSymbolEntries(project, symbol, namespace, state).forEach((related) =>
|
|
736
698
|
entries.add(related),
|
|
737
699
|
);
|
|
738
700
|
}
|
|
@@ -740,7 +702,7 @@ const surfaceEntriesForNode = (state, project, node, namespace) => {
|
|
|
740
702
|
return entries;
|
|
741
703
|
};
|
|
742
704
|
|
|
743
|
-
const scanSymbolUseFile = (root, state, project, sourceFile,
|
|
705
|
+
const scanSymbolUseFile = (root, state, project, sourceFile, names) => {
|
|
744
706
|
const projectEntries = classMemberEntrySetForProject(state, project);
|
|
745
707
|
const nodes = [];
|
|
746
708
|
visit(sourceFile, (node) => {
|
|
@@ -749,7 +711,7 @@ const scanSymbolUseFile = (root, state, project, sourceFile, includeDefaultImpor
|
|
|
749
711
|
recordStringDispatchedMemberAccess(state, projectEntries, node);
|
|
750
712
|
recordComputedMemberAccess(state, project, projectEntries, node);
|
|
751
713
|
}
|
|
752
|
-
if (isIdentifierReference(node, undefined
|
|
714
|
+
if (isIdentifierReference(node, isJavaScriptFile(sourceFile) ? undefined : names)) {
|
|
753
715
|
nodes.push(node);
|
|
754
716
|
return;
|
|
755
717
|
}
|
|
@@ -758,10 +720,9 @@ const scanSymbolUseFile = (root, state, project, sourceFile, includeDefaultImpor
|
|
|
758
720
|
const symbols = project.checker.getSymbolAtLocation(nodes);
|
|
759
721
|
nodes.forEach((node, index) => {
|
|
760
722
|
const rawSymbol = symbols[index];
|
|
761
|
-
|
|
762
|
-
referenceNamespaces(project, node, symbol).forEach((namespace) => {
|
|
723
|
+
referenceNamespaces(project, node, rawSymbol).forEach((namespace) => {
|
|
763
724
|
const entries = new Set([
|
|
764
|
-
...matchingAliasEntries(project, rawSymbol, namespace, state
|
|
725
|
+
...matchingAliasEntries(project, rawSymbol, namespace, state),
|
|
765
726
|
...surfaceEntriesForNode(state, project, node, namespace),
|
|
766
727
|
]);
|
|
767
728
|
entries.forEach((entry) => recordSymbolUse(root, state, project, entry, node, namespace));
|
|
@@ -769,7 +730,7 @@ const scanSymbolUseFile = (root, state, project, sourceFile, includeDefaultImpor
|
|
|
769
730
|
});
|
|
770
731
|
};
|
|
771
732
|
|
|
772
|
-
const scanSymbolUseProjects = (root, state, projects,
|
|
733
|
+
const scanSymbolUseProjects = (root, state, projects, names) => {
|
|
773
734
|
const scannedFiles = new Set();
|
|
774
735
|
let sourceScanCount = 0;
|
|
775
736
|
for (const project of projects) {
|
|
@@ -778,7 +739,7 @@ const scanSymbolUseProjects = (root, state, projects, includeDefaultImports) =>
|
|
|
778
739
|
if (scannedFiles.has(fileIdentity)) continue;
|
|
779
740
|
scannedFiles.add(fileIdentity);
|
|
780
741
|
sourceScanCount += 1;
|
|
781
|
-
scanSymbolUseFile(root, state, project, sourceFile,
|
|
742
|
+
scanSymbolUseFile(root, state, project, sourceFile, names);
|
|
782
743
|
}
|
|
783
744
|
}
|
|
784
745
|
return sourceScanCount;
|
|
@@ -787,10 +748,8 @@ const scanSymbolUseProjects = (root, state, projects, includeDefaultImports) =>
|
|
|
787
748
|
const batchSymbolUseEvidence = (root, resolvedQueries, scanProjects, evidenceLimit) => {
|
|
788
749
|
const state = symbolUseBatchState(resolvedQueries, evidenceLimit);
|
|
789
750
|
resolvedQueries.forEach((entry) => registerSymbolTargets(root, state, entry));
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
);
|
|
793
|
-
const sourceScanCount = scanSymbolUseProjects(root, state, scanProjects, includeDefaultImports);
|
|
751
|
+
const names = referenceNames(scanProjects, [...state.candidateNames, ...state.targetNames]);
|
|
752
|
+
const sourceScanCount = scanSymbolUseProjects(root, state, scanProjects, names);
|
|
794
753
|
return {
|
|
795
754
|
evidenceByQuery: state.evidenceByQuery,
|
|
796
755
|
totalByQuery: state.totalByQuery,
|
|
@@ -1891,7 +1850,7 @@ const analyzeGraphSemanticQuery = (root, query, graphStates, evidenceLimit) => {
|
|
|
1891
1850
|
return analyzeApiSurface(root, query, graphStates, evidenceLimit);
|
|
1892
1851
|
}
|
|
1893
1852
|
return analyzeTypeCoupling(
|
|
1894
|
-
{ root, query, states: graphStates, evidenceLimit },
|
|
1853
|
+
{ root, query, states: graphStates, evidenceLimit: query.evidenceLimit ?? evidenceLimit },
|
|
1895
1854
|
{
|
|
1896
1855
|
boundedResult,
|
|
1897
1856
|
discoverEntryPoints,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { SymbolFlags } from "typescript/unstable/sync";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
declarationName,
|
|
5
|
+
declarationNamespaces,
|
|
6
|
+
declarationsForSymbol,
|
|
7
|
+
stableDeclarationKey,
|
|
8
|
+
} from "./semantic-identity.mjs";
|
|
9
|
+
|
|
10
|
+
export const targetName = (declaration) => declarationName(declaration) ?? "default";
|
|
11
|
+
|
|
12
|
+
// A target key contains the declaration name. The name check skips the key
|
|
13
|
+
// and its file identity for every declaration that cannot match.
|
|
14
|
+
const targetsForDeclaration = (state, declaration, namespace) =>
|
|
15
|
+
state.targetNames.has(targetName(declaration))
|
|
16
|
+
? (state.targetsByKey.get(stableDeclarationKey(declaration, namespace)) ?? [])
|
|
17
|
+
: [];
|
|
18
|
+
|
|
19
|
+
export const matchingSymbolEntries = (project, symbol, namespace, state) => {
|
|
20
|
+
const declarations = declarationsForSymbol(project, symbol);
|
|
21
|
+
const matchingLane = declarations.filter((declaration) =>
|
|
22
|
+
declarationNamespaces(declaration).has(namespace),
|
|
23
|
+
);
|
|
24
|
+
if (matchingLane.length > 0) {
|
|
25
|
+
return new Set(
|
|
26
|
+
matchingLane.flatMap((declaration) => targetsForDeclaration(state, declaration, namespace)),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return new Set(
|
|
30
|
+
declarations.flatMap((declaration) =>
|
|
31
|
+
[...declarationNamespaces(declaration)].flatMap((declaredNamespace) =>
|
|
32
|
+
targetsForDeclaration(state, declaration, declaredNamespace),
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const resolveAliasEntries = (project, symbol, namespace, state) => {
|
|
39
|
+
const entries = new Set();
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
let current = symbol;
|
|
42
|
+
while (current && !seen.has(current)) {
|
|
43
|
+
seen.add(current);
|
|
44
|
+
matchingSymbolEntries(project, current, namespace, state).forEach((entry) =>
|
|
45
|
+
entries.add(entry),
|
|
46
|
+
);
|
|
47
|
+
if ((current.flags & SymbolFlags.Alias) === 0) break;
|
|
48
|
+
const aliased = project.checker.getAliasedSymbol(current);
|
|
49
|
+
if (project.checker.isUnknownSymbol(aliased) || aliased === current) break;
|
|
50
|
+
current = aliased;
|
|
51
|
+
}
|
|
52
|
+
return entries;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// The scan visits every identifier, so one symbol occurs many times. Each
|
|
56
|
+
// checker call is a round trip, and the targets do not change during a batch,
|
|
57
|
+
// so the batch keeps one answer for each symbol and namespace.
|
|
58
|
+
export const matchingAliasEntries = (project, symbol, namespace, state) => {
|
|
59
|
+
if (!symbol) return new Set();
|
|
60
|
+
const bySymbol = state.aliasEntriesByProject.get(project) ?? new Map();
|
|
61
|
+
state.aliasEntriesByProject.set(project, bySymbol);
|
|
62
|
+
const byNamespace = bySymbol.get(symbol) ?? new Map();
|
|
63
|
+
bySymbol.set(symbol, byNamespace);
|
|
64
|
+
if (!byNamespace.has(namespace)) {
|
|
65
|
+
byNamespace.set(namespace, resolveAliasEntries(project, symbol, namespace, state));
|
|
66
|
+
}
|
|
67
|
+
return byNamespace.get(namespace);
|
|
68
|
+
};
|