• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

source-academy / js-slang / 5406352152

pending completion
5406352152

Pull #1428

github

web-flow
Merge 0380f5ed7 into 8618e26e4
Pull Request #1428: Further Enhancements to the Module System

3611 of 4728 branches covered (76.37%)

Branch coverage included in aggregate %.

831 of 831 new or added lines in 50 files covered. (100.0%)

10852 of 12603 relevant lines covered (86.11%)

93898.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.57
/src/modules/preprocessor/analyzer.ts
1
import { UNKNOWN_LOCATION } from '../../constants'
52✔
2
import {
52✔
3
  ModuleInternalError,
4
  ReexportDefaultError,
5
  ReexportSymbolError,
6
  UndefinedDefaultImportError,
7
  UndefinedImportError,
8
  UndefinedNamespaceImportError
9
} from '../../modules/errors'
10
import { reduceAsync } from '../../utils'
52✔
11
import ArrayMap from '../../utils/arrayMap'
52✔
12
import assert from '../../utils/assert'
52✔
13
import * as create from '../../utils/ast/astCreator'
52✔
14
import { extractIdsFromPattern, processExportNamedDeclaration } from '../../utils/ast/astUtils'
52✔
15
import { isSourceImport } from '../../utils/ast/typeGuards'
52✔
16
import type * as es from '../../utils/ast/types'
17
import { memoizedGetModuleDocsAsync } from '../moduleLoaderAsync'
52✔
18

19
type ExportRecord = {
20
  /**
21
   * The name of the symbol defined by its source
22
   */
23
  symbolName: string
24

25
  /**
26
   * The actual source in which the symbol is defined
27
   */
28
  source: string
29

30
  loc: es.SourceLocation
31
}
32

33
/**
34
 * An abstraction of the `Set<string>` type. When `allowUndefinedImports` is true,
35
 * the set is replaced with an object that will never throw an error for any kind
36
 * of imported symbol
37
 */
38
type ExportSymbolsRecord = {
39
  has: (symbol: string) => boolean
40
  readonly size: number
41
  [Symbol.iterator]: () => Iterator<string>
42
}
43

44
/**
45
 * An abstraction of the `Map<string, ExportRecord>` type. When `allowUndefinedImports` is true,
46
 * the set is replaced with an object that will ensure no errors are thrown for any kind
47
 * of imported symbol
48
 */
49
type ExportSourceMap = {
50
  get: (symbol: string) => ExportRecord | undefined
51
  set: (symbol: string, value: ExportRecord) => void
52
  keys: () => Iterable<string>
53
}
54

55
const validateDefaultImport = (
52✔
56
  spec: es.ImportDefaultSpecifier | es.ExportSpecifier | es.ImportSpecifier,
57
  sourcePath: string,
58
  modExported: ExportSymbolsRecord
59
) => {
60
  if (!modExported.has('default')) {
9✔
61
    throw new UndefinedDefaultImportError(sourcePath, spec)
7✔
62
  }
63
}
64

65
const validateImport = (
52✔
66
  spec: es.ImportSpecifier | es.ImportDefaultSpecifier | es.ExportSpecifier,
67
  sourcePath: string,
68
  modExported: ExportSymbolsRecord
69
) => {
70
  let symbol: string
71
  switch (spec.type) {
31✔
72
    case 'ExportSpecifier': {
31✔
73
      symbol = spec.local.name
4✔
74
      break
4✔
75
    }
76
    case 'ImportSpecifier': {
77
      symbol = spec.imported.name
20✔
78
      break
20✔
79
    }
80
    case 'ImportDefaultSpecifier': {
81
      symbol = 'default'
7✔
82
      break
7✔
83
    }
84
  }
85

86
  if (symbol === 'default') {
31✔
87
    validateDefaultImport(spec, sourcePath, modExported)
9✔
88
  } else if (!modExported.has(symbol)) {
22✔
89
    throw new UndefinedImportError(symbol, sourcePath, spec)
6✔
90
  }
91
}
92

93
const validateNamespaceImport = (
52✔
94
  spec: es.ImportNamespaceSpecifier | es.ExportAllDeclaration,
95
  sourcePath: string,
96
  modExported: ExportSymbolsRecord
97
) => {
98
  if (modExported.size === 0) {
18✔
99
    throw new UndefinedNamespaceImportError(sourcePath, spec)
1✔
100
  }
101
}
102

103
/**
104
 * Check for undefined imports, and also for symbols that have multiple export
105
 * definitions, and also resolve export and import directives to their sources
106
 */
107
export default async function analyzeImportsAndExports(
52✔
108
  programs: Record<string, es.Program>,
109
  topoOrder: string[],
110
  allowUndefinedImports: boolean
111
) {
112
  const exportMap: Record<string, ExportSourceMap> = {}
1,769✔
113

114
  /**
115
    The idea behind this function is to resolve indirect exports
116
    For example
117
    ```
118
    // a.js
119
    export const a = "a";
120
    // b.js
121
    export { a as b } from './a.js'
122
    ```
123

124
    We want to change the following import statement `import { b } from './b.js'` to
125
    `import { a } from './a.js', since the `export` declaration in `b.js` just serves
126
    as a redirection and doesn't affect code behaviour
127
   */
128
  function resolveSymbol(source: string, desiredSymbol: string): [string, string] {
129
    let symbolName: string
130
    let newSource: string
131
    let loc: es.SourceLocation
132

133
      // So for each exported symbol, we return the path to the file where it is actually
134
      // defined and the name it was defined with (since exports can have aliases)
135
      // Kind of like a UFDS, where the roots of each set are symbols that are defined within
136
      // its own file, or imports from Source modules
137

138
      // eslint-disable-next-line prefer-const
139
    ;({ source: newSource, symbolName, loc } = exportMap[source].get(desiredSymbol)!)
98✔
140
    if (isSourceImport(source) || newSource === source) return [newSource, symbolName]
98✔
141
    ;[newSource, symbolName] = resolveSymbol(newSource, symbolName)
4✔
142
    exportMap[source].set(desiredSymbol, { source: newSource, symbolName, loc })
4✔
143
    return [newSource, symbolName]
4✔
144
  }
145

146
  const getDocs = async (
1,769✔
147
    node: es.ModuleDeclarationWithSource
148
  ): Promise<[ExportSymbolsRecord, string]> => {
89✔
149
    const path = node.source!.value as string
89✔
150

151
    if (allowUndefinedImports) {
89✔
152
      exportMap[path] = {
44✔
153
        get: (symbol: string) => ({
58✔
154
          source: path,
155
          symbolName: symbol,
156
          loc: UNKNOWN_LOCATION
157
        }),
158
        set: () => {},
159
        keys: () => ['']
×
160
      }
161

162
      // When undefined imports are allowed, we substitute the list of exported
163
      // symbols for an object that behaves like a set but always returns true when
164
      // `has` is queried
165
      return [
44✔
166
        {
167
          has: () => true,
×
168
          [Symbol.iterator]: () => ({ next: () => ({ done: true, value: null }) }),
5✔
169
          size: 9999
170
        },
171
        path
172
      ]
173
    }
174

175
    if (!(path in exportMap)) {
45✔
176
      // Because modules are loaded in topological order, the exported symbols for a local
177
      // module should be loaded by the time they are needed
178
      // So we can assume that it is the documentation for a Source module that needs to be
179
      // loaded here
180
      assert(
12✔
181
        isSourceImport(path),
182
        `Trying to load: ${path}, local modules should already have been loaded in topological order`
183
      )
184

185
      const docs = await memoizedGetModuleDocsAsync(path)
12✔
186
      if (!docs) {
12!
187
        throw new ModuleInternalError(path, `Failed to load documentation for ${path}`)
×
188
      }
189
      exportMap[path] = new Map(
12✔
190
        Object.keys(docs).map(symbol => [
24✔
191
          symbol,
192
          { source: path, symbolName: symbol, loc: UNKNOWN_LOCATION }
193
        ])
194
      )
195
    }
196
    return [new Set(exportMap[path].keys()), path]
45✔
197
  }
198

199
  const newImportDeclaration = (
1,769✔
200
    source: string,
201
    local: es.Identifier,
202
    imported: string
203
  ): es.ImportDeclaration => ({
70✔
204
    type: 'ImportDeclaration',
205
    source: create.literal(source),
206
    specifiers: [
207
      imported === 'default'
208
        ? {
70✔
209
            type: 'ImportDefaultSpecifier',
210
            local
211
          }
212
        : {
213
            type: 'ImportSpecifier',
214
            local,
215
            imported: create.identifier(imported)
216
          }
217
    ]
218
  })
219

220
  const newPrograms: Record<string, es.Program> = {}
1,769✔
221
  for (const moduleName of topoOrder) {
1,769✔
222
    const program = programs[moduleName]
1,829✔
223
    const exportedSymbols = new ArrayMap<string, ExportRecord>()
1,829✔
224

225
    const newBody = await reduceAsync(
1,829✔
226
      program.body,
227
      async (body, node) => {
2,933✔
228
        switch (node.type) {
2,933✔
229
          case 'ImportDeclaration': {
2,955✔
230
            const [exports, source] = await getDocs(node)
58✔
231
            const newDecls = node.specifiers.map(spec => {
58✔
232
              switch (spec.type) {
81✔
233
                case 'ImportDefaultSpecifier':
99!
234
                case 'ImportSpecifier': {
235
                  if (!allowUndefinedImports) validateImport(spec, source, exports)
81✔
236

237
                  const desiredSymbol =
238
                    spec.type === 'ImportSpecifier' ? spec.imported.name : 'default'
70✔
239
                  const [newSource, symbolName] = resolveSymbol(source, desiredSymbol)
70✔
240
                  return newImportDeclaration(newSource, spec.local, symbolName)
70✔
241
                }
242
                case 'ImportNamespaceSpecifier': {
243
                  throw new Error('Namespace imports are not supported!')
×
244
                  // validateNamespaceImport(spec, source, exports)
245
                  // return {
246
                  //   ...node,
247
                  //   specifiers: [spec]
248
                  // }
249
                }
250
              }
251
            })
252
            return [...body, ...newDecls]
47✔
253
          }
254
          case 'ExportDefaultDeclaration': {
255
            exportedSymbols.add('default', {
18✔
256
              source: moduleName,
257
              symbolName: 'default',
258
              loc: node.loc!
259
            })
260
            return [...body, node]
18✔
261
          }
262
          case 'ExportNamedDeclaration': {
263
            return await processExportNamedDeclaration(node, {
87✔
264
              withVarDecl: async ({ declarations }) => {
49✔
265
                for (const { id } of declarations) {
49✔
266
                  extractIdsFromPattern(id).forEach(({ name }) => {
49✔
267
                    exportedSymbols.add(name, {
49✔
268
                      source: moduleName,
269
                      symbolName: name,
270
                      loc: id.loc!
271
                    })
272
                  })
273
                }
274
                return [...body, node]
49✔
275
              },
276
              withFunction: async ({ id: { name } }) => {
26✔
277
                exportedSymbols.add(name, {
26✔
278
                  source: moduleName,
279
                  symbolName: name,
280
                  loc: node.loc!
281
                })
282
                return [...body, node]
26✔
283
              },
284
              withClass: async ({ id: { name } }) => {
×
285
                exportedSymbols.add(name, {
×
286
                  source: moduleName,
287
                  symbolName: name,
288
                  loc: node.loc!
289
                })
290
                return [...body, node]
×
291
              },
292
              localExports: async ({ specifiers }) => {
4✔
293
                specifiers.forEach(spec =>
4✔
294
                  exportedSymbols.add(spec.exported.name, {
5✔
295
                    source: moduleName,
296
                    symbolName: spec.local.name,
297
                    loc: spec.loc!
298
                  })
299
                )
300
                return [...body, node]
4✔
301
              },
302
              withSource: async node => {
8✔
303
                const [exports, source] = await getDocs(node)
8✔
304
                const newDecls = node.specifiers.map(spec => {
8✔
305
                  if (!allowUndefinedImports) validateImport(spec, source, exports)
8✔
306

307
                  const [newSource, symbolName] = resolveSymbol(source, spec.local.name)
6✔
308
                  exportedSymbols.add(spec.exported.name, {
6✔
309
                    source: newSource,
310
                    symbolName,
311
                    loc: spec.loc!
312
                  })
313

314
                  const newDecl: es.ExportNamedDeclarationWithSource = {
6✔
315
                    type: 'ExportNamedDeclaration',
316
                    declaration: null,
317
                    source: create.literal(newSource),
318
                    specifiers: [
319
                      {
320
                        type: 'ExportSpecifier',
321
                        exported: spec.exported,
322
                        local: create.identifier(symbolName)
323
                      }
324
                    ]
325
                  }
326
                  return newDecl
6✔
327
                })
328
                return [...body, ...newDecls]
6✔
329
              }
330
            })
331
          }
332
          case 'ExportAllDeclaration': {
333
            if (node.exported) {
23!
334
              throw new Error('ExportAllDeclarations with exported name are not supported')
×
335
              // exportedSymbols.add(node.exported.name, {
336
              //   source,
337
              //   symbolName: node.exported.name,
338
              //   loc: node.loc!,
339
              // })
340
            } else {
341
              const [exports, source] = await getDocs(node)
23✔
342
              if (!allowUndefinedImports) validateNamespaceImport(node, source, exports)
23✔
343

344
              for (const symbol of exports) {
22✔
345
                const [newSource, newSymbol] = resolveSymbol(source, symbol)
18✔
346
                exportedSymbols.add(symbol, {
18✔
347
                  source: newSource,
348
                  symbolName: newSymbol,
349
                  loc: node.loc!
350
                })
351
              }
352
            }
353
          }
354
          default:
355
            return [...body, node]
2,769✔
356
        }
357
      },
358
      [] as es.Program['body']
359
    )
360

361
    exportMap[moduleName] = new Map(
1,815✔
362
      exportedSymbols.entries().map(([symbol, records]) => {
363
        if (records.length === 1) return [symbol, records[0]]
112✔
364
        assert(records.length > 0, 'An exported symbol cannot have zero nodes associated with it')
9✔
365
        const locations = records.map(({ loc }) => loc)
18✔
366
        if (symbol === 'default') {
9✔
367
          throw new ReexportDefaultError(moduleName, locations)
5✔
368
        } else {
369
          throw new ReexportSymbolError(moduleName, symbol, locations)
4✔
370
        }
371
      })
372
    )
373

374
    newPrograms[moduleName] = {
1,806✔
375
      ...program,
376
      body: newBody
377
    }
378
  }
379

380
  return newPrograms
1,746✔
381
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc