skir 1.2.5 → 1.2.6
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/casing.d.ts +5 -0
- package/dist/casing.js +23 -0
- package/dist/casing.js.map +1 -0
- package/dist/command_line_parser.d.ts +36 -0
- package/dist/command_line_parser.d.ts.map +1 -0
- package/dist/command_line_parser.js +240 -0
- package/dist/command_line_parser.js.map +1 -0
- package/dist/compatibility_checker.d.ts +74 -0
- package/dist/compatibility_checker.js +331 -0
- package/dist/compatibility_checker.js.map +1 -0
- package/dist/compiler.d.ts +3 -0
- package/dist/compiler.js +406 -0
- package/dist/compiler.js.map +1 -0
- package/dist/completion_helper.d.ts +27 -0
- package/dist/completion_helper.js +101 -0
- package/dist/completion_helper.js.map +1 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -0
- package/dist/config_parser.d.ts +34 -0
- package/dist/config_parser.js +217 -0
- package/dist/config_parser.js.map +1 -0
- package/dist/definition_finder.d.ts +16 -0
- package/dist/definition_finder.js +375 -0
- package/dist/definition_finder.js.map +1 -0
- package/dist/dependency_manager.d.ts +14 -0
- package/dist/dependency_manager.js +109 -0
- package/dist/dependency_manager.js.map +1 -0
- package/dist/doc_comment_parser.d.ts +6 -0
- package/dist/doc_comment_parser.js +269 -0
- package/dist/doc_comment_parser.js.map +1 -0
- package/dist/doc_reference_resolver.d.ts +5 -0
- package/dist/doc_reference_resolver.js +232 -0
- package/dist/doc_reference_resolver.js.map +1 -0
- package/dist/error_renderer.d.ts +24 -0
- package/dist/error_renderer.js +326 -0
- package/dist/error_renderer.js.map +1 -0
- package/dist/exit_error.d.ts +8 -0
- package/dist/exit_error.d.ts.map +1 -0
- package/dist/exit_error.js +8 -0
- package/dist/exit_error.js.map +1 -0
- package/dist/expected_names.d.ts +11 -0
- package/dist/expected_names.js +34 -0
- package/dist/expected_names.js.map +1 -0
- package/dist/formatter.d.ts +28 -0
- package/dist/formatter.js +338 -0
- package/dist/formatter.js.map +1 -0
- package/dist/get_dependencies_flow.d.ts +40 -0
- package/dist/get_dependencies_flow.js +177 -0
- package/dist/get_dependencies_flow.js.map +1 -0
- package/dist/import_block_formatter.d.ts +3 -0
- package/dist/import_block_formatter.js +36 -0
- package/dist/import_block_formatter.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/io.d.ts +23 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +58 -0
- package/dist/io.js.map +1 -0
- package/dist/literals.d.ts +12 -0
- package/dist/literals.js +96 -0
- package/dist/literals.js.map +1 -0
- package/dist/module_collector.d.ts +9 -0
- package/dist/module_collector.js +73 -0
- package/dist/module_collector.js.map +1 -0
- package/dist/module_set.d.ts +51 -0
- package/dist/module_set.js +1331 -0
- package/dist/module_set.js.map +1 -0
- package/dist/package_downloader.d.ts +4 -0
- package/dist/package_downloader.js +256 -0
- package/dist/package_downloader.js.map +1 -0
- package/dist/package_types.d.ts +30 -0
- package/dist/package_types.d.ts.map +1 -0
- package/dist/package_types.js +2 -0
- package/dist/package_types.js.map +1 -0
- package/dist/parser.d.ts +7 -0
- package/dist/parser.js +1335 -0
- package/dist/parser.js.map +1 -0
- package/dist/project_initializer.d.ts +2 -0
- package/dist/project_initializer.d.ts.map +1 -0
- package/dist/project_initializer.js +207 -0
- package/dist/project_initializer.js.map +1 -0
- package/dist/snapshotter.d.ts +16 -0
- package/dist/snapshotter.js +263 -0
- package/dist/snapshotter.js.map +1 -0
- package/dist/tokenizer.d.ts +18 -0
- package/dist/tokenizer.js +244 -0
- package/dist/tokenizer.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1331 @@
|
|
|
1
|
+
import { unquoteAndUnescape, } from "skir-internal";
|
|
2
|
+
import { resolveDocReferences } from "./doc_reference_resolver.js";
|
|
3
|
+
import { declarationsToExpectedNames, ExpectedNamesCollector, } from "./expected_names.js";
|
|
4
|
+
import { isStringLiteral, literalValueToDenseJson, literalValueToIdentity, valueHasPrimitiveType, } from "./literals.js";
|
|
5
|
+
import { extractPackagePrefix, parseModule } from "./parser.js";
|
|
6
|
+
import { tokenizeModule } from "./tokenizer.js";
|
|
7
|
+
/**
|
|
8
|
+
* Result of compiling a set of modules. Immutable.
|
|
9
|
+
*
|
|
10
|
+
* Support incremental compilation by accepting an optional cache of the previous
|
|
11
|
+
* module set. This cache can be used to avoid re-parsing and re-resolving
|
|
12
|
+
* modules that haven't changed since the last compilation.
|
|
13
|
+
*/
|
|
14
|
+
export class ModuleSet {
|
|
15
|
+
static compile(modulePathToContent, cache, parseMode = "strict") {
|
|
16
|
+
return new ModuleSet(modulePathToContent, cache, parseMode);
|
|
17
|
+
}
|
|
18
|
+
static compileForCompletion(currentModulePath, currentPosition, modulePathToContent, cache) {
|
|
19
|
+
if (!modulePathToContent.has(currentModulePath)) {
|
|
20
|
+
throw new Error(`Not found: ${currentModulePath}`);
|
|
21
|
+
}
|
|
22
|
+
const moduleSet = new ModuleSet(modulePathToContent, cache, "lenient", {
|
|
23
|
+
modulePath: currentModulePath,
|
|
24
|
+
position: currentPosition,
|
|
25
|
+
});
|
|
26
|
+
return moduleSet.modules.get(currentModulePath);
|
|
27
|
+
}
|
|
28
|
+
constructor(modulePathToContent, cache, parseMode, completionMode) {
|
|
29
|
+
this.modulePathToContent = modulePathToContent;
|
|
30
|
+
this.parseMode = parseMode;
|
|
31
|
+
this.completionMode = completionMode;
|
|
32
|
+
this.moduleBundles = new Map();
|
|
33
|
+
this.registry = new DeclarationRegistry();
|
|
34
|
+
this.cache = cache
|
|
35
|
+
? new Cache(modulePathToContent, cache.moduleBundles, cache.registry)
|
|
36
|
+
: undefined;
|
|
37
|
+
// In completion mode, no need to recompile modules which are not dependencies of
|
|
38
|
+
// the current module.
|
|
39
|
+
const modulePaths = completionMode
|
|
40
|
+
? [completionMode.modulePath]
|
|
41
|
+
: modulePathToContent.keys();
|
|
42
|
+
for (const modulePath of modulePaths) {
|
|
43
|
+
this.parseAndResolve(modulePath, new Set());
|
|
44
|
+
}
|
|
45
|
+
this.finalizationResult = this.finalize();
|
|
46
|
+
// So it can be garbage collected.
|
|
47
|
+
this.cache = undefined;
|
|
48
|
+
}
|
|
49
|
+
parseAndResolve(modulePath, inProgressSet) {
|
|
50
|
+
const inMap = this.moduleBundles.get(modulePath);
|
|
51
|
+
if (inMap !== undefined) {
|
|
52
|
+
return inMap;
|
|
53
|
+
}
|
|
54
|
+
const result = this.doParseAndResolve(modulePath, inProgressSet);
|
|
55
|
+
if (result) {
|
|
56
|
+
this.moduleBundles.set(modulePath, result);
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
/** Called by `parseAndResolve` when the module is not in the map already. */
|
|
61
|
+
doParseAndResolve(modulePath, inProgressSet) {
|
|
62
|
+
const moduleContent = this.modulePathToContent.get(modulePath);
|
|
63
|
+
if (moduleContent === undefined) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
let moduleTokens;
|
|
67
|
+
{
|
|
68
|
+
let moduleCacheResult;
|
|
69
|
+
if (modulePath === this.completionMode?.modulePath) {
|
|
70
|
+
moduleCacheResult = { kind: "no-cache" };
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
moduleCacheResult = this.cache?.getModuleCacheResult(modulePath) ?? {
|
|
74
|
+
kind: "no-cache",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
switch (moduleCacheResult.kind) {
|
|
78
|
+
case "no-cache": {
|
|
79
|
+
moduleTokens = tokenizeModule(moduleContent, modulePath, this.completionMode);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case "module-tokens": {
|
|
83
|
+
moduleTokens = moduleCacheResult.tokens;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case "module-bundle": {
|
|
87
|
+
this.registry.mergeFrom(moduleCacheResult.bundle.registry);
|
|
88
|
+
return moduleCacheResult.bundle;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
let module;
|
|
93
|
+
const errors = [];
|
|
94
|
+
{
|
|
95
|
+
const parseResult = parseModule(moduleTokens.result, this.parseMode);
|
|
96
|
+
errors.push(...parseResult.errors);
|
|
97
|
+
module = parseResult.result;
|
|
98
|
+
}
|
|
99
|
+
const moduleBundle = new ModuleBundle(moduleTokens, {
|
|
100
|
+
result: module,
|
|
101
|
+
errors: errors,
|
|
102
|
+
});
|
|
103
|
+
// Process all imports.
|
|
104
|
+
for (const declaration of module.declarations) {
|
|
105
|
+
if (declaration.kind !== "import" &&
|
|
106
|
+
declaration.kind !== "import-alias") {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const otherModulePath = declaration.resolvedModulePath;
|
|
110
|
+
if (otherModulePath === undefined) {
|
|
111
|
+
// An error was already registered.
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
// Add the imported module to the module set.
|
|
115
|
+
const circularDependencyMessage = "Circular dependency between modules";
|
|
116
|
+
if (inProgressSet.has(modulePath)) {
|
|
117
|
+
errors.push({
|
|
118
|
+
token: declaration.modulePath,
|
|
119
|
+
message: circularDependencyMessage,
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
inProgressSet.add(modulePath);
|
|
124
|
+
const otherModule = this.parseAndResolve(otherModulePath, inProgressSet);
|
|
125
|
+
inProgressSet.delete(modulePath);
|
|
126
|
+
if (otherModule === null) {
|
|
127
|
+
errors.push({
|
|
128
|
+
token: declaration.modulePath,
|
|
129
|
+
message: "Module not found",
|
|
130
|
+
expectedNames: suggestModulePaths(unquoteAndUnescape(declaration.modulePath.text), modulePath, this.modulePathToContent.keys()),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else if (otherModule.tokens.errors.length !== 0 ||
|
|
134
|
+
otherModule.module.errors.length !== 0) {
|
|
135
|
+
const hasCircularDependency = otherModule.module.errors.some((e) => e.message === circularDependencyMessage);
|
|
136
|
+
if (hasCircularDependency) {
|
|
137
|
+
errors.push({
|
|
138
|
+
token: declaration.modulePath,
|
|
139
|
+
message: circularDependencyMessage,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
errors.push({
|
|
144
|
+
token: declaration.modulePath,
|
|
145
|
+
message: "Imported module has errors",
|
|
146
|
+
errorIsInOtherModule: true,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (declaration.kind === "import") {
|
|
151
|
+
// Make sure that the symbols we are importing exist in the imported
|
|
152
|
+
// module and are not imported symbols themselves.
|
|
153
|
+
for (const importedName of declaration.importedNames) {
|
|
154
|
+
const importedDeclaration = otherModule.module.result.nameToDeclaration[importedName.text];
|
|
155
|
+
if (importedDeclaration === undefined) {
|
|
156
|
+
errors.push({
|
|
157
|
+
token: importedName,
|
|
158
|
+
message: "Not found",
|
|
159
|
+
expectedNames: declarationsToExpectedNames(otherModule.module.result.nameToDeclaration, (d) => d.kind === "record"),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
else if (importedDeclaration.kind === "import") {
|
|
163
|
+
errors.push({
|
|
164
|
+
token: importedName,
|
|
165
|
+
message: "Cannot reimport imported record",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
else if (importedDeclaration.kind !== "record") {
|
|
169
|
+
errors.push({
|
|
170
|
+
token: importedName,
|
|
171
|
+
message: "Not a record",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (errors.length && !this.completionMode) {
|
|
178
|
+
return moduleBundle;
|
|
179
|
+
}
|
|
180
|
+
// We can't merge these 3 loops into a single one, each operation must run
|
|
181
|
+
// after the last operation ran on the whole map.
|
|
182
|
+
// Loop 1: merge the module records map into the cross-module record map.
|
|
183
|
+
for (const record of module.records) {
|
|
184
|
+
const { key } = record.record;
|
|
185
|
+
this.registry.recordMap.set(key, record);
|
|
186
|
+
moduleBundle.registry.recordMap.set(key, record);
|
|
187
|
+
const { recordNumber } = record.record;
|
|
188
|
+
if (recordNumber != null && !modulePath.startsWith("@")) {
|
|
189
|
+
moduleBundle.registry.pushNumberRecord(recordNumber, key);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Loop 2: resolve every field type of every record in the module.
|
|
193
|
+
// Store the result in the Field object.
|
|
194
|
+
const usedImports = new Set();
|
|
195
|
+
const typeResolver = new TypeResolver(module, this.moduleBundles, this.completionMode
|
|
196
|
+
? this.cache?.registry.topLevelNameToRecordLocations
|
|
197
|
+
: undefined, usedImports, errors);
|
|
198
|
+
for (const record of module.records) {
|
|
199
|
+
this.storeResolvedFieldTypes(record, typeResolver);
|
|
200
|
+
}
|
|
201
|
+
// Loop 3: once all the types of record fields have been resolved.
|
|
202
|
+
const doResolveDocReferences = (documentee) => resolveDocReferences(documentee, module, (p) => this.moduleBundles.get(p)?.module.result, this.recordMap, errors);
|
|
203
|
+
for (const moduleRecord of module.records) {
|
|
204
|
+
const { record } = moduleRecord;
|
|
205
|
+
// For every field, determine if the field is recursive, i.e. the field
|
|
206
|
+
// type depends on the record where the field is defined.
|
|
207
|
+
// Store the result in the Field object.
|
|
208
|
+
this.storeFieldRecursivity(record);
|
|
209
|
+
// Verify that the `key` field of every array type is valid.
|
|
210
|
+
for (const field of record.fields) {
|
|
211
|
+
const { type } = field;
|
|
212
|
+
if (type) {
|
|
213
|
+
this.validateArrayKeys(type, errors);
|
|
214
|
+
}
|
|
215
|
+
// Resolve the references in the doc comments of the field.
|
|
216
|
+
doResolveDocReferences({
|
|
217
|
+
kind: "field",
|
|
218
|
+
field: field,
|
|
219
|
+
record: record,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
// Resolve the references in the doc comments of the record.
|
|
223
|
+
doResolveDocReferences(record);
|
|
224
|
+
}
|
|
225
|
+
const resolveTopLevelTypeAndValidate = (type) => {
|
|
226
|
+
const resolvedType = typeResolver.resolve(type, "top-level");
|
|
227
|
+
if (resolvedType) {
|
|
228
|
+
this.validateArrayKeys(resolvedType, errors);
|
|
229
|
+
}
|
|
230
|
+
return resolvedType;
|
|
231
|
+
};
|
|
232
|
+
// Resolve every request/response type of every method in the module.
|
|
233
|
+
// Store the result in the Method object.
|
|
234
|
+
for (const method of module.methods) {
|
|
235
|
+
const { unresolvedRequestType, unresolvedResponseType } = method;
|
|
236
|
+
// Resolve request type.
|
|
237
|
+
method.requestType = resolveTopLevelTypeAndValidate(unresolvedRequestType);
|
|
238
|
+
// Resolve response type.
|
|
239
|
+
method.responseType = resolveTopLevelTypeAndValidate(unresolvedResponseType);
|
|
240
|
+
const { number } = method;
|
|
241
|
+
if (!modulePath.startsWith("@")) {
|
|
242
|
+
moduleBundle.registry.pushNumberMethod(number, method);
|
|
243
|
+
}
|
|
244
|
+
// Resolve the references in the doc comments of the method.
|
|
245
|
+
doResolveDocReferences(method);
|
|
246
|
+
}
|
|
247
|
+
for (const method of module.brokenMethods) {
|
|
248
|
+
const { unresolvedRequestType, unresolvedResponseType } = method;
|
|
249
|
+
resolveTopLevelTypeAndValidate(unresolvedRequestType);
|
|
250
|
+
if (unresolvedResponseType) {
|
|
251
|
+
resolveTopLevelTypeAndValidate(unresolvedResponseType);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Resolve every constant type. Store the result in the constant object.
|
|
255
|
+
for (const constant of module.constants) {
|
|
256
|
+
const { unresolvedType } = constant;
|
|
257
|
+
const type = resolveTopLevelTypeAndValidate(unresolvedType);
|
|
258
|
+
constant.type = type;
|
|
259
|
+
if (type) {
|
|
260
|
+
constant.valueAsDenseJson = //
|
|
261
|
+
this.valueToDenseJson(constant.value, type, errors);
|
|
262
|
+
}
|
|
263
|
+
// Resolve the references in the doc comments of the constant.
|
|
264
|
+
doResolveDocReferences(constant);
|
|
265
|
+
}
|
|
266
|
+
for (const constant of module.brokenConstants) {
|
|
267
|
+
const { unresolvedType } = constant;
|
|
268
|
+
resolveTopLevelTypeAndValidate(unresolvedType);
|
|
269
|
+
}
|
|
270
|
+
moduleBundle.registry.pushTopLevelNames(module.declarations
|
|
271
|
+
.filter((d) => d.kind === "record")
|
|
272
|
+
.map((d) => ({
|
|
273
|
+
name: d.name.text,
|
|
274
|
+
doc: d.doc,
|
|
275
|
+
modulePath: modulePath,
|
|
276
|
+
})));
|
|
277
|
+
ensureAllImportsAreUsed(module, usedImports, errors);
|
|
278
|
+
this.registry.mergeFrom(moduleBundle.registry);
|
|
279
|
+
return moduleBundle;
|
|
280
|
+
}
|
|
281
|
+
storeResolvedFieldTypes(record, typeResolver) {
|
|
282
|
+
for (const field of record.record.fields) {
|
|
283
|
+
if (field.unresolvedType === undefined) {
|
|
284
|
+
// A constant enum field.
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
field.type = typeResolver.resolve(field.unresolvedType, record);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
storeFieldRecursivity(record) {
|
|
291
|
+
for (const field of record.fields) {
|
|
292
|
+
if (!field.type)
|
|
293
|
+
continue;
|
|
294
|
+
const modes = record.recordType === "struct"
|
|
295
|
+
? ["hard", "via-optional", "soft"]
|
|
296
|
+
: ["soft"];
|
|
297
|
+
for (const mode of modes) {
|
|
298
|
+
const deps = new Set();
|
|
299
|
+
this.collectTypeDeps(field.type, mode, deps);
|
|
300
|
+
if (deps.has(record.key)) {
|
|
301
|
+
field.isRecursive = mode;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
collectTypeDeps(input, mode, out) {
|
|
308
|
+
switch (input.kind) {
|
|
309
|
+
case "record": {
|
|
310
|
+
if (mode === "via-optional")
|
|
311
|
+
return;
|
|
312
|
+
const { key } = input;
|
|
313
|
+
if (out.has(key))
|
|
314
|
+
return;
|
|
315
|
+
out.add(key);
|
|
316
|
+
// Recursively add deps of all fields of the record.
|
|
317
|
+
const record = this.recordMap.get(key)?.record;
|
|
318
|
+
if (!record) {
|
|
319
|
+
console.error("collectTypeDeps: record not found", key);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (mode === "hard" && record.recordType === "enum") {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
for (const field of record.fields) {
|
|
326
|
+
if (field.type === undefined)
|
|
327
|
+
continue;
|
|
328
|
+
this.collectTypeDeps(field.type, mode, out);
|
|
329
|
+
}
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case "array": {
|
|
333
|
+
if (mode === "soft") {
|
|
334
|
+
this.collectTypeDeps(input.item, mode, out);
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
case "optional": {
|
|
339
|
+
switch (mode) {
|
|
340
|
+
case "soft": {
|
|
341
|
+
this.collectTypeDeps(input.other, mode, out);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
case "via-optional": {
|
|
345
|
+
// Important: must pass "hard" here.
|
|
346
|
+
this.collectTypeDeps(input.other, "hard", out);
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
default: {
|
|
350
|
+
const _ = mode;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Verifies that the `key` field of every array type found in `topLevelType`
|
|
359
|
+
* is valid. Populates the `keyType` field of every field path.
|
|
360
|
+
*/
|
|
361
|
+
validateArrayKeys(topLevelType, errors) {
|
|
362
|
+
const validate = (type) => {
|
|
363
|
+
const { key, item } = type;
|
|
364
|
+
if (!key) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const { path } = key;
|
|
368
|
+
// Iterate the fields in the sequence.
|
|
369
|
+
let currentType = item;
|
|
370
|
+
let enumRef;
|
|
371
|
+
for (let i = 0; i < path.length; ++i) {
|
|
372
|
+
const pathItem = path[i];
|
|
373
|
+
const fieldName = pathItem.name;
|
|
374
|
+
if (currentType.kind !== "record") {
|
|
375
|
+
if (i === 0) {
|
|
376
|
+
errors.push({
|
|
377
|
+
token: key.pipeToken,
|
|
378
|
+
message: "Item must have struct type",
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
const previousFieldName = path[i - 1].name;
|
|
383
|
+
errors.push({
|
|
384
|
+
token: previousFieldName,
|
|
385
|
+
message: "Must have struct type",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const record = this.recordMap.get(currentType.key)?.record;
|
|
391
|
+
if (!record) {
|
|
392
|
+
console.error("validateArrayKeys: record not found", currentType.key);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (record.recordType === "struct") {
|
|
396
|
+
const field = record.nameToDeclaration[fieldName.text];
|
|
397
|
+
if (field?.kind !== "field") {
|
|
398
|
+
errors.push({
|
|
399
|
+
token: fieldName,
|
|
400
|
+
message: `Field not found in struct ${record.name.text}`,
|
|
401
|
+
expectedNames: declarationsToExpectedNames(record.nameToDeclaration, (d) => d.kind === "field"),
|
|
402
|
+
});
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
pathItem.declaration = field;
|
|
406
|
+
if (!field.type) {
|
|
407
|
+
// An error was already registered.
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
currentType = field.type;
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
// An enum.
|
|
414
|
+
if (fieldName.text !== "kind") {
|
|
415
|
+
errors.push({
|
|
416
|
+
token: fieldName,
|
|
417
|
+
expected: "'kind'",
|
|
418
|
+
expectedNames: [{ name: "kind" }],
|
|
419
|
+
});
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
enumRef = currentType;
|
|
423
|
+
currentType = {
|
|
424
|
+
kind: "primitive",
|
|
425
|
+
primitive: "string",
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (currentType.kind !== "primitive") {
|
|
430
|
+
errors.push({
|
|
431
|
+
token: path.at(-1).name,
|
|
432
|
+
message: "Does not have primitive type",
|
|
433
|
+
});
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
// If the last field name of the `kind` field of an enum, we store a
|
|
437
|
+
// reference to the enum in the `keyType` field of the array type.
|
|
438
|
+
key.keyType = enumRef || currentType;
|
|
439
|
+
};
|
|
440
|
+
const traverseType = (type) => {
|
|
441
|
+
switch (type.kind) {
|
|
442
|
+
case "array":
|
|
443
|
+
validate(type);
|
|
444
|
+
return traverseType(type.item);
|
|
445
|
+
case "optional":
|
|
446
|
+
return traverseType(type.other);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
traverseType(topLevelType);
|
|
450
|
+
}
|
|
451
|
+
valueToDenseJson(value, expectedType, errors) {
|
|
452
|
+
switch (expectedType.kind) {
|
|
453
|
+
case "optional": {
|
|
454
|
+
if (value.kind === "literal" && value.token.text === "null") {
|
|
455
|
+
value.type = { kind: "null" };
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
return this.valueToDenseJson(value, expectedType.other, errors);
|
|
459
|
+
}
|
|
460
|
+
case "array": {
|
|
461
|
+
if (value.kind !== "array") {
|
|
462
|
+
errors.push({
|
|
463
|
+
token: value.token,
|
|
464
|
+
expected: "array",
|
|
465
|
+
});
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
const json = [];
|
|
469
|
+
let allGood = true;
|
|
470
|
+
for (const item of value.items) {
|
|
471
|
+
const itemJson = //
|
|
472
|
+
this.valueToDenseJson(item, expectedType.item, errors);
|
|
473
|
+
if (itemJson !== undefined) {
|
|
474
|
+
json.push(itemJson);
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
// Even if we could return now, better to verify the type of the
|
|
478
|
+
// other items.
|
|
479
|
+
allGood = false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (!allGood) {
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
const { key } = expectedType;
|
|
486
|
+
value.key = key;
|
|
487
|
+
if (key) {
|
|
488
|
+
validateKeyedItems(value.items, key, errors);
|
|
489
|
+
}
|
|
490
|
+
return json;
|
|
491
|
+
}
|
|
492
|
+
case "record": {
|
|
493
|
+
const record = this.recordMap.get(expectedType.key);
|
|
494
|
+
if (!record) {
|
|
495
|
+
// An error was already registered.
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
498
|
+
return record.record.recordType === "struct"
|
|
499
|
+
? this.structValueToDenseJson(value, record.record, errors)
|
|
500
|
+
: this.enumValueToDenseJson(value, record.record, errors);
|
|
501
|
+
}
|
|
502
|
+
case "primitive": {
|
|
503
|
+
const { token } = value;
|
|
504
|
+
const { primitive } = expectedType;
|
|
505
|
+
if (value.kind !== "literal" ||
|
|
506
|
+
!valueHasPrimitiveType(token.text, primitive)) {
|
|
507
|
+
errors.push({
|
|
508
|
+
token: value.token,
|
|
509
|
+
expected: primitive,
|
|
510
|
+
});
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
value.type = expectedType;
|
|
514
|
+
return literalValueToDenseJson(token.text, expectedType.primitive);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
structValueToDenseJson(value, expectedStruct, errors) {
|
|
519
|
+
const { token } = value;
|
|
520
|
+
if (value.kind !== "object") {
|
|
521
|
+
errors.push({
|
|
522
|
+
token: token,
|
|
523
|
+
expected: "object",
|
|
524
|
+
});
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
const json = Array(expectedStruct.numSlotsInclRemovedNumbers).fill(0);
|
|
528
|
+
let allGood = true;
|
|
529
|
+
const fieldNameTokens = Object.values(value.entries)
|
|
530
|
+
.map((e) => e.name)
|
|
531
|
+
.concat(value.orphanNames);
|
|
532
|
+
const fieldNames = new Set(fieldNameTokens.map((t) => t.text));
|
|
533
|
+
for (const fieldName of fieldNameTokens) {
|
|
534
|
+
const field = expectedStruct.nameToDeclaration[fieldName.text];
|
|
535
|
+
if (field?.kind !== "field") {
|
|
536
|
+
errors.push({
|
|
537
|
+
token: fieldName,
|
|
538
|
+
message: `Field not found in struct ${expectedStruct.name.text}`,
|
|
539
|
+
expectedNames: declarationsToExpectedNames(expectedStruct.nameToDeclaration, (d) => d.kind === "field" && !fieldNames.has(d.name.text)),
|
|
540
|
+
});
|
|
541
|
+
allGood = false;
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
let arrayLen = 0;
|
|
546
|
+
for (const field of expectedStruct.fields) {
|
|
547
|
+
const { type } = field;
|
|
548
|
+
if (!type) {
|
|
549
|
+
allGood = false;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
const fieldEntry = value.entries[field.name.text];
|
|
553
|
+
let valueJson;
|
|
554
|
+
if (fieldEntry) {
|
|
555
|
+
fieldEntry.fieldDeclaration = field;
|
|
556
|
+
valueJson = this.valueToDenseJson(fieldEntry.value, type, errors);
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
// Unless the object is declared partial, all fields are required.
|
|
560
|
+
if (value.partial) {
|
|
561
|
+
valueJson = this.getDefaultJson(type);
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
errors.push({
|
|
565
|
+
token: token,
|
|
566
|
+
message: `Missing entry: ${field.name.text}`,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (valueJson === undefined) {
|
|
571
|
+
allGood = false;
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
json[field.number] = valueJson;
|
|
575
|
+
const hasDefaultValue = type.kind === "optional"
|
|
576
|
+
? valueJson === null
|
|
577
|
+
: !valueJson ||
|
|
578
|
+
(Array.isArray(valueJson) && !valueJson.length) ||
|
|
579
|
+
(type.kind === "primitive" &&
|
|
580
|
+
(type.primitive === "int64" || type.primitive === "hash64") &&
|
|
581
|
+
valueJson === "0");
|
|
582
|
+
if (!hasDefaultValue) {
|
|
583
|
+
arrayLen = Math.max(arrayLen, field.number + 1);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (!allGood) {
|
|
587
|
+
return undefined;
|
|
588
|
+
}
|
|
589
|
+
value.record = expectedStruct;
|
|
590
|
+
return json.slice(0, arrayLen);
|
|
591
|
+
}
|
|
592
|
+
enumValueToDenseJson(value, expectedEnum, errors) {
|
|
593
|
+
const { token } = value;
|
|
594
|
+
if (value.kind === "literal" && isStringLiteral(token.text)) {
|
|
595
|
+
// The value is a string.
|
|
596
|
+
// It must match the name of one of the constants defined in the enum.
|
|
597
|
+
const fieldName = unquoteAndUnescape(token.text);
|
|
598
|
+
if (fieldName === "UNKNOWN") {
|
|
599
|
+
// Present on every enum.
|
|
600
|
+
return 0;
|
|
601
|
+
}
|
|
602
|
+
const field = expectedEnum.nameToDeclaration[fieldName];
|
|
603
|
+
if (field?.kind !== "field") {
|
|
604
|
+
errors.push({
|
|
605
|
+
token: token,
|
|
606
|
+
message: `Variant not found in enum ${expectedEnum.name.text}`,
|
|
607
|
+
expectedNames: [{ name: "UNKNOWN" }].concat(declarationsToExpectedNames(expectedEnum.nameToDeclaration, (d) => d.kind === "field" && !d.type)),
|
|
608
|
+
});
|
|
609
|
+
return undefined;
|
|
610
|
+
}
|
|
611
|
+
if (field.type) {
|
|
612
|
+
errors.push({
|
|
613
|
+
token: token,
|
|
614
|
+
message: "Refers to a wrapper variant",
|
|
615
|
+
});
|
|
616
|
+
return undefined;
|
|
617
|
+
}
|
|
618
|
+
value.type = {
|
|
619
|
+
kind: "enum",
|
|
620
|
+
enum: expectedEnum,
|
|
621
|
+
variant: field,
|
|
622
|
+
};
|
|
623
|
+
return field.number;
|
|
624
|
+
}
|
|
625
|
+
else if (value.kind === "object") {
|
|
626
|
+
// The value is an object. It must have exactly two entries:
|
|
627
|
+
// · 'kind' must match the name of one of the wrapper variants defined in
|
|
628
|
+
// the enum
|
|
629
|
+
// · 'value' must match the type of the wrapper variant
|
|
630
|
+
const { entries } = value;
|
|
631
|
+
const kindEntry = entries.kind;
|
|
632
|
+
const enumValue = entries.value;
|
|
633
|
+
const nameTokens = Object.values(entries)
|
|
634
|
+
.map((e) => e.name)
|
|
635
|
+
.concat(value.orphanNames);
|
|
636
|
+
for (const nameToken of nameTokens) {
|
|
637
|
+
if (nameToken.text === "kind" || nameToken.text === "value") {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const expectedNames = (kindEntry ? [] : [{ name: "kind" }]).concat(enumValue ? [] : [{ name: "value" }]);
|
|
641
|
+
errors.push({
|
|
642
|
+
token: nameToken,
|
|
643
|
+
message: "Extraneous entry",
|
|
644
|
+
expectedNames: expectedNames,
|
|
645
|
+
});
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
if (!kindEntry) {
|
|
649
|
+
errors.push({
|
|
650
|
+
token: token,
|
|
651
|
+
message: "Missing entry: kind",
|
|
652
|
+
});
|
|
653
|
+
return undefined;
|
|
654
|
+
}
|
|
655
|
+
const kindValueToken = kindEntry.value.token;
|
|
656
|
+
if (kindEntry.value.kind !== "literal" ||
|
|
657
|
+
!isStringLiteral(kindValueToken.text)) {
|
|
658
|
+
errors.push({
|
|
659
|
+
token: kindValueToken,
|
|
660
|
+
expected: "string",
|
|
661
|
+
});
|
|
662
|
+
return undefined;
|
|
663
|
+
}
|
|
664
|
+
const variantName = unquoteAndUnescape(kindValueToken.text);
|
|
665
|
+
const variant = expectedEnum.nameToDeclaration[variantName];
|
|
666
|
+
if (variant?.kind !== "field") {
|
|
667
|
+
errors.push({
|
|
668
|
+
token: kindValueToken,
|
|
669
|
+
message: `Variant not found in enum ${expectedEnum.name.text}`,
|
|
670
|
+
expectedNames: declarationsToExpectedNames(expectedEnum.nameToDeclaration, (d) => d.kind === "field" && !!d.type),
|
|
671
|
+
});
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
if (!variant.type) {
|
|
675
|
+
errors.push({
|
|
676
|
+
token: kindValueToken,
|
|
677
|
+
message: "Refers to a constant variant",
|
|
678
|
+
});
|
|
679
|
+
return undefined;
|
|
680
|
+
}
|
|
681
|
+
if (!enumValue) {
|
|
682
|
+
errors.push({
|
|
683
|
+
token: token,
|
|
684
|
+
message: "Missing entry: value",
|
|
685
|
+
});
|
|
686
|
+
return undefined;
|
|
687
|
+
}
|
|
688
|
+
const valueJson = //
|
|
689
|
+
this.valueToDenseJson(enumValue.value, variant.type, errors);
|
|
690
|
+
if (valueJson === undefined) {
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
value.record = expectedEnum;
|
|
694
|
+
// Return an array of length 2.
|
|
695
|
+
return [variant.number, valueJson];
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
// The value is neither a string nor an object. It can't be of enum type.
|
|
699
|
+
errors.push({
|
|
700
|
+
token: token,
|
|
701
|
+
expected: "string or object",
|
|
702
|
+
});
|
|
703
|
+
return undefined;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
getDefaultJson(type) {
|
|
707
|
+
switch (type.kind) {
|
|
708
|
+
case "primitive": {
|
|
709
|
+
switch (type.primitive) {
|
|
710
|
+
case "bool":
|
|
711
|
+
case "int32":
|
|
712
|
+
case "int64":
|
|
713
|
+
case "hash64":
|
|
714
|
+
case "float32":
|
|
715
|
+
case "float64":
|
|
716
|
+
case "timestamp":
|
|
717
|
+
return 0;
|
|
718
|
+
case "string":
|
|
719
|
+
case "bytes":
|
|
720
|
+
return "";
|
|
721
|
+
default: {
|
|
722
|
+
const _ = type.primitive;
|
|
723
|
+
throw new TypeError(_);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
case "array":
|
|
728
|
+
return [];
|
|
729
|
+
case "optional":
|
|
730
|
+
return null;
|
|
731
|
+
case "record": {
|
|
732
|
+
const record = this.recordMap.get(type.key);
|
|
733
|
+
switch (record.record.recordType) {
|
|
734
|
+
case "struct":
|
|
735
|
+
return [];
|
|
736
|
+
case "enum":
|
|
737
|
+
return 0;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
finalize() {
|
|
743
|
+
const modules = new Map();
|
|
744
|
+
for (const [modulePath, moduleBundle] of this.moduleBundles) {
|
|
745
|
+
const { module, tokens } = moduleBundle;
|
|
746
|
+
const moduleErrors = tokens.errors.length ? tokens.errors : module.errors;
|
|
747
|
+
modules.set(modulePath, {
|
|
748
|
+
result: module.result,
|
|
749
|
+
errors: [...moduleErrors],
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
// Look for duplicate method numbers.
|
|
753
|
+
for (const methods of this.registry.numberToMethods.values()) {
|
|
754
|
+
if (methods.length <= 1) {
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
const pushError = (method, other) => {
|
|
758
|
+
const modulePath = method.name.line.modulePath;
|
|
759
|
+
const moduleResult = modules.get(modulePath);
|
|
760
|
+
const otherMethodName = other.name.text;
|
|
761
|
+
const otherModulePath = other.name.line.modulePath;
|
|
762
|
+
moduleResult.errors.push({
|
|
763
|
+
token: method.name,
|
|
764
|
+
message: `Same number as ${otherMethodName} in ${otherModulePath}`,
|
|
765
|
+
});
|
|
766
|
+
};
|
|
767
|
+
pushError(methods[0], methods[1]);
|
|
768
|
+
for (let i = 1; i < methods.length; ++i) {
|
|
769
|
+
pushError(methods[i], methods[0]);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
// Look for duplicate record numbers.
|
|
773
|
+
for (const records of this.registry.numberToRecords.values()) {
|
|
774
|
+
if (records.length <= 1) {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
const pushError = (recordKey, otherKey) => {
|
|
778
|
+
const record = this.registry.recordMap.get(recordKey);
|
|
779
|
+
const other = this.registry.recordMap.get(otherKey);
|
|
780
|
+
const modulePath = record.record.name.line.modulePath;
|
|
781
|
+
const moduleResult = modules.get(modulePath);
|
|
782
|
+
const otherRecordName = other.record.name.text;
|
|
783
|
+
const otherModulePath = other.modulePath;
|
|
784
|
+
moduleResult.errors.push({
|
|
785
|
+
token: record.record.name,
|
|
786
|
+
message: `Same number as ${otherRecordName} in ${otherModulePath}`,
|
|
787
|
+
});
|
|
788
|
+
};
|
|
789
|
+
pushError(records[0], records[1]);
|
|
790
|
+
for (let i = 1; i < records.length; ++i) {
|
|
791
|
+
pushError(records[i], records[0]);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// Aggregate errors across all modules.
|
|
795
|
+
const errors = [];
|
|
796
|
+
for (const moduleBundle of modules.values()) {
|
|
797
|
+
errors.push(...moduleBundle.errors);
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
modules: modules,
|
|
801
|
+
errors: errors,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
// END PROPERTIES
|
|
805
|
+
findRecordByNumber(recordNumber) {
|
|
806
|
+
const { numberToRecords, recordMap } = this.registry;
|
|
807
|
+
const recordKeys = numberToRecords.get(recordNumber);
|
|
808
|
+
return recordKeys?.length === 1 ? recordMap.get(recordKeys[0]) : undefined;
|
|
809
|
+
}
|
|
810
|
+
findMethodByNumber(methodNumber) {
|
|
811
|
+
const { numberToMethods } = this.registry;
|
|
812
|
+
const methods = numberToMethods.get(methodNumber);
|
|
813
|
+
return methods?.length === 1 ? methods[0] : undefined;
|
|
814
|
+
}
|
|
815
|
+
get recordMap() {
|
|
816
|
+
return this.registry.recordMap;
|
|
817
|
+
}
|
|
818
|
+
get errors() {
|
|
819
|
+
return this.finalizationResult.errors;
|
|
820
|
+
}
|
|
821
|
+
get modules() {
|
|
822
|
+
return this.finalizationResult.modules;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* If the array type is keyed, the array value must satisfy two conditions.
|
|
827
|
+
* First: the key field of every item must be set.
|
|
828
|
+
* Second: not two items can have the same key.
|
|
829
|
+
*/
|
|
830
|
+
function validateKeyedItems(items, fieldPath, errors) {
|
|
831
|
+
const { keyType, path } = fieldPath;
|
|
832
|
+
const tryExtractKeyFromItem = (item) => {
|
|
833
|
+
let value = item;
|
|
834
|
+
for (const pathItem of path) {
|
|
835
|
+
const fieldName = pathItem.name;
|
|
836
|
+
if (value.kind === "literal" && fieldName.text === "kind") {
|
|
837
|
+
// An enum constant.
|
|
838
|
+
return value;
|
|
839
|
+
}
|
|
840
|
+
if (value.kind !== "object") {
|
|
841
|
+
// An error was already registered.
|
|
842
|
+
return undefined;
|
|
843
|
+
}
|
|
844
|
+
const entry = value.entries[fieldName.text];
|
|
845
|
+
if (!entry) {
|
|
846
|
+
errors.push({
|
|
847
|
+
token: value.token,
|
|
848
|
+
message: `Missing entry: ${fieldName.text}`,
|
|
849
|
+
});
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
value = entry.value;
|
|
853
|
+
}
|
|
854
|
+
return value;
|
|
855
|
+
};
|
|
856
|
+
const keyIdentityToKeys = new Map();
|
|
857
|
+
for (const item of items) {
|
|
858
|
+
const key = tryExtractKeyFromItem(item);
|
|
859
|
+
if (!key) {
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (key.kind !== "literal") {
|
|
863
|
+
// Cannot happen.
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
let keyIdentity;
|
|
867
|
+
const keyToken = key.token.text;
|
|
868
|
+
if (keyType.kind === "primitive") {
|
|
869
|
+
const { primitive } = keyType;
|
|
870
|
+
if (!valueHasPrimitiveType(keyToken, primitive)) {
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
keyIdentity = literalValueToIdentity(keyToken, primitive);
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
// The key is an enum, use the enum field name as the key identity.
|
|
877
|
+
if (!isStringLiteral(keyToken)) {
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
keyIdentity = unquoteAndUnescape(keyToken);
|
|
881
|
+
}
|
|
882
|
+
if (keyIdentityToKeys.has(keyIdentity)) {
|
|
883
|
+
keyIdentityToKeys.get(keyIdentity).push(key);
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
keyIdentityToKeys.set(keyIdentity, [key]);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
// Verify that every key in `keyIdentityToItems` has a single value.
|
|
890
|
+
for (const duplicateKeys of keyIdentityToKeys.values()) {
|
|
891
|
+
if (duplicateKeys.length <= 1) {
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
for (const key of duplicateKeys) {
|
|
895
|
+
errors.push({
|
|
896
|
+
token: key.token,
|
|
897
|
+
message: "Duplicate key",
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
class TypeResolver {
|
|
903
|
+
constructor(module, moduleBundles,
|
|
904
|
+
/** For automatic imports in completion mode. */
|
|
905
|
+
topLevelNameToRecordLocations, usedImports, errors) {
|
|
906
|
+
this.module = module;
|
|
907
|
+
this.moduleBundles = moduleBundles;
|
|
908
|
+
this.topLevelNameToRecordLocations = topLevelNameToRecordLocations;
|
|
909
|
+
this.usedImports = usedImports;
|
|
910
|
+
this.errors = errors;
|
|
911
|
+
}
|
|
912
|
+
resolve(input, recordOrigin) {
|
|
913
|
+
switch (input.kind) {
|
|
914
|
+
case "primitive":
|
|
915
|
+
return input;
|
|
916
|
+
case "array": {
|
|
917
|
+
const item = this.resolve(input.item, recordOrigin);
|
|
918
|
+
if (!item) {
|
|
919
|
+
return undefined;
|
|
920
|
+
}
|
|
921
|
+
return { kind: "array", item: item, key: input.key };
|
|
922
|
+
}
|
|
923
|
+
case "optional": {
|
|
924
|
+
const value = this.resolve(input.other, recordOrigin);
|
|
925
|
+
if (!value) {
|
|
926
|
+
return undefined;
|
|
927
|
+
}
|
|
928
|
+
return { kind: "optional", other: value };
|
|
929
|
+
}
|
|
930
|
+
case "record": {
|
|
931
|
+
return this.resolveRecordRef(input, recordOrigin);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Finds the definition of the actual record referenced from a value type.
|
|
937
|
+
* This is where we implement the name resolution algorithm.
|
|
938
|
+
*/
|
|
939
|
+
resolveRecordRef(recordRef, recordOrigin) {
|
|
940
|
+
const firstNamePart = recordRef.nameParts[0];
|
|
941
|
+
const makeNotARecordError = (name) => ({
|
|
942
|
+
token: name,
|
|
943
|
+
message: "Does not refer to a struct or an enum",
|
|
944
|
+
});
|
|
945
|
+
const makeCannotFindNameError = (name, expectedNames) => ({
|
|
946
|
+
token: name,
|
|
947
|
+
message: `Cannot find name '${name.text}'`,
|
|
948
|
+
expectedNames: expectedNames,
|
|
949
|
+
});
|
|
950
|
+
const { errors, module, moduleBundles, usedImports } = this;
|
|
951
|
+
// The most nested record/module which contains the first name in the record
|
|
952
|
+
// reference, or the module if the record reference is absolute (starts with
|
|
953
|
+
// a dot).
|
|
954
|
+
let start;
|
|
955
|
+
const expectedNamesCollector = new ExpectedNamesCollector();
|
|
956
|
+
if (recordOrigin !== "top-level" && !recordRef.absolute) {
|
|
957
|
+
// Traverse the chain of ancestors from most nested to top-level.
|
|
958
|
+
for (const fromRecord of [...recordOrigin.recordAncestors].reverse()) {
|
|
959
|
+
const matchMaybe = fromRecord.nameToDeclaration[firstNamePart.text];
|
|
960
|
+
if (matchMaybe && matchMaybe.kind === "record") {
|
|
961
|
+
start = fromRecord;
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
expectedNamesCollector.collect(declarationsToExpectedNames(fromRecord.nameToDeclaration, (d) => d.kind === "record"));
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (start === undefined) {
|
|
970
|
+
const maybeMatch = module.nameToDeclaration[firstNamePart.text];
|
|
971
|
+
if (!maybeMatch) {
|
|
972
|
+
expectedNamesCollector.collect(declarationsToExpectedNames(module.nameToDeclaration, (d) => d.kind === "record" ||
|
|
973
|
+
d.kind === "import" ||
|
|
974
|
+
d.kind === "import-alias"));
|
|
975
|
+
// For automatic imports.
|
|
976
|
+
expectedNamesCollector.collect(this.topLevelNameToRecordLocations
|
|
977
|
+
?.get(firstNamePart.text)
|
|
978
|
+
?.filter((l) => l.modulePath !== module.path) ?? []);
|
|
979
|
+
errors.push(makeCannotFindNameError(firstNamePart, expectedNamesCollector.expectedNames));
|
|
980
|
+
return undefined;
|
|
981
|
+
}
|
|
982
|
+
start = module;
|
|
983
|
+
}
|
|
984
|
+
let it = start;
|
|
985
|
+
const nameParts = [];
|
|
986
|
+
for (let i = 0; i < recordRef.nameParts.length; ++i) {
|
|
987
|
+
const namePart = recordRef.nameParts[i];
|
|
988
|
+
const name = namePart.text;
|
|
989
|
+
let newIt = it.nameToDeclaration[name];
|
|
990
|
+
if (newIt === undefined) {
|
|
991
|
+
errors.push(makeCannotFindNameError(namePart, declarationsToExpectedNames(it.nameToDeclaration, (d) => d.kind === "record" ||
|
|
992
|
+
(i === 0 && d.kind === "import-alias") ||
|
|
993
|
+
(i === 0 && d.kind === "import"))));
|
|
994
|
+
return undefined;
|
|
995
|
+
}
|
|
996
|
+
else if (newIt.kind === "record") {
|
|
997
|
+
it = newIt;
|
|
998
|
+
}
|
|
999
|
+
else if (newIt.kind === "import" || newIt.kind === "import-alias") {
|
|
1000
|
+
const transitiveImportError = () => ({
|
|
1001
|
+
token: namePart,
|
|
1002
|
+
message: "Cannot refer to imports of imported module",
|
|
1003
|
+
});
|
|
1004
|
+
if (i !== 0) {
|
|
1005
|
+
errors.push(transitiveImportError());
|
|
1006
|
+
return undefined;
|
|
1007
|
+
}
|
|
1008
|
+
usedImports.add(name);
|
|
1009
|
+
const newModulePath = newIt.resolvedModulePath;
|
|
1010
|
+
if (newModulePath === undefined) {
|
|
1011
|
+
return undefined;
|
|
1012
|
+
}
|
|
1013
|
+
const newModuleResult = moduleBundles.get(newModulePath);
|
|
1014
|
+
if (!newModuleResult) {
|
|
1015
|
+
// The module was not found or has errors: an error was already
|
|
1016
|
+
// registered, no need to register a new one.
|
|
1017
|
+
return undefined;
|
|
1018
|
+
}
|
|
1019
|
+
const newModule = newModuleResult.module.result;
|
|
1020
|
+
if (newIt.kind === "import") {
|
|
1021
|
+
newIt = newModule.nameToDeclaration[name];
|
|
1022
|
+
if (!newIt) {
|
|
1023
|
+
errors.push(makeCannotFindNameError(namePart, declarationsToExpectedNames(newModule.nameToDeclaration, (d) => d.kind === "record")));
|
|
1024
|
+
return undefined;
|
|
1025
|
+
}
|
|
1026
|
+
if (newIt.kind !== "record") {
|
|
1027
|
+
this.errors.push(newIt.kind === "import" || newIt.kind === "import-alias"
|
|
1028
|
+
? transitiveImportError()
|
|
1029
|
+
: makeNotARecordError(namePart));
|
|
1030
|
+
return undefined;
|
|
1031
|
+
}
|
|
1032
|
+
it = newIt;
|
|
1033
|
+
}
|
|
1034
|
+
else {
|
|
1035
|
+
it = newModule;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
this.errors.push(makeNotARecordError(namePart));
|
|
1040
|
+
return undefined;
|
|
1041
|
+
}
|
|
1042
|
+
nameParts.push({ token: namePart, declaration: newIt });
|
|
1043
|
+
}
|
|
1044
|
+
if (it.kind !== "record") {
|
|
1045
|
+
const name = recordRef.nameParts[0];
|
|
1046
|
+
this.errors.push(makeNotARecordError(name));
|
|
1047
|
+
return undefined;
|
|
1048
|
+
}
|
|
1049
|
+
return {
|
|
1050
|
+
kind: "record",
|
|
1051
|
+
key: it.key,
|
|
1052
|
+
recordType: it.recordType,
|
|
1053
|
+
nameParts: nameParts,
|
|
1054
|
+
refToken: recordRef.nameParts.at(-1),
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
class Cache {
|
|
1059
|
+
constructor(modulePathToNewContent, modulePathToOldBundle, registry) {
|
|
1060
|
+
this.registry = registry;
|
|
1061
|
+
this.modulePathToCacheResult = new Map();
|
|
1062
|
+
const unchangedModulePaths = new Set();
|
|
1063
|
+
for (const [modulePath, newContent] of modulePathToNewContent) {
|
|
1064
|
+
const oldBundle = modulePathToOldBundle.get(modulePath);
|
|
1065
|
+
if (oldBundle?.tokens.result.sourceCode === newContent) {
|
|
1066
|
+
unchangedModulePaths.add(modulePath);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
const classify = (modulePath) => {
|
|
1070
|
+
const oldBundle = modulePathToOldBundle.get(modulePath);
|
|
1071
|
+
if (!oldBundle) {
|
|
1072
|
+
return { kind: "no-cache" };
|
|
1073
|
+
}
|
|
1074
|
+
{
|
|
1075
|
+
const inMap = this.modulePathToCacheResult.get(modulePath);
|
|
1076
|
+
if (inMap) {
|
|
1077
|
+
return inMap;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
let result;
|
|
1081
|
+
const newContent = modulePathToNewContent.get(modulePath);
|
|
1082
|
+
if (newContent === oldBundle.tokens.result.sourceCode) {
|
|
1083
|
+
// Assume best case, may downgrade later.
|
|
1084
|
+
this.modulePathToCacheResult.set(modulePath, {
|
|
1085
|
+
kind: "module-bundle",
|
|
1086
|
+
bundle: oldBundle,
|
|
1087
|
+
});
|
|
1088
|
+
const directDependencies = Object.keys(oldBundle.module.result?.pathToImportedNames);
|
|
1089
|
+
result = directDependencies.every((dep) => classify(dep).kind === "module-bundle")
|
|
1090
|
+
? { kind: "module-bundle", bundle: oldBundle }
|
|
1091
|
+
: { kind: "module-tokens", tokens: oldBundle.tokens };
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
result = { kind: "no-cache" };
|
|
1095
|
+
}
|
|
1096
|
+
this.modulePathToCacheResult.set(modulePath, result);
|
|
1097
|
+
return result;
|
|
1098
|
+
};
|
|
1099
|
+
for (const modulePath of modulePathToOldBundle.keys()) {
|
|
1100
|
+
classify(modulePath);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
getModuleCacheResult(modulePath) {
|
|
1104
|
+
return this.modulePathToCacheResult.get(modulePath) ?? { kind: "no-cache" };
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
/** Registry of declarations possibly across multiple modules. */
|
|
1108
|
+
class DeclarationRegistry {
|
|
1109
|
+
constructor() {
|
|
1110
|
+
this.recordMap = new Map();
|
|
1111
|
+
this.numberToRecords = new Map();
|
|
1112
|
+
this.numberToMethods = new Map();
|
|
1113
|
+
/**
|
|
1114
|
+
* Key: name of a record defined at the top level of a module
|
|
1115
|
+
* Value: array of record locations (record name, doc, module path)
|
|
1116
|
+
*/
|
|
1117
|
+
this.topLevelNameToRecordLocations = new Map();
|
|
1118
|
+
}
|
|
1119
|
+
mergeFrom(other) {
|
|
1120
|
+
for (const [key, value] of other.recordMap) {
|
|
1121
|
+
this.recordMap.set(key, value);
|
|
1122
|
+
}
|
|
1123
|
+
for (const [number, value] of other.numberToRecords) {
|
|
1124
|
+
let existing = this.numberToRecords.get(number);
|
|
1125
|
+
if (!existing) {
|
|
1126
|
+
existing = [];
|
|
1127
|
+
this.numberToRecords.set(number, existing);
|
|
1128
|
+
}
|
|
1129
|
+
existing.push(...value);
|
|
1130
|
+
}
|
|
1131
|
+
for (const [number, value] of other.numberToMethods) {
|
|
1132
|
+
let existing = this.numberToMethods.get(number);
|
|
1133
|
+
if (!existing) {
|
|
1134
|
+
existing = [];
|
|
1135
|
+
this.numberToMethods.set(number, existing);
|
|
1136
|
+
}
|
|
1137
|
+
existing.push(...value);
|
|
1138
|
+
}
|
|
1139
|
+
for (const [topLevelName, recordLocations,] of other.topLevelNameToRecordLocations) {
|
|
1140
|
+
let existing = this.topLevelNameToRecordLocations.get(topLevelName);
|
|
1141
|
+
if (!existing) {
|
|
1142
|
+
existing = [];
|
|
1143
|
+
this.topLevelNameToRecordLocations.set(topLevelName, existing);
|
|
1144
|
+
}
|
|
1145
|
+
existing.push(...recordLocations);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
pushNumberRecord(number, record) {
|
|
1149
|
+
let value = this.numberToRecords.get(number);
|
|
1150
|
+
if (!value) {
|
|
1151
|
+
value = [];
|
|
1152
|
+
this.numberToRecords.set(number, value);
|
|
1153
|
+
}
|
|
1154
|
+
value.push(record);
|
|
1155
|
+
}
|
|
1156
|
+
pushNumberMethod(number, method) {
|
|
1157
|
+
let value = this.numberToMethods.get(number);
|
|
1158
|
+
if (!value) {
|
|
1159
|
+
value = [];
|
|
1160
|
+
this.numberToMethods.set(number, value);
|
|
1161
|
+
}
|
|
1162
|
+
value.push(method);
|
|
1163
|
+
}
|
|
1164
|
+
pushTopLevelNames(recordLocations) {
|
|
1165
|
+
for (const recordName of recordLocations) {
|
|
1166
|
+
let value = this.topLevelNameToRecordLocations.get(recordName.name);
|
|
1167
|
+
if (!value) {
|
|
1168
|
+
value = [];
|
|
1169
|
+
this.topLevelNameToRecordLocations.set(recordName.name, value);
|
|
1170
|
+
}
|
|
1171
|
+
value.push(recordName);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
class ModuleBundle {
|
|
1176
|
+
constructor(tokens, module) {
|
|
1177
|
+
this.tokens = tokens;
|
|
1178
|
+
this.module = module;
|
|
1179
|
+
/**
|
|
1180
|
+
* Registry of declarations found in this module only.
|
|
1181
|
+
* Will be merged into the "global" registry.
|
|
1182
|
+
*/
|
|
1183
|
+
this.registry = new DeclarationRegistry();
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
function ensureAllImportsAreUsed(module, usedImports, errors) {
|
|
1187
|
+
for (const declaration of module.declarations) {
|
|
1188
|
+
if (declaration.kind === "import") {
|
|
1189
|
+
for (const importedName of declaration.importedNames) {
|
|
1190
|
+
if (!usedImports.has(importedName.text)) {
|
|
1191
|
+
errors.push({
|
|
1192
|
+
token: importedName,
|
|
1193
|
+
message: "Unused import",
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
else if (declaration.kind === "import-alias") {
|
|
1199
|
+
if (!usedImports.has(declaration.name.text)) {
|
|
1200
|
+
errors.push({
|
|
1201
|
+
token: declaration.name,
|
|
1202
|
+
message: "Unused import alias",
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
function posixDirname(p) {
|
|
1209
|
+
const i = p.lastIndexOf("/");
|
|
1210
|
+
return i >= 0 ? p.slice(0, i) : "";
|
|
1211
|
+
}
|
|
1212
|
+
function posixJoin(base, rel) {
|
|
1213
|
+
const parts = (base + "/" + rel).split("/");
|
|
1214
|
+
const result = [];
|
|
1215
|
+
for (const part of parts) {
|
|
1216
|
+
if (part === "..") {
|
|
1217
|
+
if (result.length > 0 && result[result.length - 1] !== "..") {
|
|
1218
|
+
result.pop();
|
|
1219
|
+
}
|
|
1220
|
+
else {
|
|
1221
|
+
result.push("..");
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
else if (part !== "." && part !== "") {
|
|
1225
|
+
result.push(part);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return result.join("/");
|
|
1229
|
+
}
|
|
1230
|
+
function posixRelative(from, to) {
|
|
1231
|
+
const fromParts = from === "" ? [] : from.split("/");
|
|
1232
|
+
const toParts = to === "" ? [] : to.split("/");
|
|
1233
|
+
let commonLen = 0;
|
|
1234
|
+
while (commonLen < fromParts.length &&
|
|
1235
|
+
commonLen < toParts.length &&
|
|
1236
|
+
fromParts[commonLen] === toParts[commonLen]) {
|
|
1237
|
+
commonLen++;
|
|
1238
|
+
}
|
|
1239
|
+
const upCount = fromParts.length - commonLen;
|
|
1240
|
+
const downParts = toParts.slice(commonLen);
|
|
1241
|
+
return [...Array(upCount).fill(".."), ...downParts].join("/");
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Returns suggested module paths for import auto-completion.
|
|
1245
|
+
*
|
|
1246
|
+
* Given the partial path the user has typed, returns suggestions from
|
|
1247
|
+
* `modulePathToContent`. For paths where there is more after the matched
|
|
1248
|
+
* segment, the suggestion is truncated at the next "/" and a trailing "/" is
|
|
1249
|
+
* appended to signal that it is a directory, not a file.
|
|
1250
|
+
*
|
|
1251
|
+
* Handles both absolute paths (e.g. "bb/ee") and relative paths (e.g.
|
|
1252
|
+
* "./other", "../sibling").
|
|
1253
|
+
*/
|
|
1254
|
+
function suggestModulePaths(typedPath, originModulePath, modulePathToContent) {
|
|
1255
|
+
const isRelative = typedPath.startsWith("./") || typedPath.startsWith("../");
|
|
1256
|
+
// Compute the absolute path prefix to match against all module paths.
|
|
1257
|
+
let absolutePrefix;
|
|
1258
|
+
if (isRelative) {
|
|
1259
|
+
// Split at the last "/" so that the directory component (fully typed) can
|
|
1260
|
+
// be resolved cleanly, while the tail (partial filename/dir prefix) is
|
|
1261
|
+
// appended afterwards.
|
|
1262
|
+
const lastSlash = typedPath.lastIndexOf("/");
|
|
1263
|
+
const dirComponent = typedPath.slice(0, lastSlash + 1);
|
|
1264
|
+
const filePrefix = typedPath.slice(lastSlash + 1);
|
|
1265
|
+
// Append a dummy filename so Paths.join/normalize treat the directory
|
|
1266
|
+
// component as a file path (avoids trailing-slash edge-cases), then strip
|
|
1267
|
+
// it off again.
|
|
1268
|
+
const dummy = dirComponent + "__dummy__";
|
|
1269
|
+
const resolvedDummy = posixJoin(posixDirname(originModulePath), dummy.startsWith("./") ? dummy.slice(2) : dummy);
|
|
1270
|
+
const absoluteDir = resolvedDummy.slice(0, resolvedDummy.length - "__dummy__".length);
|
|
1271
|
+
absolutePrefix = absoluteDir + filePrefix;
|
|
1272
|
+
}
|
|
1273
|
+
else if (originModulePath.startsWith("@") && !typedPath.startsWith("@")) {
|
|
1274
|
+
absolutePrefix = extractPackagePrefix(originModulePath) + typedPath;
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
absolutePrefix = typedPath;
|
|
1278
|
+
}
|
|
1279
|
+
absolutePrefix = absolutePrefix.replace(/\\/g, "/");
|
|
1280
|
+
if (absolutePrefix.startsWith("../")) {
|
|
1281
|
+
// Typed path escapes the root; no valid suggestions.
|
|
1282
|
+
return [];
|
|
1283
|
+
}
|
|
1284
|
+
const originBaseDir = posixDirname(originModulePath);
|
|
1285
|
+
const suggestions = new Set();
|
|
1286
|
+
for (const path of modulePathToContent) {
|
|
1287
|
+
if (!path.startsWith(absolutePrefix))
|
|
1288
|
+
continue;
|
|
1289
|
+
if (path === originModulePath)
|
|
1290
|
+
continue;
|
|
1291
|
+
// When the user typed a relative path, never suggest modules that live
|
|
1292
|
+
// under a package root ("@...") — those must be imported with an absolute
|
|
1293
|
+
// path.
|
|
1294
|
+
if (isRelative && path.startsWith("@"))
|
|
1295
|
+
continue;
|
|
1296
|
+
const remaining = path.slice(absolutePrefix.length);
|
|
1297
|
+
const slashIndex = remaining.indexOf("/");
|
|
1298
|
+
// If there is a sub-path after the matched prefix, collapse to the next
|
|
1299
|
+
// directory segment and append "/". Otherwise use the full path.
|
|
1300
|
+
const absoluteSuggestion = slashIndex >= 0
|
|
1301
|
+
? absolutePrefix + remaining.slice(0, slashIndex + 1)
|
|
1302
|
+
: path;
|
|
1303
|
+
if (isRelative) {
|
|
1304
|
+
const isDir = absoluteSuggestion.endsWith("/");
|
|
1305
|
+
const absPath = isDir
|
|
1306
|
+
? absoluteSuggestion.slice(0, -1)
|
|
1307
|
+
: absoluteSuggestion;
|
|
1308
|
+
let rel = posixRelative(originBaseDir, absPath);
|
|
1309
|
+
// Paths.relative returns "" when both paths are the same; normalise to
|
|
1310
|
+
// "." so the subsequent prefix-check and directory-slash logic work
|
|
1311
|
+
// correctly (avoids producing the invalid path ".//" for same-dir cases).
|
|
1312
|
+
if (rel === "") {
|
|
1313
|
+
rel = ".";
|
|
1314
|
+
}
|
|
1315
|
+
if (!rel.startsWith(".")) {
|
|
1316
|
+
rel = "./" + rel;
|
|
1317
|
+
}
|
|
1318
|
+
const suggestion = isDir ? rel + "/" : rel;
|
|
1319
|
+
// When the user typed "../", don't suggest "./" — they would have typed
|
|
1320
|
+
// "./" directly if they wanted files in their own directory.
|
|
1321
|
+
if (typedPath.startsWith("../") && suggestion === "./")
|
|
1322
|
+
continue;
|
|
1323
|
+
suggestions.add(suggestion);
|
|
1324
|
+
}
|
|
1325
|
+
else {
|
|
1326
|
+
suggestions.add(absoluteSuggestion);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return [...suggestions].map((name) => ({ name }));
|
|
1330
|
+
}
|
|
1331
|
+
//# sourceMappingURL=module_set.js.map
|