@untangle-code/file-graph 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/NOTICE +90 -0
- package/README.md +284 -2
- package/THIRD_PARTY_LICENSES.md +29 -0
- package/dist/assignment.d.ts +15 -0
- package/dist/assignment.d.ts.map +1 -0
- package/dist/assignment.js +75 -0
- package/dist/assignment.js.map +1 -0
- package/dist/boundaries/endpoints.d.ts +11 -0
- package/dist/boundaries/endpoints.d.ts.map +1 -0
- package/dist/boundaries/endpoints.js +195 -0
- package/dist/boundaries/endpoints.js.map +1 -0
- package/dist/boundaries/index.d.ts +9 -0
- package/dist/boundaries/index.d.ts.map +1 -0
- package/dist/boundaries/index.js +93 -0
- package/dist/boundaries/index.js.map +1 -0
- package/dist/boundaries/requests.d.ts +12 -0
- package/dist/boundaries/requests.d.ts.map +1 -0
- package/dist/boundaries/requests.js +311 -0
- package/dist/boundaries/requests.js.map +1 -0
- package/dist/edges.d.ts +49 -0
- package/dist/edges.d.ts.map +1 -0
- package/dist/edges.js +223 -0
- package/dist/edges.js.map +1 -0
- package/dist/file-index.d.ts +37 -0
- package/dist/file-index.d.ts.map +1 -0
- package/dist/file-index.js +103 -0
- package/dist/file-index.js.map +1 -0
- package/dist/graph.d.ts +160 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +448 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics.d.ts +67 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +272 -0
- package/dist/metrics.js.map +1 -0
- package/dist/package-catalog.d.ts +43 -0
- package/dist/package-catalog.d.ts.map +1 -0
- package/dist/package-catalog.js +136 -0
- package/dist/package-catalog.js.map +1 -0
- package/dist/paths.d.ts +56 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +129 -0
- package/dist/paths.js.map +1 -0
- package/dist/persistence.d.ts +83 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +538 -0
- package/dist/persistence.js.map +1 -0
- package/dist/resolve/candidates.d.ts +52 -0
- package/dist/resolve/candidates.d.ts.map +1 -0
- package/dist/resolve/candidates.js +202 -0
- package/dist/resolve/candidates.js.map +1 -0
- package/dist/resolve/package-exports.d.ts +31 -0
- package/dist/resolve/package-exports.d.ts.map +1 -0
- package/dist/resolve/package-exports.js +229 -0
- package/dist/resolve/package-exports.js.map +1 -0
- package/dist/resolve/pipeline.d.ts +11 -0
- package/dist/resolve/pipeline.d.ts.map +1 -0
- package/dist/resolve/pipeline.js +109 -0
- package/dist/resolve/pipeline.js.map +1 -0
- package/dist/resolve/resolvers.d.ts +25 -0
- package/dist/resolve/resolvers.d.ts.map +1 -0
- package/dist/resolve/resolvers.js +367 -0
- package/dist/resolve/resolvers.js.map +1 -0
- package/dist/restore.d.ts +75 -0
- package/dist/restore.d.ts.map +1 -0
- package/dist/restore.js +136 -0
- package/dist/restore.js.map +1 -0
- package/dist/topology/compose.d.ts +27 -0
- package/dist/topology/compose.d.ts.map +1 -0
- package/dist/topology/compose.js +208 -0
- package/dist/topology/compose.js.map +1 -0
- package/dist/topology/jsonc.d.ts +38 -0
- package/dist/topology/jsonc.d.ts.map +1 -0
- package/dist/topology/jsonc.js +137 -0
- package/dist/topology/jsonc.js.map +1 -0
- package/dist/topology/load.d.ts +61 -0
- package/dist/topology/load.d.ts.map +1 -0
- package/dist/topology/load.js +436 -0
- package/dist/topology/load.js.map +1 -0
- package/dist/topology/manifest.d.ts +48 -0
- package/dist/topology/manifest.d.ts.map +1 -0
- package/dist/topology/manifest.js +109 -0
- package/dist/topology/manifest.js.map +1 -0
- package/dist/topology/project-config.d.ts +39 -0
- package/dist/topology/project-config.d.ts.map +1 -0
- package/dist/topology/project-config.js +150 -0
- package/dist/topology/project-config.js.map +1 -0
- package/dist/topology/workspace-definition.d.ts +26 -0
- package/dist/topology/workspace-definition.d.ts.map +1 -0
- package/dist/topology/workspace-definition.js +104 -0
- package/dist/topology/workspace-definition.js.map +1 -0
- package/dist/traverse.d.ts +49 -0
- package/dist/traverse.d.ts.map +1 -0
- package/dist/traverse.js +167 -0
- package/dist/traverse.js.map +1 -0
- package/dist/types.d.ts +332 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -3
- package/src/assignment.ts +83 -0
- package/src/boundaries/endpoints.ts +273 -0
- package/src/boundaries/index.ts +118 -0
- package/src/boundaries/requests.ts +411 -0
- package/src/edges.ts +334 -0
- package/src/file-index.ts +127 -0
- package/src/graph.ts +591 -0
- package/src/index.ts +58 -0
- package/src/metrics.ts +350 -0
- package/src/package-catalog.ts +145 -0
- package/src/paths.ts +138 -0
- package/src/persistence.ts +746 -0
- package/src/resolve/candidates.ts +230 -0
- package/src/resolve/package-exports.ts +268 -0
- package/src/resolve/pipeline.ts +140 -0
- package/src/resolve/resolvers.ts +468 -0
- package/src/restore.ts +201 -0
- package/src/topology/compose.ts +268 -0
- package/src/topology/jsonc.ts +147 -0
- package/src/topology/load.ts +644 -0
- package/src/topology/manifest.ts +132 -0
- package/src/topology/project-config.ts +183 -0
- package/src/topology/workspace-definition.ts +115 -0
- package/src/traverse.ts +232 -0
- package/src/types.ts +372 -0
package/src/edges.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rohimporte → Kanten des Dateigraphen (spec.md §14.2).
|
|
3
|
+
*
|
|
4
|
+
* Diese Datei ist die Stelle, an der E4 konkret wird. Drei Faelle, die hier
|
|
5
|
+
* NICHT stillschweigend verschwinden:
|
|
6
|
+
*
|
|
7
|
+
* 1. Ein Import, der sich nicht aufloesen laesst, bleibt eine Kante. Er
|
|
8
|
+
* behaelt seine Art (`import` bleibt `import`) und bekommt
|
|
9
|
+
* `resolution.status: "unresolved"` mit benanntem Grund. So bleibt die
|
|
10
|
+
* Import-Resolution-Rate (spec.md §14.4) eine ehrliche Zahl.
|
|
11
|
+
* 2. Ein Import OHNE Spezifizierer kann keine Kante werden — `rawSpecifier`
|
|
12
|
+
* darf nie leer sein (`validateFileEdge`). Er wird deshalb zum
|
|
13
|
+
* Ledger-Eintrag, nicht zu einer weggelassenen Zeile.
|
|
14
|
+
* 3. Eine Datei, deren Importe gar nicht erst extrahiert werden konnten
|
|
15
|
+
* (`importsMissingReason`), erzeugt ebenfalls einen Ledger-Eintrag.
|
|
16
|
+
* Ohne ihn saehe eine nicht geparste Datei wie eine importfreie aus.
|
|
17
|
+
*/
|
|
18
|
+
import type {
|
|
19
|
+
FileEdgeKind,
|
|
20
|
+
FileEdgeSiteV1,
|
|
21
|
+
FileEdgeV1,
|
|
22
|
+
RawImport,
|
|
23
|
+
RawImportKind,
|
|
24
|
+
SkipLedgerEntryV1,
|
|
25
|
+
} from "@untangle-code/domain";
|
|
26
|
+
import {
|
|
27
|
+
FILE_GRAPH_VERSION,
|
|
28
|
+
buildFileEdgeId,
|
|
29
|
+
buildSkipLedgerEntry,
|
|
30
|
+
compareCodepoints,
|
|
31
|
+
} from "@untangle-code/domain";
|
|
32
|
+
|
|
33
|
+
import { assignContainerService, assignPackage } from "./assignment.js";
|
|
34
|
+
import { isAssetSpecifier } from "./resolve/candidates.js";
|
|
35
|
+
import { resolveImport } from "./resolve/pipeline.js";
|
|
36
|
+
import type {
|
|
37
|
+
ContainerServiceV1,
|
|
38
|
+
FileImportsV1,
|
|
39
|
+
ImportResolverV1,
|
|
40
|
+
ResolveContextV1,
|
|
41
|
+
WorkspacePackageCatalogV1,
|
|
42
|
+
WorkspacePackageV1,
|
|
43
|
+
} from "./types.js";
|
|
44
|
+
|
|
45
|
+
/** Abbildung der Parser-Importart auf die Kantenart des Graphen. */
|
|
46
|
+
export const RAW_IMPORT_KIND_TO_EDGE_KIND: Readonly<Record<RawImportKind, FileEdgeKind>> = {
|
|
47
|
+
import: "import",
|
|
48
|
+
re_export: "re-export",
|
|
49
|
+
dynamic_import: "dynamic-import",
|
|
50
|
+
// `package_dependency` entsteht nicht im Quelltext, sondern aus einem
|
|
51
|
+
// Manifest. Als Bezug auf eine Konfigurationsdatei ist es genau das, was
|
|
52
|
+
// `config-reference` beschreibt — und NICHT `import`, weil kein Modul
|
|
53
|
+
// geladen wird.
|
|
54
|
+
package_dependency: "config-reference",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Zeichen, die einen dynamischen Spezifizierer als nicht statisch auswertbar
|
|
59
|
+
* ausweisen. Der Parser liefert bei `import(variable)` den Ausdruckstext; der
|
|
60
|
+
* ist ein ehrlicher Rohstring, aber kein Modulpfad.
|
|
61
|
+
*/
|
|
62
|
+
const NON_LITERAL_SPECIFIER = /[`$+(){}[\]\s]|\.\.\.|\?\?|&&|\|\|/;
|
|
63
|
+
|
|
64
|
+
/** Alles, was der Kantenbau je Datei braucht. */
|
|
65
|
+
export interface EdgeBuildContextV1 {
|
|
66
|
+
workspaceId: string;
|
|
67
|
+
graphGeneration: string;
|
|
68
|
+
packages: readonly WorkspacePackageV1[];
|
|
69
|
+
/** Repository-bewusster Paketkatalog; siehe `package-catalog.ts`. */
|
|
70
|
+
packageCatalog: WorkspacePackageCatalogV1;
|
|
71
|
+
services: readonly ContainerServiceV1[];
|
|
72
|
+
projectAliases: ResolveContextV1["projectAliases"];
|
|
73
|
+
configuredAliases: ResolveContextV1["configuredAliases"];
|
|
74
|
+
extensionOrder: readonly string[];
|
|
75
|
+
files: ResolveContextV1["files"];
|
|
76
|
+
/** repoId → refId, fuer die Zielangabe aufgeloester Kanten. */
|
|
77
|
+
refIdByRepoId: ReadonlyMap<string, string>;
|
|
78
|
+
/** repoId + Pfad → fileId. */
|
|
79
|
+
resolveFileId: (repoId: string, path: string) => string | null;
|
|
80
|
+
pipeline?: readonly ImportResolverV1[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Ergebnis des Kantenbaus fuer EINE Datei. */
|
|
84
|
+
export interface FileEdgesV1 {
|
|
85
|
+
edges: FileEdgeV1[];
|
|
86
|
+
skipped: SkipLedgerEntryV1[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Baut alle Kanten einer einzelnen Datei. */
|
|
90
|
+
export function buildEdgesForFile(file: FileImportsV1, context: EdgeBuildContextV1): FileEdgesV1 {
|
|
91
|
+
const edges: FileEdgeV1[] = [];
|
|
92
|
+
const skipped: SkipLedgerEntryV1[] = [];
|
|
93
|
+
|
|
94
|
+
if (file.importsMissingReason !== undefined) {
|
|
95
|
+
skipped.push(
|
|
96
|
+
buildSkipLedgerEntry({
|
|
97
|
+
stage: "file_graph",
|
|
98
|
+
subjectKind: "file",
|
|
99
|
+
subjectId: file.fileId,
|
|
100
|
+
path: file.path,
|
|
101
|
+
repoId: file.repoId,
|
|
102
|
+
refId: file.refId,
|
|
103
|
+
reason: "parse_failed",
|
|
104
|
+
detail: `imports of "${file.path}" were not extracted: ${file.importsMissingReason}`,
|
|
105
|
+
recoverable: true,
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const fromPackage = assignPackage(context.packages, file.repoId, file.path);
|
|
111
|
+
const fromService = assignContainerService(context.services, file.repoId, file.path);
|
|
112
|
+
|
|
113
|
+
const resolveContext: ResolveContextV1 = {
|
|
114
|
+
workspaceId: context.workspaceId,
|
|
115
|
+
fromRepoId: file.repoId,
|
|
116
|
+
fromRefId: file.refId,
|
|
117
|
+
fromPath: file.path,
|
|
118
|
+
fromLanguageId: file.languageId,
|
|
119
|
+
...(fromPackage === null ? {} : { fromPackage }),
|
|
120
|
+
files: context.files,
|
|
121
|
+
packageCatalog: context.packageCatalog,
|
|
122
|
+
projectAliases: context.projectAliases,
|
|
123
|
+
configuredAliases: context.configuredAliases,
|
|
124
|
+
extensionOrder: context.extensionOrder,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const ordinals = new Map<string, number>();
|
|
128
|
+
|
|
129
|
+
for (const rawImport of file.imports) {
|
|
130
|
+
const specifier = rawImport.specifier;
|
|
131
|
+
if (specifier.trim() === "") {
|
|
132
|
+
skipped.push(
|
|
133
|
+
buildSkipLedgerEntry({
|
|
134
|
+
stage: "file_graph",
|
|
135
|
+
subjectKind: "file_edge",
|
|
136
|
+
path: file.path,
|
|
137
|
+
repoId: file.repoId,
|
|
138
|
+
refId: file.refId,
|
|
139
|
+
range: rawImport.range,
|
|
140
|
+
reason: "resolution_failed",
|
|
141
|
+
detail: `parser reported an import of kind "${rawImport.importKind}" without a specifier at line ${String(rawImport.range.startLine)}; an edge without a raw specifier would violate the file-edge invariant`,
|
|
142
|
+
recoverable: false,
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const kind = edgeKindOf(rawImport, specifier);
|
|
149
|
+
const ordinalKey = `${kind}\u001f${specifier}`;
|
|
150
|
+
const occurrenceOrdinal = ordinals.get(ordinalKey) ?? 0;
|
|
151
|
+
ordinals.set(ordinalKey, occurrenceOrdinal + 1);
|
|
152
|
+
|
|
153
|
+
const resolution = resolveSpecifier(rawImport, specifier, resolveContext, context);
|
|
154
|
+
|
|
155
|
+
const site: FileEdgeSiteV1 = {
|
|
156
|
+
range: rawImport.range,
|
|
157
|
+
...(rawImport.importedNames !== undefined && rawImport.importedNames.length > 0
|
|
158
|
+
? { importedNames: [...rawImport.importedNames] }
|
|
159
|
+
: {}),
|
|
160
|
+
...(rawImport.typeOnly === undefined ? {} : { typeOnly: rawImport.typeOnly }),
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Die Grenzen werden aus der ENDGUELTIGEN Aufloesung abgeleitet, nicht aus
|
|
164
|
+
// der des Resolvers: `toDomainResolution` kann einen Treffer noch zu einem
|
|
165
|
+
// Fehlschlag herabstufen (wenn der Index keine `file_id` zum Pfad hat), und
|
|
166
|
+
// eine Kante mit `crossRepo: true` bei `status: "unresolved"` waere eine
|
|
167
|
+
// Behauptung ohne Grundlage.
|
|
168
|
+
const domainResolution = toDomainResolution(resolution, context);
|
|
169
|
+
|
|
170
|
+
let targetPackage: WorkspacePackageV1 | null = null;
|
|
171
|
+
let targetService: ContainerServiceV1 | null = null;
|
|
172
|
+
let externalPackageName: string | undefined;
|
|
173
|
+
|
|
174
|
+
if (domainResolution.status === "resolved") {
|
|
175
|
+
targetPackage = assignPackage(
|
|
176
|
+
context.packages,
|
|
177
|
+
domainResolution.targetRepoId,
|
|
178
|
+
domainResolution.targetPath,
|
|
179
|
+
);
|
|
180
|
+
targetService = assignContainerService(
|
|
181
|
+
context.services,
|
|
182
|
+
domainResolution.targetRepoId,
|
|
183
|
+
domainResolution.targetPath,
|
|
184
|
+
);
|
|
185
|
+
} else if (domainResolution.status === "external") {
|
|
186
|
+
externalPackageName = domainResolution.packageName;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const crossRepo =
|
|
190
|
+
domainResolution.status === "resolved" && domainResolution.targetRepoId !== file.repoId;
|
|
191
|
+
const toPackageName = targetPackage?.name ?? externalPackageName;
|
|
192
|
+
// Paketgleichheit ist NICHT Namensgleichheit: zwei Repositories eines
|
|
193
|
+
// Workspace duerfen ein gleichnamiges Paket fuehren, und eine Kante von
|
|
194
|
+
// einem ins andere ueberquert sehr wohl eine Paketgrenze. Verglichen wird
|
|
195
|
+
// deshalb die Identitaet (Repository + Verzeichnis), nicht der Name.
|
|
196
|
+
const crossPackage =
|
|
197
|
+
toPackageName !== undefined &&
|
|
198
|
+
(fromPackage === null ||
|
|
199
|
+
(targetPackage === null
|
|
200
|
+
? fromPackage.name !== toPackageName
|
|
201
|
+
: packageKeyOf(fromPackage) !== packageKeyOf(targetPackage)));
|
|
202
|
+
const crossContainer =
|
|
203
|
+
fromService !== null && targetService !== null && fromService.name !== targetService.name;
|
|
204
|
+
|
|
205
|
+
const edge: FileEdgeV1 = {
|
|
206
|
+
schemaVersion: 1,
|
|
207
|
+
edgeId: buildFileEdgeId({
|
|
208
|
+
workspaceId: context.workspaceId,
|
|
209
|
+
fromFileId: file.fileId,
|
|
210
|
+
edgeKind: kind,
|
|
211
|
+
rawSpecifier: specifier,
|
|
212
|
+
occurrenceOrdinal,
|
|
213
|
+
}),
|
|
214
|
+
workspaceId: context.workspaceId,
|
|
215
|
+
repoId: file.repoId,
|
|
216
|
+
refId: file.refId,
|
|
217
|
+
fromFileId: file.fileId,
|
|
218
|
+
fromPath: file.path,
|
|
219
|
+
kind,
|
|
220
|
+
rawSpecifier: specifier,
|
|
221
|
+
resolution: domainResolution,
|
|
222
|
+
boundary: {
|
|
223
|
+
crossRepo,
|
|
224
|
+
crossPackage,
|
|
225
|
+
crossContainer,
|
|
226
|
+
...(fromPackage === null ? {} : { fromPackageName: fromPackage.name }),
|
|
227
|
+
...(toPackageName === undefined ? {} : { toPackageName }),
|
|
228
|
+
...(fromService === null ? {} : { fromContainerService: fromService.name }),
|
|
229
|
+
...(targetService === null ? {} : { toContainerService: targetService.name }),
|
|
230
|
+
},
|
|
231
|
+
sites: [site],
|
|
232
|
+
occurrenceOrdinal,
|
|
233
|
+
graphGeneration: context.graphGeneration,
|
|
234
|
+
graphVersion: FILE_GRAPH_VERSION,
|
|
235
|
+
};
|
|
236
|
+
edges.push(edge);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
edges.sort(compareEdges);
|
|
240
|
+
return { edges, skipped };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Identitaet eines Pakets: Repository plus Verzeichnis, nicht der Name. */
|
|
244
|
+
function packageKeyOf(entry: WorkspacePackageV1): string {
|
|
245
|
+
return `${entry.repoId}\u001f${entry.packageDir}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Stabile Kantenordnung: Quelle, Art, Rohstring, laufende Nummer. */
|
|
249
|
+
export function compareEdges(left: FileEdgeV1, right: FileEdgeV1): number {
|
|
250
|
+
return (
|
|
251
|
+
compareCodepoints(left.fromFileId, right.fromFileId) ||
|
|
252
|
+
compareCodepoints(left.kind, right.kind) ||
|
|
253
|
+
compareCodepoints(left.rawSpecifier, right.rawSpecifier) ||
|
|
254
|
+
left.occurrenceOrdinal - right.occurrenceOrdinal ||
|
|
255
|
+
compareCodepoints(left.edgeId, right.edgeId)
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function edgeKindOf(rawImport: RawImport, specifier: string): FileEdgeKind {
|
|
260
|
+
const base = RAW_IMPORT_KIND_TO_EDGE_KIND[rawImport.importKind];
|
|
261
|
+
// Ein Import einer Nicht-Code-Ressource bleibt eine echte Abhaengigkeit,
|
|
262
|
+
// ist aber keine Modulbeziehung — `asset-reference` macht sichtbar, dass
|
|
263
|
+
// die Zieldatei nie Symbole beitragen wird (spec.md §19/§20).
|
|
264
|
+
if ((base === "import" || base === "dynamic-import") && isAssetSpecifier(specifier)) {
|
|
265
|
+
return "asset-reference";
|
|
266
|
+
}
|
|
267
|
+
return base;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
type LocalResolution = ReturnType<typeof resolveImport>["result"];
|
|
271
|
+
|
|
272
|
+
function resolveSpecifier(
|
|
273
|
+
rawImport: RawImport,
|
|
274
|
+
specifier: string,
|
|
275
|
+
resolveContext: ResolveContextV1,
|
|
276
|
+
context: EdgeBuildContextV1,
|
|
277
|
+
): LocalResolution {
|
|
278
|
+
if (rawImport.importKind === "dynamic_import" && NON_LITERAL_SPECIFIER.test(specifier)) {
|
|
279
|
+
return {
|
|
280
|
+
status: "unresolved",
|
|
281
|
+
reason: "dynamic_specifier_not_statically_analyzable",
|
|
282
|
+
detail: `dynamic import argument "${specifier}" is an expression, not a literal module specifier; the raw text is kept so the call site stays findable`,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return resolveImport(specifier, resolveContext, context.pipeline).result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function toDomainResolution(
|
|
289
|
+
resolution: LocalResolution,
|
|
290
|
+
context: EdgeBuildContextV1,
|
|
291
|
+
): FileEdgeV1["resolution"] {
|
|
292
|
+
if (resolution.status === "external") {
|
|
293
|
+
return {
|
|
294
|
+
status: "external",
|
|
295
|
+
mechanism: resolution.mechanism,
|
|
296
|
+
packageName: resolution.packageName,
|
|
297
|
+
...(resolution.declaredVersion === undefined
|
|
298
|
+
? {}
|
|
299
|
+
: { declaredVersion: resolution.declaredVersion }),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (resolution.status === "unresolved") {
|
|
303
|
+
return {
|
|
304
|
+
status: "unresolved",
|
|
305
|
+
reason: resolution.reason,
|
|
306
|
+
detail: resolution.detail,
|
|
307
|
+
...(resolution.rejectedCandidates === undefined
|
|
308
|
+
? {}
|
|
309
|
+
: { rejectedCandidates: resolution.rejectedCandidates }),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const targetFileId = context.resolveFileId(resolution.targetRepoId, resolution.targetPath);
|
|
313
|
+
if (targetFileId === null) {
|
|
314
|
+
// Kann nur passieren, wenn der Index zwischen Probe und Kantenbau
|
|
315
|
+
// veraendert wurde. Der Fall wird zum benannten Fehlschlag, nicht zu
|
|
316
|
+
// einer Kante mit leerer Ziel-ID.
|
|
317
|
+
return {
|
|
318
|
+
status: "unresolved",
|
|
319
|
+
reason: "target_file_not_found",
|
|
320
|
+
detail: `resolver found "${resolution.targetPath}" in repository "${resolution.targetRepoId}", but the file index has no id for it`,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
status: "resolved",
|
|
325
|
+
mechanism: resolution.mechanism,
|
|
326
|
+
targetFileId,
|
|
327
|
+
targetRepoId: resolution.targetRepoId,
|
|
328
|
+
targetRefId: context.refIdByRepoId.get(resolution.targetRepoId) ?? "",
|
|
329
|
+
targetPath: resolution.targetPath,
|
|
330
|
+
...(resolution.viaPackageName === undefined
|
|
331
|
+
? {}
|
|
332
|
+
: { viaPackageName: resolution.viaPackageName }),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dateiverzeichnis: die einzige Nachschlagequelle der Aufloesung.
|
|
3
|
+
*
|
|
4
|
+
* Die gesamte Modulaufloesung arbeitet gegen diesen Index und NICHT gegen das
|
|
5
|
+
* Dateisystem. Grund: eine Aufloesung, die das Dateisystem befragt, haengt am
|
|
6
|
+
* Zustand der Platte zum Zeitpunkt des Laufs und ist damit weder
|
|
7
|
+
* reproduzierbar noch ohne echtes Repository testbar. Der Index wird einmal
|
|
8
|
+
* aus der Dateiliste des Scanners gebaut; ab da ist jede Aufloesung eine reine
|
|
9
|
+
* Funktion.
|
|
10
|
+
*/
|
|
11
|
+
import { compareCodepoints } from "@untangle-code/domain";
|
|
12
|
+
|
|
13
|
+
import { normalizePath } from "./paths.js";
|
|
14
|
+
import type { FileImportsV1, FileLookupV1 } from "./types.js";
|
|
15
|
+
|
|
16
|
+
/** Eintrag des Index — genau das, was die Aufloesung ueber eine Datei wissen muss. */
|
|
17
|
+
export interface FileIndexEntryV1 {
|
|
18
|
+
fileId: string;
|
|
19
|
+
repoId: string;
|
|
20
|
+
refId: string;
|
|
21
|
+
path: string;
|
|
22
|
+
languageId: string;
|
|
23
|
+
contentHash: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class FileIndex implements FileLookupV1 {
|
|
27
|
+
private readonly byRepoPath = new Map<string, FileIndexEntryV1>();
|
|
28
|
+
private readonly byFileId = new Map<string, FileIndexEntryV1>();
|
|
29
|
+
private readonly repos = new Set<string>();
|
|
30
|
+
|
|
31
|
+
static fromFiles(files: readonly FileImportsV1[]): FileIndex {
|
|
32
|
+
const index = new FileIndex();
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
index.add({
|
|
35
|
+
fileId: file.fileId,
|
|
36
|
+
repoId: file.repoId,
|
|
37
|
+
refId: file.refId,
|
|
38
|
+
path: normalizePath(file.path),
|
|
39
|
+
languageId: file.languageId,
|
|
40
|
+
contentHash: file.contentHash,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return index;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Nimmt einen Eintrag auf und ERSETZT dabei jede fruehere Bindung derselben
|
|
48
|
+
* `file_id`.
|
|
49
|
+
*
|
|
50
|
+
* Der zweite Schritt ist der wichtige: eine Umbenennung erreicht den Index
|
|
51
|
+
* als `add()` mit neuem Pfad (`FileGraphChangeV1` kennt kein „renamed"). Wer
|
|
52
|
+
* dabei nur den neuen Schluessel setzt, laesst den alten stehen — der Index
|
|
53
|
+
* behauptet danach eine Datei unter einem Pfad, den keine Datei mehr hat,
|
|
54
|
+
* und die Aufloesung meldet eine Kante dorthin als `resolved`. Ein Phantom
|
|
55
|
+
* mit Aufloesungsrate 1 ist genau das stille Verschweigen, das E4 verbietet.
|
|
56
|
+
*/
|
|
57
|
+
add(entry: FileIndexEntryV1): void {
|
|
58
|
+
const normalized: FileIndexEntryV1 = { ...entry, path: normalizePath(entry.path) };
|
|
59
|
+
const newKey = keyOf(normalized.repoId, normalized.path);
|
|
60
|
+
const previous = this.byFileId.get(normalized.fileId);
|
|
61
|
+
if (previous !== undefined) {
|
|
62
|
+
const previousKey = keyOf(previous.repoId, previous.path);
|
|
63
|
+
// Nur loeschen, wenn der alte Schluessel WIRKLICH noch auf diese Datei
|
|
64
|
+
// zeigt: hat inzwischen eine andere Datei den Pfad uebernommen, waere das
|
|
65
|
+
// Loeschen ein zweites Phantom — diesmal ein fehlendes statt eines
|
|
66
|
+
// ueberzaehligen.
|
|
67
|
+
if (
|
|
68
|
+
previousKey !== newKey &&
|
|
69
|
+
this.byRepoPath.get(previousKey)?.fileId === normalized.fileId
|
|
70
|
+
) {
|
|
71
|
+
this.byRepoPath.delete(previousKey);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
this.byRepoPath.set(newKey, normalized);
|
|
75
|
+
this.byFileId.set(normalized.fileId, normalized);
|
|
76
|
+
this.repos.add(normalized.repoId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Entfernt einen Eintrag; True, wenn er vorhanden war. */
|
|
80
|
+
remove(fileId: string): boolean {
|
|
81
|
+
const entry = this.byFileId.get(fileId);
|
|
82
|
+
if (entry === undefined) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
this.byFileId.delete(fileId);
|
|
86
|
+
const key = keyOf(entry.repoId, entry.path);
|
|
87
|
+
// Denselben Grund wie in `add()`: den Pfadschluessel nur freigeben, wenn er
|
|
88
|
+
// noch auf genau diese Datei zeigt.
|
|
89
|
+
if (this.byRepoPath.get(key)?.fileId === fileId) {
|
|
90
|
+
this.byRepoPath.delete(key);
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
hasFile(repoId: string, path: string): boolean {
|
|
96
|
+
return this.byRepoPath.has(keyOf(repoId, normalizePath(path)));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fileIdOf(repoId: string, path: string): string | null {
|
|
100
|
+
return this.byRepoPath.get(keyOf(repoId, normalizePath(path)))?.fileId ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
entryOf(repoId: string, path: string): FileIndexEntryV1 | null {
|
|
104
|
+
return this.byRepoPath.get(keyOf(repoId, normalizePath(path))) ?? null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
entryById(fileId: string): FileIndexEntryV1 | null {
|
|
108
|
+
return this.byFileId.get(fileId) ?? null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
repoIds(): readonly string[] {
|
|
112
|
+
return [...this.repos].sort(compareCodepoints);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get size(): number {
|
|
116
|
+
return this.byFileId.size;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* U+001F als Trenner: derselbe Trenner, den `domain`s ID-Ableitung nutzt. Er
|
|
122
|
+
* kann in keinem Pfad und in keiner Repository-ID vorkommen, also kann der
|
|
123
|
+
* Schluessel von ("ab", "c") nie mit dem von ("a", "bc") kollidieren.
|
|
124
|
+
*/
|
|
125
|
+
function keyOf(repoId: string, path: string): string {
|
|
126
|
+
return `${repoId}\u001f${path}`;
|
|
127
|
+
}
|