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