skir 1.1.2 → 1.1.3
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/dist/compatibility_checker.d.ts.map +1 -1
- package/dist/compatibility_checker.js +2 -1
- package/dist/compatibility_checker.js.map +1 -1
- package/dist/compiler.js +11 -6
- package/dist/compiler.js.map +1 -1
- package/dist/module_collector.d.ts +1 -1
- package/dist/module_collector.d.ts.map +1 -1
- package/dist/module_collector.js +11 -5
- package/dist/module_collector.js.map +1 -1
- package/dist/module_set.d.ts +24 -19
- package/dist/module_set.d.ts.map +1 -1
- package/dist/module_set.js +327 -136
- package/dist/module_set.js.map +1 -1
- package/dist/snapshotter.js +6 -4
- package/dist/snapshotter.js.map +1 -1
- package/package.json +1 -1
- package/src/compatibility_checker.ts +2 -1
- package/src/compiler.ts +17 -6
- package/src/module_collector.ts +13 -4
- package/src/module_set.ts +406 -157
- package/src/snapshotter.ts +6 -4
package/src/module_set.ts
CHANGED
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
type UnresolvedType,
|
|
36
36
|
type Value,
|
|
37
37
|
} from "skir-internal";
|
|
38
|
-
import { FileReader } from "./io.js";
|
|
39
38
|
import {
|
|
40
39
|
isStringLiteral,
|
|
41
40
|
literalValueToDenseJson,
|
|
@@ -43,37 +42,50 @@ import {
|
|
|
43
42
|
valueHasPrimitiveType,
|
|
44
43
|
} from "./literals.js";
|
|
45
44
|
import { parseModule } from "./parser.js";
|
|
46
|
-
import { tokenizeModule } from "./tokenizer.js";
|
|
45
|
+
import { ModuleTokens, tokenizeModule } from "./tokenizer.js";
|
|
47
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Result of compiling a set of modules. Immutable.
|
|
49
|
+
*
|
|
50
|
+
* Support incremental compilation by accepting an optional cache of the previous
|
|
51
|
+
* module set. This cache can be used to avoid re-parsing and re-resolving
|
|
52
|
+
* modules that haven't changed since the last compilation.
|
|
53
|
+
*/
|
|
48
54
|
export class ModuleSet {
|
|
49
|
-
static
|
|
50
|
-
|
|
55
|
+
static compile(
|
|
56
|
+
modulePathToContent: ReadonlyMap<string, string>,
|
|
57
|
+
cache?: ModuleSet,
|
|
58
|
+
): ModuleSet {
|
|
59
|
+
return new ModuleSet(modulePathToContent, cache);
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
constructor(
|
|
63
|
+
private readonly modulePathToContent: ReadonlyMap<string, string>,
|
|
64
|
+
cache: ModuleSet | undefined,
|
|
65
|
+
) {
|
|
66
|
+
this.cache = cache
|
|
67
|
+
? new Cache(modulePathToContent, cache.moduleBundles)
|
|
68
|
+
: undefined;
|
|
69
|
+
for (const modulePath of modulePathToContent.keys()) {
|
|
70
|
+
this.parseAndResolve(modulePath, new Set<string>());
|
|
57
71
|
}
|
|
58
|
-
|
|
72
|
+
this.finalizationResult = this.finalize();
|
|
73
|
+
// So it can be garbage collected.
|
|
74
|
+
this.cache = undefined;
|
|
59
75
|
}
|
|
60
76
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
parseAndResolve(
|
|
77
|
+
private parseAndResolve(
|
|
64
78
|
modulePath: string,
|
|
65
|
-
inProgressSet
|
|
66
|
-
):
|
|
67
|
-
const inMap = this.
|
|
79
|
+
inProgressSet: Set<string>,
|
|
80
|
+
): ModuleBundle | null {
|
|
81
|
+
const inMap = this.moduleBundles.get(modulePath);
|
|
68
82
|
if (inMap !== undefined) {
|
|
69
83
|
return inMap;
|
|
70
84
|
}
|
|
71
|
-
const result = this.doParseAndResolve(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.mutableModules.set(modulePath, result);
|
|
76
|
-
this.mutableErrors.push(...result.errors);
|
|
85
|
+
const result = this.doParseAndResolve(modulePath, inProgressSet);
|
|
86
|
+
if (result) {
|
|
87
|
+
this.moduleBundles.set(modulePath, result);
|
|
88
|
+
}
|
|
77
89
|
return result;
|
|
78
90
|
}
|
|
79
91
|
|
|
@@ -81,19 +93,46 @@ export class ModuleSet {
|
|
|
81
93
|
private doParseAndResolve(
|
|
82
94
|
modulePath: string,
|
|
83
95
|
inProgressSet: Set<string>,
|
|
84
|
-
):
|
|
85
|
-
const
|
|
96
|
+
): ModuleBundle | null {
|
|
97
|
+
const moduleContent = this.modulePathToContent.get(modulePath);
|
|
98
|
+
if (moduleContent === undefined) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
86
101
|
|
|
87
|
-
let
|
|
102
|
+
let moduleTokens: Result<ModuleTokens>;
|
|
88
103
|
{
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
const moduleCacheResult = this.cache?.getModuleCacheResult(
|
|
105
|
+
modulePath,
|
|
106
|
+
) ?? { kind: "no-cache" };
|
|
107
|
+
switch (moduleCacheResult.kind) {
|
|
108
|
+
case "no-cache": {
|
|
109
|
+
moduleTokens = tokenizeModule(moduleContent, modulePath);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case "module-tokens": {
|
|
113
|
+
moduleTokens = moduleCacheResult.tokens;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "module-bundle": {
|
|
117
|
+
this.registry.mergeFrom(moduleCacheResult.bundle.registry);
|
|
118
|
+
return moduleCacheResult.bundle;
|
|
119
|
+
}
|
|
92
120
|
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let module: MutableModule;
|
|
124
|
+
const errors: SkirError[] = [];
|
|
125
|
+
{
|
|
126
|
+
const parseResult = parseModule(moduleTokens.result, "strict");
|
|
93
127
|
errors.push(...parseResult.errors);
|
|
94
128
|
module = parseResult.result;
|
|
95
129
|
}
|
|
96
130
|
|
|
131
|
+
const moduleBundle = new ModuleBundle(moduleTokens, {
|
|
132
|
+
result: module,
|
|
133
|
+
errors: errors,
|
|
134
|
+
});
|
|
135
|
+
|
|
97
136
|
// Process all imports.
|
|
98
137
|
const pathToImports = new Map<string, Array<Import | ImportAlias>>();
|
|
99
138
|
for (const declaration of module.declarations) {
|
|
@@ -133,13 +172,21 @@ export class ModuleSet {
|
|
|
133
172
|
const otherModule = this.parseAndResolve(otherModulePath, inProgressSet);
|
|
134
173
|
inProgressSet.delete(modulePath);
|
|
135
174
|
|
|
136
|
-
if (otherModule
|
|
175
|
+
if (otherModule === null) {
|
|
137
176
|
errors.push({
|
|
138
177
|
token: declaration.modulePath,
|
|
139
178
|
message: "Module not found",
|
|
179
|
+
expectedNames: suggestModulePaths(
|
|
180
|
+
unquoteAndUnescape(declaration.modulePath.text),
|
|
181
|
+
modulePath,
|
|
182
|
+
this.modulePathToContent,
|
|
183
|
+
),
|
|
140
184
|
});
|
|
141
|
-
} else if (
|
|
142
|
-
|
|
185
|
+
} else if (
|
|
186
|
+
otherModule.tokens.errors.length !== 0 ||
|
|
187
|
+
otherModule.module.errors.length !== 0
|
|
188
|
+
) {
|
|
189
|
+
const hasCircularDependency = otherModule.module.errors.some(
|
|
143
190
|
(e) => e.message === circularDependencyMessage,
|
|
144
191
|
);
|
|
145
192
|
if (hasCircularDependency) {
|
|
@@ -159,13 +206,13 @@ export class ModuleSet {
|
|
|
159
206
|
// module and are not imported symbols themselves.
|
|
160
207
|
for (const importedName of declaration.importedNames) {
|
|
161
208
|
const importedDeclaration =
|
|
162
|
-
otherModule.result.nameToDeclaration[importedName.text];
|
|
209
|
+
otherModule.module.result.nameToDeclaration[importedName.text];
|
|
163
210
|
if (importedDeclaration === undefined) {
|
|
164
211
|
errors.push({
|
|
165
212
|
token: importedName,
|
|
166
213
|
message: "Not found",
|
|
167
214
|
expectedNames: declarationsToExpectedNames(
|
|
168
|
-
otherModule.result.nameToDeclaration,
|
|
215
|
+
otherModule.module.result.nameToDeclaration,
|
|
169
216
|
(d) => d.kind === "record",
|
|
170
217
|
),
|
|
171
218
|
});
|
|
@@ -185,7 +232,7 @@ export class ModuleSet {
|
|
|
185
232
|
}
|
|
186
233
|
|
|
187
234
|
const pathToImportedNames = module.pathToImportedNames;
|
|
188
|
-
for (const [path, imports] of pathToImports
|
|
235
|
+
for (const [path, imports] of pathToImports) {
|
|
189
236
|
const importsNoAlias = imports.filter(
|
|
190
237
|
(i): i is Import => i.kind === "import",
|
|
191
238
|
);
|
|
@@ -232,38 +279,21 @@ export class ModuleSet {
|
|
|
232
279
|
}
|
|
233
280
|
}
|
|
234
281
|
|
|
235
|
-
const result: Result<Module> = {
|
|
236
|
-
result: module,
|
|
237
|
-
errors: errors,
|
|
238
|
-
};
|
|
239
|
-
|
|
240
282
|
if (errors.length) {
|
|
241
|
-
return
|
|
283
|
+
return moduleBundle;
|
|
242
284
|
}
|
|
243
285
|
|
|
244
|
-
this.mutableResolvedModules.push(module);
|
|
245
|
-
|
|
246
286
|
// We can't merge these 3 loops into a single one, each operation must run
|
|
247
287
|
// after the last operation ran on the whole map.
|
|
248
288
|
|
|
249
289
|
// Loop 1: merge the module records map into the cross-module record map.
|
|
250
290
|
for (const record of module.records) {
|
|
251
291
|
const { key } = record.record;
|
|
252
|
-
this.
|
|
292
|
+
this.registry.recordMap.set(key, record);
|
|
293
|
+
moduleBundle.registry.recordMap.set(key, record);
|
|
253
294
|
const { recordNumber } = record.record;
|
|
254
295
|
if (recordNumber != null && !modulePath.startsWith("@")) {
|
|
255
|
-
|
|
256
|
-
if (existing === undefined) {
|
|
257
|
-
this.numberToRecord.set(recordNumber, key);
|
|
258
|
-
} else {
|
|
259
|
-
const otherRecord = this.recordMap.get(existing)!;
|
|
260
|
-
const otherRecordName = otherRecord.record.name.text;
|
|
261
|
-
const otherModulePath = otherRecord.modulePath;
|
|
262
|
-
errors.push({
|
|
263
|
-
token: record.record.name,
|
|
264
|
-
message: `Same number as ${otherRecordName} in ${otherModulePath}`,
|
|
265
|
-
});
|
|
266
|
-
}
|
|
296
|
+
moduleBundle.registry.pushNumberRecord(recordNumber, key);
|
|
267
297
|
}
|
|
268
298
|
}
|
|
269
299
|
|
|
@@ -272,7 +302,7 @@ export class ModuleSet {
|
|
|
272
302
|
const usedImports = new Set<string>();
|
|
273
303
|
const typeResolver = new TypeResolver(
|
|
274
304
|
module,
|
|
275
|
-
this.
|
|
305
|
+
this.moduleBundles,
|
|
276
306
|
usedImports,
|
|
277
307
|
errors,
|
|
278
308
|
);
|
|
@@ -326,20 +356,8 @@ export class ModuleSet {
|
|
|
326
356
|
this.validateArrayKeys(responseType, errors);
|
|
327
357
|
}
|
|
328
358
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const existing = this.numberToMethod.get(number);
|
|
332
|
-
if (existing === undefined) {
|
|
333
|
-
this.numberToMethod.set(number, method);
|
|
334
|
-
} else {
|
|
335
|
-
const otherMethodName = existing.name.text;
|
|
336
|
-
const otherModulePath = existing.name.line.modulePath;
|
|
337
|
-
errors.push({
|
|
338
|
-
token: method.name,
|
|
339
|
-
message: `Same number as ${otherMethodName} in ${otherModulePath}`,
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
}
|
|
359
|
+
const { number } = method;
|
|
360
|
+
moduleBundle.registry.pushNumberMethod(number, method);
|
|
343
361
|
// Resolve the references in the doc comments of the method.
|
|
344
362
|
this.resolveDocReferences(method, module, errors);
|
|
345
363
|
}
|
|
@@ -359,7 +377,9 @@ export class ModuleSet {
|
|
|
359
377
|
|
|
360
378
|
ensureAllImportsAreUsed(module, usedImports, errors);
|
|
361
379
|
|
|
362
|
-
|
|
380
|
+
this.registry.mergeFrom(moduleBundle.registry);
|
|
381
|
+
|
|
382
|
+
return moduleBundle;
|
|
363
383
|
}
|
|
364
384
|
|
|
365
385
|
private storeResolvedFieldTypes(
|
|
@@ -915,8 +935,8 @@ export class ModuleSet {
|
|
|
915
935
|
if (!resolvedModulePath) {
|
|
916
936
|
return false;
|
|
917
937
|
}
|
|
918
|
-
const importedModule = this.
|
|
919
|
-
if (!importedModule
|
|
938
|
+
const importedModule = this.moduleBundles.get(resolvedModulePath!);
|
|
939
|
+
if (!importedModule) {
|
|
920
940
|
return false;
|
|
921
941
|
}
|
|
922
942
|
let newNameChain: readonly MutableDocReferenceName[];
|
|
@@ -929,7 +949,7 @@ export class ModuleSet {
|
|
|
929
949
|
return tryResolveReference(
|
|
930
950
|
ref,
|
|
931
951
|
newNameChain,
|
|
932
|
-
importedModule.result,
|
|
952
|
+
importedModule.module.result,
|
|
933
953
|
);
|
|
934
954
|
}
|
|
935
955
|
case "constant":
|
|
@@ -944,7 +964,7 @@ export class ModuleSet {
|
|
|
944
964
|
}
|
|
945
965
|
};
|
|
946
966
|
|
|
947
|
-
const { recordMap } = this;
|
|
967
|
+
const { recordMap } = this.registry;
|
|
948
968
|
// Build list of naming scopes to search, in order of priority.
|
|
949
969
|
const scopes: Array<Record | Module> = [];
|
|
950
970
|
const pushRecordAncestorsToScopes = (record: Record): void => {
|
|
@@ -1012,56 +1032,107 @@ export class ModuleSet {
|
|
|
1012
1032
|
}
|
|
1013
1033
|
}
|
|
1014
1034
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1035
|
+
finalize(): FinalizationResult {
|
|
1036
|
+
type MutableModuleResult = {
|
|
1037
|
+
result: Module;
|
|
1038
|
+
errors: SkirError[];
|
|
1039
|
+
};
|
|
1040
|
+
const modules = new Map<string, MutableModuleResult>();
|
|
1041
|
+
|
|
1042
|
+
for (const [modulePath, moduleBundle] of this.moduleBundles) {
|
|
1043
|
+
const { module, tokens } = moduleBundle;
|
|
1044
|
+
const moduleErrors = [...tokens.errors, ...module.errors];
|
|
1045
|
+
modules.set(modulePath, {
|
|
1046
|
+
result: module.result,
|
|
1047
|
+
errors: moduleErrors,
|
|
1048
|
+
});
|
|
1018
1049
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1050
|
+
|
|
1051
|
+
// Look for duplicate method numbers.
|
|
1052
|
+
for (const methods of this.registry.numberToMethods.values()) {
|
|
1053
|
+
if (methods.length <= 1) {
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
const pushError = (method: Method, other: Method): void => {
|
|
1057
|
+
const modulePath = method.name.line.modulePath;
|
|
1058
|
+
const moduleResult = modules.get(modulePath)!;
|
|
1059
|
+
const otherMethodName = other.name.text;
|
|
1060
|
+
const otherModulePath = other.name.line.modulePath;
|
|
1061
|
+
moduleResult.errors.push({
|
|
1062
|
+
token: method.name,
|
|
1063
|
+
message: `Same number as ${otherMethodName} in ${otherModulePath}`,
|
|
1064
|
+
});
|
|
1065
|
+
};
|
|
1066
|
+
pushError(methods[0]!, methods[1]!);
|
|
1067
|
+
for (let i = 1; i < methods.length; ++i) {
|
|
1068
|
+
pushError(methods[i]!, methods[0]!);
|
|
1069
|
+
}
|
|
1021
1070
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1071
|
+
|
|
1072
|
+
// Look for duplicate record numbers.
|
|
1073
|
+
for (const records of this.registry.numberToRecords.values()) {
|
|
1074
|
+
if (records.length <= 1) {
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
const pushError = (recordKey: RecordKey, otherKey: RecordKey): void => {
|
|
1078
|
+
const record = this.registry.recordMap.get(recordKey)!;
|
|
1079
|
+
const other = this.registry.recordMap.get(otherKey)!;
|
|
1080
|
+
|
|
1081
|
+
const modulePath = record.record.name.line.modulePath;
|
|
1082
|
+
const moduleResult = modules.get(modulePath)!;
|
|
1083
|
+
const otherRecordName = other.record.name.text;
|
|
1084
|
+
const otherModulePath = other.modulePath;
|
|
1085
|
+
moduleResult.errors.push({
|
|
1086
|
+
token: record.record.name,
|
|
1087
|
+
message: `Same number as ${otherRecordName} in ${otherModulePath}`,
|
|
1088
|
+
});
|
|
1089
|
+
};
|
|
1090
|
+
pushError(records[0]!, records[1]!);
|
|
1091
|
+
for (let i = 1; i < records.length; ++i) {
|
|
1092
|
+
pushError(records[i]!, records[0]!);
|
|
1093
|
+
}
|
|
1025
1094
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1095
|
+
|
|
1096
|
+
// Aggregate errors across all modules.
|
|
1097
|
+
const errors: SkirError[] = [];
|
|
1098
|
+
for (const moduleBundle of modules.values()) {
|
|
1099
|
+
errors.push(...moduleBundle.errors);
|
|
1028
1100
|
}
|
|
1029
|
-
|
|
1101
|
+
return {
|
|
1102
|
+
modules: modules,
|
|
1103
|
+
errors: errors,
|
|
1104
|
+
};
|
|
1030
1105
|
}
|
|
1031
1106
|
|
|
1032
|
-
|
|
1033
|
-
private
|
|
1034
|
-
private readonly
|
|
1035
|
-
private readonly
|
|
1036
|
-
private readonly
|
|
1037
|
-
|
|
1107
|
+
// BEGIN PROPERTIES
|
|
1108
|
+
private cache: Cache | undefined;
|
|
1109
|
+
private readonly moduleBundles = new Map<string, ModuleBundle>();
|
|
1110
|
+
private readonly registry = new DeclarationRegistry();
|
|
1111
|
+
private readonly finalizationResult: FinalizationResult;
|
|
1112
|
+
// END PROPERTIES
|
|
1038
1113
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
get recordMap(): ReadonlyMap<RecordKey, RecordLocation> {
|
|
1044
|
-
return this.mutableRecordMap;
|
|
1114
|
+
findRecordByNumber(recordNumber: number): RecordLocation | undefined {
|
|
1115
|
+
const { numberToRecords, recordMap } = this.registry;
|
|
1116
|
+
const recordKeys = numberToRecords.get(recordNumber);
|
|
1117
|
+
return recordKeys?.length === 1 ? recordMap.get(recordKeys[0]!) : undefined;
|
|
1045
1118
|
}
|
|
1046
1119
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1120
|
+
findMethodByNumber(methodNumber: number): Method | undefined {
|
|
1121
|
+
const { numberToMethods } = this.registry;
|
|
1122
|
+
const methods = numberToMethods.get(methodNumber);
|
|
1123
|
+
return methods?.length === 1 ? methods[0]! : undefined;
|
|
1049
1124
|
}
|
|
1050
1125
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
if (recordKey === undefined) {
|
|
1054
|
-
return undefined;
|
|
1055
|
-
}
|
|
1056
|
-
return this.recordMap.get(recordKey);
|
|
1126
|
+
get recordMap(): ReadonlyMap<RecordKey, RecordLocation> {
|
|
1127
|
+
return this.registry.recordMap;
|
|
1057
1128
|
}
|
|
1058
1129
|
|
|
1059
|
-
|
|
1060
|
-
return this.
|
|
1130
|
+
get errors(): readonly SkirError[] {
|
|
1131
|
+
return this.finalizationResult.errors;
|
|
1061
1132
|
}
|
|
1062
1133
|
|
|
1063
|
-
get
|
|
1064
|
-
return this.
|
|
1134
|
+
get modules(): ReadonlyMap<string, Result<Module>> {
|
|
1135
|
+
return this.finalizationResult.modules;
|
|
1065
1136
|
}
|
|
1066
1137
|
}
|
|
1067
1138
|
|
|
@@ -1150,7 +1221,7 @@ function validateKeyedItems(
|
|
|
1150
1221
|
class TypeResolver {
|
|
1151
1222
|
constructor(
|
|
1152
1223
|
private readonly module: Module,
|
|
1153
|
-
private readonly
|
|
1224
|
+
private readonly moduleBundles: Map<string, ModuleBundle>,
|
|
1154
1225
|
private readonly usedImports: Set<string>,
|
|
1155
1226
|
private readonly errors: ErrorSink,
|
|
1156
1227
|
) {}
|
|
@@ -1196,7 +1267,7 @@ class TypeResolver {
|
|
|
1196
1267
|
// reference, or the module if the record reference is absolute (starts with
|
|
1197
1268
|
// a dot).
|
|
1198
1269
|
let start: Record | Module | undefined;
|
|
1199
|
-
const { errors, module, modules, usedImports } = this;
|
|
1270
|
+
const { errors, module, moduleBundles: modules, usedImports } = this;
|
|
1200
1271
|
if (recordOrigin !== "top-level") {
|
|
1201
1272
|
if (!recordRef.absolute) {
|
|
1202
1273
|
// Traverse the chain of ancestors from most nested to top-level.
|
|
@@ -1269,12 +1340,12 @@ class TypeResolver {
|
|
|
1269
1340
|
return undefined;
|
|
1270
1341
|
}
|
|
1271
1342
|
const newModuleResult = modules.get(newModulePath);
|
|
1272
|
-
if (newModuleResult
|
|
1343
|
+
if (!newModuleResult) {
|
|
1273
1344
|
// The module was not found or has errors: an error was already
|
|
1274
1345
|
// registered, no need to register a new one.
|
|
1275
1346
|
return undefined;
|
|
1276
1347
|
}
|
|
1277
|
-
const newModule = newModuleResult.result;
|
|
1348
|
+
const newModule = newModuleResult.module.result;
|
|
1278
1349
|
if (newIt.kind === "import") {
|
|
1279
1350
|
newIt = newModule.nameToDeclaration[name];
|
|
1280
1351
|
if (!newIt) {
|
|
@@ -1322,6 +1393,145 @@ class TypeResolver {
|
|
|
1322
1393
|
}
|
|
1323
1394
|
}
|
|
1324
1395
|
|
|
1396
|
+
type ModuleCacheResult =
|
|
1397
|
+
| {
|
|
1398
|
+
kind: "no-cache";
|
|
1399
|
+
}
|
|
1400
|
+
| {
|
|
1401
|
+
kind: "module-tokens";
|
|
1402
|
+
tokens: Result<ModuleTokens>;
|
|
1403
|
+
}
|
|
1404
|
+
| {
|
|
1405
|
+
kind: "module-bundle";
|
|
1406
|
+
bundle: ModuleBundle;
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
class Cache {
|
|
1410
|
+
private readonly modulePathToCacheResult = new Map<
|
|
1411
|
+
string,
|
|
1412
|
+
ModuleCacheResult
|
|
1413
|
+
>();
|
|
1414
|
+
|
|
1415
|
+
constructor(
|
|
1416
|
+
modulePathToNewContent: ReadonlyMap<string, string>,
|
|
1417
|
+
private readonly modulePathToOldBundle: ReadonlyMap<string, ModuleBundle>,
|
|
1418
|
+
) {
|
|
1419
|
+
const unchangedModulePaths = new Set<string>();
|
|
1420
|
+
for (const [modulePath, newContent] of modulePathToNewContent) {
|
|
1421
|
+
const oldBundle = modulePathToOldBundle.get(modulePath);
|
|
1422
|
+
if (oldBundle?.tokens.result.sourceCode === newContent) {
|
|
1423
|
+
unchangedModulePaths.add(modulePath);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
const classify = (modulePath: string): ModuleCacheResult => {
|
|
1427
|
+
const oldBundle = modulePathToOldBundle.get(modulePath);
|
|
1428
|
+
if (!oldBundle) {
|
|
1429
|
+
return { kind: "no-cache" };
|
|
1430
|
+
}
|
|
1431
|
+
{
|
|
1432
|
+
const inMap = this.modulePathToCacheResult.get(modulePath);
|
|
1433
|
+
if (inMap) {
|
|
1434
|
+
return inMap;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
let result: ModuleCacheResult;
|
|
1438
|
+
const newContent = modulePathToNewContent.get(modulePath);
|
|
1439
|
+
if (newContent === oldBundle.tokens.result.sourceCode) {
|
|
1440
|
+
// Assume best case, may downgrade later.
|
|
1441
|
+
this.modulePathToCacheResult.set(modulePath, {
|
|
1442
|
+
kind: "module-bundle",
|
|
1443
|
+
bundle: oldBundle,
|
|
1444
|
+
});
|
|
1445
|
+
const directDependencies = Object.keys(
|
|
1446
|
+
oldBundle.module.result?.pathToImportedNames,
|
|
1447
|
+
);
|
|
1448
|
+
result = directDependencies.every(
|
|
1449
|
+
(dep) => classify(dep).kind === "module-bundle",
|
|
1450
|
+
)
|
|
1451
|
+
? { kind: "module-bundle", bundle: oldBundle }
|
|
1452
|
+
: { kind: "module-tokens", tokens: oldBundle.tokens };
|
|
1453
|
+
} else {
|
|
1454
|
+
result = { kind: "no-cache" };
|
|
1455
|
+
}
|
|
1456
|
+
this.modulePathToCacheResult.set(modulePath, result);
|
|
1457
|
+
return result;
|
|
1458
|
+
};
|
|
1459
|
+
for (const modulePath of modulePathToOldBundle.keys()) {
|
|
1460
|
+
classify(modulePath);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
getModuleCacheResult(modulePath: string): ModuleCacheResult {
|
|
1465
|
+
return this.modulePathToCacheResult.get(modulePath) ?? { kind: "no-cache" };
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/** Registry of declarations possibly across multiple modules. */
|
|
1470
|
+
class DeclarationRegistry {
|
|
1471
|
+
readonly recordMap = new Map<RecordKey, RecordLocation>();
|
|
1472
|
+
readonly numberToRecords = new Map<number, RecordKey[]>();
|
|
1473
|
+
readonly numberToMethods = new Map<number, Method[]>();
|
|
1474
|
+
|
|
1475
|
+
mergeFrom(other: DeclarationRegistry): void {
|
|
1476
|
+
for (const [key, value] of other.recordMap) {
|
|
1477
|
+
this.recordMap.set(key, value);
|
|
1478
|
+
}
|
|
1479
|
+
for (const [number, value] of other.numberToRecords) {
|
|
1480
|
+
let existing = this.numberToRecords.get(number);
|
|
1481
|
+
if (!existing) {
|
|
1482
|
+
existing = [];
|
|
1483
|
+
this.numberToRecords.set(number, existing);
|
|
1484
|
+
}
|
|
1485
|
+
existing.push(...value);
|
|
1486
|
+
}
|
|
1487
|
+
for (const [number, value] of other.numberToMethods) {
|
|
1488
|
+
let existing = this.numberToMethods.get(number);
|
|
1489
|
+
if (!existing) {
|
|
1490
|
+
existing = [];
|
|
1491
|
+
this.numberToMethods.set(number, existing);
|
|
1492
|
+
}
|
|
1493
|
+
existing.push(...value);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
pushNumberRecord(number: number, record: RecordKey): void {
|
|
1498
|
+
let value = this.numberToRecords.get(number);
|
|
1499
|
+
if (!value) {
|
|
1500
|
+
value = [];
|
|
1501
|
+
this.numberToRecords.set(number, value);
|
|
1502
|
+
}
|
|
1503
|
+
value.push(record);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
pushNumberMethod(number: number, method: Method): void {
|
|
1507
|
+
let value = this.numberToMethods.get(number);
|
|
1508
|
+
if (!value) {
|
|
1509
|
+
value = [];
|
|
1510
|
+
this.numberToMethods.set(number, value);
|
|
1511
|
+
}
|
|
1512
|
+
value.push(method);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
class ModuleBundle {
|
|
1517
|
+
constructor(
|
|
1518
|
+
readonly tokens: Result<ModuleTokens>,
|
|
1519
|
+
readonly module: Result<Module>,
|
|
1520
|
+
) {}
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Registry of declarations found in this module only.
|
|
1524
|
+
* Will be merged into the "global" registry.
|
|
1525
|
+
*/
|
|
1526
|
+
readonly registry = new DeclarationRegistry();
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
interface FinalizationResult {
|
|
1530
|
+
readonly modules: ReadonlyMap<string, Result<Module>>;
|
|
1531
|
+
/** Errors aggregated across all modules. */
|
|
1532
|
+
readonly errors: readonly SkirError[];
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1325
1535
|
function ensureAllImportsAreUsed(
|
|
1326
1536
|
module: Module,
|
|
1327
1537
|
usedImports: Set<string>,
|
|
@@ -1348,55 +1558,94 @@ function ensureAllImportsAreUsed(
|
|
|
1348
1558
|
}
|
|
1349
1559
|
}
|
|
1350
1560
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1561
|
+
/**
|
|
1562
|
+
* Returns suggested module paths for import auto-completion.
|
|
1563
|
+
*
|
|
1564
|
+
* Given the partial path the user has typed, returns suggestions from
|
|
1565
|
+
* `modulePathToContent`. For paths where there is more after the matched
|
|
1566
|
+
* segment, the suggestion is truncated at the next "/" and a trailing "/" is
|
|
1567
|
+
* appended to signal that it is a directory, not a file.
|
|
1568
|
+
*
|
|
1569
|
+
* Handles both absolute paths (e.g. "bb/ee") and relative paths (e.g.
|
|
1570
|
+
* "./other", "../sibling").
|
|
1571
|
+
*/
|
|
1572
|
+
function suggestModulePaths(
|
|
1573
|
+
typedPath: string,
|
|
1574
|
+
originModulePath: string,
|
|
1575
|
+
modulePathToContent: ReadonlyMap<string, string>,
|
|
1576
|
+
): ReadonlyArray<{ readonly name: string }> {
|
|
1577
|
+
const isRelative = typedPath.startsWith("./") || typedPath.startsWith("../");
|
|
1578
|
+
|
|
1579
|
+
// Compute the absolute path prefix to match against all module paths.
|
|
1580
|
+
let absolutePrefix: string;
|
|
1581
|
+
if (isRelative) {
|
|
1582
|
+
// Split at the last "/" so that the directory component (fully typed) can
|
|
1583
|
+
// be resolved cleanly, while the tail (partial filename/dir prefix) is
|
|
1584
|
+
// appended afterwards.
|
|
1585
|
+
const lastSlash = typedPath.lastIndexOf("/");
|
|
1586
|
+
const dirComponent = typedPath.slice(0, lastSlash + 1);
|
|
1587
|
+
const filePrefix = typedPath.slice(lastSlash + 1);
|
|
1588
|
+
// Append a dummy filename so Paths.join/normalize treat the directory
|
|
1589
|
+
// component as a file path (avoids trailing-slash edge-cases), then strip
|
|
1590
|
+
// it off again.
|
|
1591
|
+
const dummy = dirComponent + "__dummy__";
|
|
1592
|
+
const resolvedDummy = Paths.join(
|
|
1593
|
+
Paths.dirname(originModulePath),
|
|
1594
|
+
dummy.startsWith("./") ? dummy.slice(2) : dummy,
|
|
1595
|
+
).replace(/\\/g, "/");
|
|
1596
|
+
const absoluteDir = resolvedDummy.slice(
|
|
1597
|
+
0,
|
|
1598
|
+
resolvedDummy.length - "__dummy__".length,
|
|
1599
|
+
);
|
|
1600
|
+
absolutePrefix = absoluteDir + filePrefix;
|
|
1601
|
+
} else if (originModulePath.startsWith("@") && !typedPath.startsWith("@")) {
|
|
1602
|
+
// Mirror the package-prefix logic from resolveModulePath.
|
|
1603
|
+
absolutePrefix = extractPackagePrefix(originModulePath) + typedPath;
|
|
1604
|
+
} else {
|
|
1605
|
+
absolutePrefix = typedPath;
|
|
1385
1606
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1607
|
+
absolutePrefix = absolutePrefix.replace(/\\/g, "/");
|
|
1608
|
+
if (absolutePrefix.startsWith("../")) {
|
|
1609
|
+
// Typed path escapes the root; no valid suggestions.
|
|
1610
|
+
return [];
|
|
1389
1611
|
}
|
|
1390
|
-
}
|
|
1391
1612
|
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1613
|
+
const originBaseDir = Paths.dirname(originModulePath).replace(/\\/g, "/");
|
|
1614
|
+
const suggestions = new Set<string>();
|
|
1615
|
+
|
|
1616
|
+
for (const path of modulePathToContent.keys()) {
|
|
1617
|
+
if (!path.startsWith(absolutePrefix)) continue;
|
|
1618
|
+
const remaining = path.slice(absolutePrefix.length);
|
|
1619
|
+
const slashIndex = remaining.indexOf("/");
|
|
1620
|
+
// If there is a sub-path after the matched prefix, collapse to the next
|
|
1621
|
+
// directory segment and append "/". Otherwise use the full path.
|
|
1622
|
+
const absoluteSuggestion =
|
|
1623
|
+
slashIndex >= 0
|
|
1624
|
+
? absolutePrefix + remaining.slice(0, slashIndex + 1)
|
|
1625
|
+
: path;
|
|
1626
|
+
|
|
1627
|
+
if (isRelative) {
|
|
1628
|
+
const isDir = absoluteSuggestion.endsWith("/");
|
|
1629
|
+
const absPath = isDir
|
|
1630
|
+
? absoluteSuggestion.slice(0, -1)
|
|
1631
|
+
: absoluteSuggestion;
|
|
1632
|
+
let rel = Paths.relative(originBaseDir, absPath).replace(/\\/g, "/");
|
|
1633
|
+
// Paths.relative returns "" when both paths are the same; normalise to
|
|
1634
|
+
// "." so the subsequent prefix-check and directory-slash logic work
|
|
1635
|
+
// correctly (avoids producing the invalid path ".//" for same-dir cases).
|
|
1636
|
+
if (rel === "") {
|
|
1637
|
+
rel = ".";
|
|
1638
|
+
}
|
|
1639
|
+
if (!rel.startsWith(".")) {
|
|
1640
|
+
rel = "./" + rel;
|
|
1641
|
+
}
|
|
1642
|
+
suggestions.add(isDir ? rel + "/" : rel);
|
|
1643
|
+
} else {
|
|
1644
|
+
suggestions.add(absoluteSuggestion);
|
|
1645
|
+
}
|
|
1395
1646
|
}
|
|
1396
1647
|
|
|
1397
|
-
|
|
1398
|
-
return this.moduleMap.get(modulePath);
|
|
1399
|
-
}
|
|
1648
|
+
return [...suggestions].map((name) => ({ name }));
|
|
1400
1649
|
}
|
|
1401
1650
|
|
|
1402
1651
|
function resolveModulePath(
|