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.
@@ -3,48 +3,78 @@ import { unquoteAndUnescape, } from "skir-internal";
3
3
  import { isStringLiteral, literalValueToDenseJson, literalValueToIdentity, valueHasPrimitiveType, } from "./literals.js";
4
4
  import { parseModule } from "./parser.js";
5
5
  import { tokenizeModule } from "./tokenizer.js";
6
+ /**
7
+ * Result of compiling a set of modules. Immutable.
8
+ *
9
+ * Support incremental compilation by accepting an optional cache of the previous
10
+ * module set. This cache can be used to avoid re-parsing and re-resolving
11
+ * modules that haven't changed since the last compilation.
12
+ */
6
13
  export class ModuleSet {
7
- static create(fileReader, rootPath) {
8
- return new ModuleSet(new DefaultModuleParser(fileReader, rootPath));
14
+ static compile(modulePathToContent, cache, parseMode = "strict") {
15
+ return new ModuleSet(modulePathToContent, cache, parseMode);
9
16
  }
10
- static fromMap(map) {
11
- const result = new ModuleSet(new MapBasedModuleParser(map));
12
- for (const modulePath of map.keys()) {
13
- result.parseAndResolve(modulePath);
17
+ constructor(modulePathToContent, cache, parseMode) {
18
+ this.modulePathToContent = modulePathToContent;
19
+ this.parseMode = parseMode;
20
+ this.moduleBundles = new Map();
21
+ this.registry = new DeclarationRegistry();
22
+ this.cache = cache
23
+ ? new Cache(modulePathToContent, cache.moduleBundles)
24
+ : undefined;
25
+ for (const modulePath of modulePathToContent.keys()) {
26
+ this.parseAndResolve(modulePath, new Set());
14
27
  }
15
- return result;
16
- }
17
- constructor(moduleParser) {
18
- this.moduleParser = moduleParser;
19
- this.mutableModules = new Map();
20
- this.mutableRecordMap = new Map();
21
- this.mutableResolvedModules = [];
22
- this.numberToRecord = new Map();
23
- this.numberToMethod = new Map();
24
- this.mutableErrors = [];
28
+ this.finalizationResult = this.finalize();
29
+ // So it can be garbage collected.
30
+ this.cache = undefined;
25
31
  }
26
32
  parseAndResolve(modulePath, inProgressSet) {
27
- const inMap = this.mutableModules.get(modulePath);
33
+ const inMap = this.moduleBundles.get(modulePath);
28
34
  if (inMap !== undefined) {
29
35
  return inMap;
30
36
  }
31
- const result = this.doParseAndResolve(modulePath, inProgressSet || new Set());
32
- this.mutableModules.set(modulePath, result);
33
- this.mutableErrors.push(...result.errors);
37
+ const result = this.doParseAndResolve(modulePath, inProgressSet);
38
+ if (result) {
39
+ this.moduleBundles.set(modulePath, result);
40
+ }
34
41
  return result;
35
42
  }
36
43
  /** Called by `parseAndResolve` when the module is not in the map already. */
37
44
  doParseAndResolve(modulePath, inProgressSet) {
38
- const errors = [];
39
- let module;
45
+ const moduleContent = this.modulePathToContent.get(modulePath);
46
+ if (moduleContent === undefined) {
47
+ return null;
48
+ }
49
+ let moduleTokens;
40
50
  {
41
- const parseResult = this.moduleParser.parseModule(modulePath);
42
- if (parseResult.result === null) {
43
- return parseResult;
51
+ const moduleCacheResult = this.cache?.getModuleCacheResult(modulePath) ?? { kind: "no-cache" };
52
+ switch (moduleCacheResult.kind) {
53
+ case "no-cache": {
54
+ moduleTokens = tokenizeModule(moduleContent, modulePath);
55
+ break;
56
+ }
57
+ case "module-tokens": {
58
+ moduleTokens = moduleCacheResult.tokens;
59
+ break;
60
+ }
61
+ case "module-bundle": {
62
+ this.registry.mergeFrom(moduleCacheResult.bundle.registry);
63
+ return moduleCacheResult.bundle;
64
+ }
44
65
  }
66
+ }
67
+ let module;
68
+ const errors = [];
69
+ {
70
+ const parseResult = parseModule(moduleTokens.result, this.parseMode);
45
71
  errors.push(...parseResult.errors);
46
72
  module = parseResult.result;
47
73
  }
74
+ const moduleBundle = new ModuleBundle(moduleTokens, {
75
+ result: module,
76
+ errors: errors,
77
+ });
48
78
  // Process all imports.
49
79
  const pathToImports = new Map();
50
80
  for (const declaration of module.declarations) {
@@ -76,14 +106,16 @@ export class ModuleSet {
76
106
  inProgressSet.add(modulePath);
77
107
  const otherModule = this.parseAndResolve(otherModulePath, inProgressSet);
78
108
  inProgressSet.delete(modulePath);
79
- if (otherModule.result === null) {
109
+ if (otherModule === null) {
80
110
  errors.push({
81
111
  token: declaration.modulePath,
82
112
  message: "Module not found",
113
+ expectedNames: suggestModulePaths(unquoteAndUnescape(declaration.modulePath.text), modulePath, this.modulePathToContent),
83
114
  });
84
115
  }
85
- else if (otherModule.errors.length !== 0) {
86
- const hasCircularDependency = otherModule.errors.some((e) => e.message === circularDependencyMessage);
116
+ else if (otherModule.tokens.errors.length !== 0 ||
117
+ otherModule.module.errors.length !== 0) {
118
+ const hasCircularDependency = otherModule.module.errors.some((e) => e.message === circularDependencyMessage);
87
119
  if (hasCircularDependency) {
88
120
  errors.push({
89
121
  token: declaration.modulePath,
@@ -102,12 +134,12 @@ export class ModuleSet {
102
134
  // Make sure that the symbols we are importing exist in the imported
103
135
  // module and are not imported symbols themselves.
104
136
  for (const importedName of declaration.importedNames) {
105
- const importedDeclaration = otherModule.result.nameToDeclaration[importedName.text];
137
+ const importedDeclaration = otherModule.module.result.nameToDeclaration[importedName.text];
106
138
  if (importedDeclaration === undefined) {
107
139
  errors.push({
108
140
  token: importedName,
109
141
  message: "Not found",
110
- expectedNames: declarationsToExpectedNames(otherModule.result.nameToDeclaration, (d) => d.kind === "record"),
142
+ expectedNames: declarationsToExpectedNames(otherModule.module.result.nameToDeclaration, (d) => d.kind === "record"),
111
143
  });
112
144
  }
113
145
  else if (importedDeclaration.kind === "import") {
@@ -126,7 +158,7 @@ export class ModuleSet {
126
158
  }
127
159
  }
128
160
  const pathToImportedNames = module.pathToImportedNames;
129
- for (const [path, imports] of pathToImports.entries()) {
161
+ for (const [path, imports] of pathToImports) {
130
162
  const importsNoAlias = imports.filter((i) => i.kind === "import");
131
163
  const importsWithAlias = imports.filter((i) => i.kind === "import-alias");
132
164
  if (importsNoAlias.length && importsWithAlias.length) {
@@ -167,41 +199,25 @@ export class ModuleSet {
167
199
  };
168
200
  }
169
201
  }
170
- const result = {
171
- result: module,
172
- errors: errors,
173
- };
174
202
  if (errors.length) {
175
- return result;
203
+ return moduleBundle;
176
204
  }
177
- this.mutableResolvedModules.push(module);
178
205
  // We can't merge these 3 loops into a single one, each operation must run
179
206
  // after the last operation ran on the whole map.
180
207
  // Loop 1: merge the module records map into the cross-module record map.
181
208
  for (const record of module.records) {
182
209
  const { key } = record.record;
183
- this.mutableRecordMap.set(key, record);
210
+ this.registry.recordMap.set(key, record);
211
+ moduleBundle.registry.recordMap.set(key, record);
184
212
  const { recordNumber } = record.record;
185
213
  if (recordNumber != null && !modulePath.startsWith("@")) {
186
- const existing = this.numberToRecord.get(recordNumber);
187
- if (existing === undefined) {
188
- this.numberToRecord.set(recordNumber, key);
189
- }
190
- else {
191
- const otherRecord = this.recordMap.get(existing);
192
- const otherRecordName = otherRecord.record.name.text;
193
- const otherModulePath = otherRecord.modulePath;
194
- errors.push({
195
- token: record.record.name,
196
- message: `Same number as ${otherRecordName} in ${otherModulePath}`,
197
- });
198
- }
214
+ moduleBundle.registry.pushNumberRecord(recordNumber, key);
199
215
  }
200
216
  }
201
217
  // Loop 2: resolve every field type of every record in the module.
202
218
  // Store the result in the Field object.
203
219
  const usedImports = new Set();
204
- const typeResolver = new TypeResolver(module, this.mutableModules, usedImports, errors);
220
+ const typeResolver = new TypeResolver(module, this.moduleBundles, usedImports, errors);
205
221
  for (const record of module.records) {
206
222
  this.storeResolvedFieldTypes(record, typeResolver);
207
223
  }
@@ -247,21 +263,8 @@ export class ModuleSet {
247
263
  this.validateArrayKeys(responseType, errors);
248
264
  }
249
265
  }
250
- if (!modulePath.startsWith("@")) {
251
- const { number } = method;
252
- const existing = this.numberToMethod.get(number);
253
- if (existing === undefined) {
254
- this.numberToMethod.set(number, method);
255
- }
256
- else {
257
- const otherMethodName = existing.name.text;
258
- const otherModulePath = existing.name.line.modulePath;
259
- errors.push({
260
- token: method.name,
261
- message: `Same number as ${otherMethodName} in ${otherModulePath}`,
262
- });
263
- }
264
- }
266
+ const { number } = method;
267
+ moduleBundle.registry.pushNumberMethod(number, method);
265
268
  // Resolve the references in the doc comments of the method.
266
269
  this.resolveDocReferences(method, module, errors);
267
270
  }
@@ -279,7 +282,8 @@ export class ModuleSet {
279
282
  this.resolveDocReferences(constant, module, errors);
280
283
  }
281
284
  ensureAllImportsAreUsed(module, usedImports, errors);
282
- return result;
285
+ this.registry.mergeFrom(moduleBundle.registry);
286
+ return moduleBundle;
283
287
  }
284
288
  storeResolvedFieldTypes(record, typeResolver) {
285
289
  for (const field of record.record.fields) {
@@ -776,8 +780,8 @@ export class ModuleSet {
776
780
  if (!resolvedModulePath) {
777
781
  return false;
778
782
  }
779
- const importedModule = this.mutableModules.get(resolvedModulePath);
780
- if (!importedModule?.result) {
783
+ const importedModule = this.moduleBundles.get(resolvedModulePath);
784
+ if (!importedModule) {
781
785
  return false;
782
786
  }
783
787
  let newNameChain;
@@ -788,7 +792,7 @@ export class ModuleSet {
788
792
  firstName.declaration = match;
789
793
  newNameChain = nameParts.slice(1);
790
794
  }
791
- return tryResolveReference(ref, newNameChain, importedModule.result);
795
+ return tryResolveReference(ref, newNameChain, importedModule.module.result);
792
796
  }
793
797
  case "constant":
794
798
  case "field":
@@ -801,7 +805,7 @@ export class ModuleSet {
801
805
  }
802
806
  }
803
807
  };
804
- const { recordMap } = this;
808
+ const { recordMap } = this.registry;
805
809
  // Build list of naming scopes to search, in order of priority.
806
810
  const scopes = [];
807
811
  const pushRecordAncestorsToScopes = (record) => {
@@ -867,43 +871,87 @@ export class ModuleSet {
867
871
  }
868
872
  }
869
873
  }
870
- mergeFrom(other) {
871
- for (const [key, value] of other.mutableModules.entries()) {
872
- this.mutableModules.set(key, value);
874
+ finalize() {
875
+ const modules = new Map();
876
+ for (const [modulePath, moduleBundle] of this.moduleBundles) {
877
+ const { module, tokens } = moduleBundle;
878
+ const moduleErrors = tokens.errors.length ? tokens.errors : module.errors;
879
+ modules.set(modulePath, {
880
+ result: module.result,
881
+ errors: [...moduleErrors],
882
+ });
873
883
  }
874
- for (const [key, value] of other.recordMap.entries()) {
875
- this.mutableRecordMap.set(key, value);
884
+ // Look for duplicate method numbers.
885
+ for (const methods of this.registry.numberToMethods.values()) {
886
+ if (methods.length <= 1) {
887
+ continue;
888
+ }
889
+ const pushError = (method, other) => {
890
+ const modulePath = method.name.line.modulePath;
891
+ const moduleResult = modules.get(modulePath);
892
+ const otherMethodName = other.name.text;
893
+ const otherModulePath = other.name.line.modulePath;
894
+ moduleResult.errors.push({
895
+ token: method.name,
896
+ message: `Same number as ${otherMethodName} in ${otherModulePath}`,
897
+ });
898
+ };
899
+ pushError(methods[0], methods[1]);
900
+ for (let i = 1; i < methods.length; ++i) {
901
+ pushError(methods[i], methods[0]);
902
+ }
876
903
  }
877
- this.mutableResolvedModules.push(...other.resolvedModules);
878
- for (const [key, value] of other.numberToRecord.entries()) {
879
- this.numberToRecord.set(key, value);
904
+ // Look for duplicate record numbers.
905
+ for (const records of this.registry.numberToRecords.values()) {
906
+ if (records.length <= 1) {
907
+ continue;
908
+ }
909
+ const pushError = (recordKey, otherKey) => {
910
+ const record = this.registry.recordMap.get(recordKey);
911
+ const other = this.registry.recordMap.get(otherKey);
912
+ const modulePath = record.record.name.line.modulePath;
913
+ const moduleResult = modules.get(modulePath);
914
+ const otherRecordName = other.record.name.text;
915
+ const otherModulePath = other.modulePath;
916
+ moduleResult.errors.push({
917
+ token: record.record.name,
918
+ message: `Same number as ${otherRecordName} in ${otherModulePath}`,
919
+ });
920
+ };
921
+ pushError(records[0], records[1]);
922
+ for (let i = 1; i < records.length; ++i) {
923
+ pushError(records[i], records[0]);
924
+ }
880
925
  }
881
- for (const [key, value] of other.numberToMethod.entries()) {
882
- this.numberToMethod.set(key, value);
926
+ // Aggregate errors across all modules.
927
+ const errors = [];
928
+ for (const moduleBundle of modules.values()) {
929
+ errors.push(...moduleBundle.errors);
883
930
  }
884
- this.mutableErrors.push(...other.errors);
885
- }
886
- get modules() {
887
- return this.mutableModules;
888
- }
889
- get recordMap() {
890
- return this.mutableRecordMap;
891
- }
892
- get resolvedModules() {
893
- return this.mutableResolvedModules;
931
+ return {
932
+ modules: modules,
933
+ errors: errors,
934
+ };
894
935
  }
936
+ // END PROPERTIES
895
937
  findRecordByNumber(recordNumber) {
896
- const recordKey = this.numberToRecord.get(recordNumber);
897
- if (recordKey === undefined) {
898
- return undefined;
899
- }
900
- return this.recordMap.get(recordKey);
938
+ const { numberToRecords, recordMap } = this.registry;
939
+ const recordKeys = numberToRecords.get(recordNumber);
940
+ return recordKeys?.length === 1 ? recordMap.get(recordKeys[0]) : undefined;
901
941
  }
902
942
  findMethodByNumber(methodNumber) {
903
- return this.numberToMethod.get(methodNumber);
943
+ const { numberToMethods } = this.registry;
944
+ const methods = numberToMethods.get(methodNumber);
945
+ return methods?.length === 1 ? methods[0] : undefined;
946
+ }
947
+ get recordMap() {
948
+ return this.registry.recordMap;
904
949
  }
905
950
  get errors() {
906
- return this.mutableErrors;
951
+ return this.finalizationResult.errors;
952
+ }
953
+ get modules() {
954
+ return this.finalizationResult.modules;
907
955
  }
908
956
  }
909
957
  /**
@@ -984,9 +1032,9 @@ function validateKeyedItems(items, fieldPath, errors) {
984
1032
  }
985
1033
  }
986
1034
  class TypeResolver {
987
- constructor(module, modules, usedImports, errors) {
1035
+ constructor(module, moduleBundles, usedImports, errors) {
988
1036
  this.module = module;
989
- this.modules = modules;
1037
+ this.moduleBundles = moduleBundles;
990
1038
  this.usedImports = usedImports;
991
1039
  this.errors = errors;
992
1040
  }
@@ -1023,7 +1071,7 @@ class TypeResolver {
1023
1071
  // reference, or the module if the record reference is absolute (starts with
1024
1072
  // a dot).
1025
1073
  let start;
1026
- const { errors, module, modules, usedImports } = this;
1074
+ const { errors, module, moduleBundles: modules, usedImports } = this;
1027
1075
  if (recordOrigin !== "top-level") {
1028
1076
  if (!recordRef.absolute) {
1029
1077
  // Traverse the chain of ancestors from most nested to top-level.
@@ -1079,12 +1127,12 @@ class TypeResolver {
1079
1127
  return undefined;
1080
1128
  }
1081
1129
  const newModuleResult = modules.get(newModulePath);
1082
- if (newModuleResult === undefined || newModuleResult.result === null) {
1130
+ if (!newModuleResult) {
1083
1131
  // The module was not found or has errors: an error was already
1084
1132
  // registered, no need to register a new one.
1085
1133
  return undefined;
1086
1134
  }
1087
- const newModule = newModuleResult.result;
1135
+ const newModule = newModuleResult.module.result;
1088
1136
  if (newIt.kind === "import") {
1089
1137
  newIt = newModule.nameToDeclaration[name];
1090
1138
  if (!newIt) {
@@ -1123,6 +1171,111 @@ class TypeResolver {
1123
1171
  };
1124
1172
  }
1125
1173
  }
1174
+ class Cache {
1175
+ constructor(modulePathToNewContent, modulePathToOldBundle) {
1176
+ this.modulePathToOldBundle = modulePathToOldBundle;
1177
+ this.modulePathToCacheResult = new Map();
1178
+ const unchangedModulePaths = new Set();
1179
+ for (const [modulePath, newContent] of modulePathToNewContent) {
1180
+ const oldBundle = modulePathToOldBundle.get(modulePath);
1181
+ if (oldBundle?.tokens.result.sourceCode === newContent) {
1182
+ unchangedModulePaths.add(modulePath);
1183
+ }
1184
+ }
1185
+ const classify = (modulePath) => {
1186
+ const oldBundle = modulePathToOldBundle.get(modulePath);
1187
+ if (!oldBundle) {
1188
+ return { kind: "no-cache" };
1189
+ }
1190
+ {
1191
+ const inMap = this.modulePathToCacheResult.get(modulePath);
1192
+ if (inMap) {
1193
+ return inMap;
1194
+ }
1195
+ }
1196
+ let result;
1197
+ const newContent = modulePathToNewContent.get(modulePath);
1198
+ if (newContent === oldBundle.tokens.result.sourceCode) {
1199
+ // Assume best case, may downgrade later.
1200
+ this.modulePathToCacheResult.set(modulePath, {
1201
+ kind: "module-bundle",
1202
+ bundle: oldBundle,
1203
+ });
1204
+ const directDependencies = Object.keys(oldBundle.module.result?.pathToImportedNames);
1205
+ result = directDependencies.every((dep) => classify(dep).kind === "module-bundle")
1206
+ ? { kind: "module-bundle", bundle: oldBundle }
1207
+ : { kind: "module-tokens", tokens: oldBundle.tokens };
1208
+ }
1209
+ else {
1210
+ result = { kind: "no-cache" };
1211
+ }
1212
+ this.modulePathToCacheResult.set(modulePath, result);
1213
+ return result;
1214
+ };
1215
+ for (const modulePath of modulePathToOldBundle.keys()) {
1216
+ classify(modulePath);
1217
+ }
1218
+ }
1219
+ getModuleCacheResult(modulePath) {
1220
+ return this.modulePathToCacheResult.get(modulePath) ?? { kind: "no-cache" };
1221
+ }
1222
+ }
1223
+ /** Registry of declarations possibly across multiple modules. */
1224
+ class DeclarationRegistry {
1225
+ constructor() {
1226
+ this.recordMap = new Map();
1227
+ this.numberToRecords = new Map();
1228
+ this.numberToMethods = new Map();
1229
+ }
1230
+ mergeFrom(other) {
1231
+ for (const [key, value] of other.recordMap) {
1232
+ this.recordMap.set(key, value);
1233
+ }
1234
+ for (const [number, value] of other.numberToRecords) {
1235
+ let existing = this.numberToRecords.get(number);
1236
+ if (!existing) {
1237
+ existing = [];
1238
+ this.numberToRecords.set(number, existing);
1239
+ }
1240
+ existing.push(...value);
1241
+ }
1242
+ for (const [number, value] of other.numberToMethods) {
1243
+ let existing = this.numberToMethods.get(number);
1244
+ if (!existing) {
1245
+ existing = [];
1246
+ this.numberToMethods.set(number, existing);
1247
+ }
1248
+ existing.push(...value);
1249
+ }
1250
+ }
1251
+ pushNumberRecord(number, record) {
1252
+ let value = this.numberToRecords.get(number);
1253
+ if (!value) {
1254
+ value = [];
1255
+ this.numberToRecords.set(number, value);
1256
+ }
1257
+ value.push(record);
1258
+ }
1259
+ pushNumberMethod(number, method) {
1260
+ let value = this.numberToMethods.get(number);
1261
+ if (!value) {
1262
+ value = [];
1263
+ this.numberToMethods.set(number, value);
1264
+ }
1265
+ value.push(method);
1266
+ }
1267
+ }
1268
+ class ModuleBundle {
1269
+ constructor(tokens, module) {
1270
+ this.tokens = tokens;
1271
+ this.module = module;
1272
+ /**
1273
+ * Registry of declarations found in this module only.
1274
+ * Will be merged into the "global" registry.
1275
+ */
1276
+ this.registry = new DeclarationRegistry();
1277
+ }
1278
+ }
1126
1279
  function ensureAllImportsAreUsed(module, usedImports, errors) {
1127
1280
  for (const declaration of module.declarations) {
1128
1281
  if (declaration.kind === "import") {
@@ -1145,43 +1298,82 @@ function ensureAllImportsAreUsed(module, usedImports, errors) {
1145
1298
  }
1146
1299
  }
1147
1300
  }
1148
- class ModuleParserBase {
1149
- parseModule(modulePath) {
1150
- const code = this.readSourceCode(modulePath);
1151
- if (code === undefined) {
1152
- return {
1153
- result: null,
1154
- errors: [],
1155
- };
1156
- }
1157
- const tokens = tokenizeModule(code, modulePath);
1158
- if (tokens.errors.length !== 0) {
1159
- return {
1160
- result: null,
1161
- errors: tokens.errors,
1162
- };
1163
- }
1164
- return parseModule(tokens.result, "strict");
1301
+ /**
1302
+ * Returns suggested module paths for import auto-completion.
1303
+ *
1304
+ * Given the partial path the user has typed, returns suggestions from
1305
+ * `modulePathToContent`. For paths where there is more after the matched
1306
+ * segment, the suggestion is truncated at the next "/" and a trailing "/" is
1307
+ * appended to signal that it is a directory, not a file.
1308
+ *
1309
+ * Handles both absolute paths (e.g. "bb/ee") and relative paths (e.g.
1310
+ * "./other", "../sibling").
1311
+ */
1312
+ function suggestModulePaths(typedPath, originModulePath, modulePathToContent) {
1313
+ const isRelative = typedPath.startsWith("./") || typedPath.startsWith("../");
1314
+ // Compute the absolute path prefix to match against all module paths.
1315
+ let absolutePrefix;
1316
+ if (isRelative) {
1317
+ // Split at the last "/" so that the directory component (fully typed) can
1318
+ // be resolved cleanly, while the tail (partial filename/dir prefix) is
1319
+ // appended afterwards.
1320
+ const lastSlash = typedPath.lastIndexOf("/");
1321
+ const dirComponent = typedPath.slice(0, lastSlash + 1);
1322
+ const filePrefix = typedPath.slice(lastSlash + 1);
1323
+ // Append a dummy filename so Paths.join/normalize treat the directory
1324
+ // component as a file path (avoids trailing-slash edge-cases), then strip
1325
+ // it off again.
1326
+ const dummy = dirComponent + "__dummy__";
1327
+ const resolvedDummy = Paths.join(Paths.dirname(originModulePath), dummy.startsWith("./") ? dummy.slice(2) : dummy).replace(/\\/g, "/");
1328
+ const absoluteDir = resolvedDummy.slice(0, resolvedDummy.length - "__dummy__".length);
1329
+ absolutePrefix = absoluteDir + filePrefix;
1165
1330
  }
1166
- }
1167
- class DefaultModuleParser extends ModuleParserBase {
1168
- constructor(fileReader, rootPath) {
1169
- super();
1170
- this.fileReader = fileReader;
1171
- this.rootPath = rootPath;
1331
+ else if (originModulePath.startsWith("@") && !typedPath.startsWith("@")) {
1332
+ // Mirror the package-prefix logic from resolveModulePath.
1333
+ absolutePrefix = extractPackagePrefix(originModulePath) + typedPath;
1172
1334
  }
1173
- readSourceCode(modulePath) {
1174
- return this.fileReader.readTextFile(Paths.join(this.rootPath, modulePath));
1335
+ else {
1336
+ absolutePrefix = typedPath;
1175
1337
  }
1176
- }
1177
- class MapBasedModuleParser extends ModuleParserBase {
1178
- constructor(moduleMap) {
1179
- super();
1180
- this.moduleMap = moduleMap;
1338
+ absolutePrefix = absolutePrefix.replace(/\\/g, "/");
1339
+ if (absolutePrefix.startsWith("../")) {
1340
+ // Typed path escapes the root; no valid suggestions.
1341
+ return [];
1181
1342
  }
1182
- readSourceCode(modulePath) {
1183
- return this.moduleMap.get(modulePath);
1343
+ const originBaseDir = Paths.dirname(originModulePath).replace(/\\/g, "/");
1344
+ const suggestions = new Set();
1345
+ for (const path of modulePathToContent.keys()) {
1346
+ if (!path.startsWith(absolutePrefix))
1347
+ continue;
1348
+ const remaining = path.slice(absolutePrefix.length);
1349
+ const slashIndex = remaining.indexOf("/");
1350
+ // If there is a sub-path after the matched prefix, collapse to the next
1351
+ // directory segment and append "/". Otherwise use the full path.
1352
+ const absoluteSuggestion = slashIndex >= 0
1353
+ ? absolutePrefix + remaining.slice(0, slashIndex + 1)
1354
+ : path;
1355
+ if (isRelative) {
1356
+ const isDir = absoluteSuggestion.endsWith("/");
1357
+ const absPath = isDir
1358
+ ? absoluteSuggestion.slice(0, -1)
1359
+ : absoluteSuggestion;
1360
+ let rel = Paths.relative(originBaseDir, absPath).replace(/\\/g, "/");
1361
+ // Paths.relative returns "" when both paths are the same; normalise to
1362
+ // "." so the subsequent prefix-check and directory-slash logic work
1363
+ // correctly (avoids producing the invalid path ".//" for same-dir cases).
1364
+ if (rel === "") {
1365
+ rel = ".";
1366
+ }
1367
+ if (!rel.startsWith(".")) {
1368
+ rel = "./" + rel;
1369
+ }
1370
+ suggestions.add(isDir ? rel + "/" : rel);
1371
+ }
1372
+ else {
1373
+ suggestions.add(absoluteSuggestion);
1374
+ }
1184
1375
  }
1376
+ return [...suggestions].map((name) => ({ name }));
1185
1377
  }
1186
1378
  function resolveModulePath(pathToken, originModulePath, errors) {
1187
1379
  let modulePath = unquoteAndUnescape(pathToken.text);