skir 1.1.2 → 1.1.3

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