• 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

91.73
/src/modules/preprocessor/transformers/transformProgramToFunctionDeclaration.ts
1
import * as pathlib from 'path'
53✔
2

3
import {
53✔
4
  accessExportFunctionName,
5
  defaultExportLookupName
6
} from '../../../stdlib/localImport.prelude'
7
import assert from '../../../utils/assert'
53✔
8
import { processExportDefaultDeclaration } from '../../../utils/ast/astUtils'
53✔
9
import {
53✔
10
  isDeclaration,
11
  isDirective,
12
  isModuleDeclaration,
13
  isSourceImport,
14
  isStatement
15
} from '../../../utils/ast/typeGuards'
16
import type * as es from '../../../utils/ast/types'
17
import {
53✔
18
  createCallExpression,
19
  createFunctionDeclaration,
20
  createIdentifier,
21
  createLiteral,
22
  createReturnStatement
23
} from '../constructors/baseConstructors'
24
import {
53✔
25
  createImportedNameDeclaration,
26
  createListCallExpression,
27
  createPairCallExpression
28
} from '../constructors/contextSpecificConstructors'
29
import {
53✔
30
  transformFilePathToValidFunctionName,
31
  transformFunctionNameToInvokedFunctionResultVariableName
32
} from '../filePaths'
33

34
export const getInvokedFunctionResultVariableNameToImportSpecifiersMap = (
53✔
35
  nodes: es.ModuleDeclaration[],
36
  currentDirPath: string
37
): Record<string, (es.ImportSpecifiers | es.ExportSpecifier)[]> => {
38
  const invokedFunctionResultVariableNameToImportSpecifierMap: Record<
39
    string,
40
    (es.ImportSpecifiers | es.ExportSpecifier)[]
41
  > = {}
1,740✔
42
  nodes.forEach((node: es.ModuleDeclaration): void => {
1,740✔
43
    switch (node.type) {
80✔
44
      case 'ExportNamedDeclaration': {
80✔
45
        if (!node.source) return
28✔
46
        break
×
47
      }
48
      case 'ImportDeclaration':
49
        break
39✔
50
      default:
51
        return
13✔
52
    }
53

54
    const importSource = node.source!.value
39✔
55
    assert(
39✔
56
      typeof importSource === 'string',
57
      `Encountered an ${node.type} node with a non-string source. This should never occur.`
58
    )
59

60
    // Only handle import declarations for non-Source modules.
61
    if (isSourceImport(importSource)) {
39✔
62
      return
17✔
63
    }
64

65
    // Different import sources can refer to the same file. For example,
66
    // both './b.js' & '../dir/b.js' can refer to the same file if the
67
    // current file path is '/dir/a.js'. To ensure that every file is
68
    // processed only once, we resolve the import source against the
69
    // current file path to get the absolute file path of the file to
70
    // be imported. Since the absolute file path is guaranteed to be
71
    // unique, it is also the canonical file path.
72
    const importFilePath = pathlib.resolve(currentDirPath, importSource)
22✔
73

74
    // Even though we limit the chars that can appear in Source file
75
    // paths, some chars in file paths (such as '/') cannot be used
76
    // in function names. As such, we substitute illegal chars with
77
    // legal ones in a manner that gives us a bijective mapping from
78
    // file paths to function names.
79
    const importFunctionName = transformFilePathToValidFunctionName(importFilePath)
22✔
80

81
    // In the top-level environment of the resulting program, for every
82
    // imported file, we will end up with two different names; one for
83
    // the function declaration, and another for the variable holding
84
    // the result of invoking the function. The former is represented
85
    // by 'importFunctionName', while the latter is represented by
86
    // 'invokedFunctionResultVariableName'. Since multiple files can
87
    // import the same file, yet we only want the code in each file to
88
    // be evaluated a single time (and share the same state), we need to
89
    // evaluate the transformed functions (of imported files) only once
90
    // in the top-level environment of the resulting program, then pass
91
    // the result (the exported names) into other transformed functions.
92
    // Having the two different names helps us to achieve this objective.
93
    const invokedFunctionResultVariableName =
94
      transformFunctionNameToInvokedFunctionResultVariableName(importFunctionName)
22✔
95

96
    // If this is the file ImportDeclaration node for the canonical
97
    // file path, instantiate the entry in the map.
98
    if (
22✔
99
      invokedFunctionResultVariableNameToImportSpecifierMap[invokedFunctionResultVariableName] ===
100
      undefined
101
    ) {
102
      invokedFunctionResultVariableNameToImportSpecifierMap[invokedFunctionResultVariableName] = []
14✔
103
    }
104
    invokedFunctionResultVariableNameToImportSpecifierMap[invokedFunctionResultVariableName].push(
22✔
105
      ...node.specifiers
106
    )
107
  })
108

109
  return invokedFunctionResultVariableNameToImportSpecifierMap
1,740✔
110
}
111

112
const getExportExpressions = (
53✔
113
  nodes: es.ModuleDeclaration[],
114
  invokedFunctionResultVariableNameToImportSpecifierMap: Record<
115
    string,
116
    (es.ImportSpecifiers | es.ExportSpecifier)[]
117
  >
118
) => {
119
  const exportExpressions: Record<string, es.Expression> = {}
27✔
120

121
  for (const node of nodes) {
27✔
122
    switch (node.type) {
50✔
123
      case 'ExportNamedDeclaration': {
29✔
124
        if (node.declaration) {
19✔
125
          let identifier: es.Identifier
126
          if (node.declaration.type === 'VariableDeclaration') {
14✔
127
            const {
128
              declarations: [{ id }]
129
            } = node.declaration
11✔
130
            identifier = id as es.Identifier
11✔
131
          } else {
132
            identifier = node.declaration.id!
3✔
133
          }
134
          exportExpressions[identifier.name] = identifier
14✔
135
        } else if (!node.source) {
5✔
136
          node.specifiers.forEach(({ exported: { name }, local }) => {
5✔
137
            exportExpressions[name] = local
15✔
138
          })
139
        }
140
        break
19✔
141
      }
142
      case 'ExportDefaultDeclaration': {
143
        exportExpressions[defaultExportLookupName] = processExportDefaultDeclaration(node, {
10✔
144
          ClassDeclaration: ({ id }) => id,
×
145
          FunctionDeclaration: ({ id }) => id,
2✔
146
          Expression: expr => expr
8✔
147
        })
148
        break
10✔
149
      }
150
    }
151
  }
152

153
  for (const [source, nodes] of Object.entries(
27✔
154
    invokedFunctionResultVariableNameToImportSpecifierMap
155
  )) {
156
    for (const node of nodes) {
11✔
157
      if (node.type !== 'ExportSpecifier') continue
15✔
158

159
      const {
160
        exported: { name: exportName },
161
        local: { name: localName }
162
      } = node
×
163
      exportExpressions[exportName] = createCallExpression(accessExportFunctionName, [
×
164
        createIdentifier(source),
165
        createLiteral(localName)
166
      ])
167
    }
168
  }
169

170
  return exportExpressions
27✔
171
}
172

173
export const createAccessImportStatements = (
53✔
174
  invokedFunctionResultVariableNameToImportSpecifiersMap: Record<
175
    string,
176
    (es.ImportSpecifiers | es.ExportSpecifier)[]
177
  >
178
): es.VariableDeclaration[] => {
179
  const importDeclarations: es.VariableDeclaration[] = []
1,740✔
180
  for (const [invokedFunctionResultVariableName, importSpecifiers] of Object.entries(
1,740✔
181
    invokedFunctionResultVariableNameToImportSpecifiersMap
182
  )) {
183
    importSpecifiers.forEach(importSpecifier => {
14✔
184
      let importDeclaration
185
      switch (importSpecifier.type) {
22✔
186
        case 'ImportSpecifier':
22!
187
          importDeclaration = createImportedNameDeclaration(
18✔
188
            invokedFunctionResultVariableName,
189
            importSpecifier.local,
190
            importSpecifier.imported.name
191
          )
192
          break
18✔
193
        case 'ImportDefaultSpecifier':
194
          importDeclaration = createImportedNameDeclaration(
4✔
195
            invokedFunctionResultVariableName,
196
            importSpecifier.local,
197
            defaultExportLookupName
198
          )
199
          break
4✔
200
        case 'ImportNamespaceSpecifier':
201
          // In order to support namespace imports, Source would need to first support objects.
202
          throw new Error('Namespace imports are not supported.')
×
203
        case 'ExportSpecifier':
204
          return
×
205
      }
206
      importDeclarations.push(importDeclaration)
22✔
207
    })
208
  }
209
  return importDeclarations
1,740✔
210
}
211

212
const createReturnListArguments = (
53✔
213
  exportedNameToIdentifierMap: Record<string, es.Expression>
214
): Array<es.Expression | es.SpreadElement> => {
215
  return Object.entries(exportedNameToIdentifierMap).map(
27✔
216
    ([exportedName, expr]: [string, es.Identifier]): es.SimpleCallExpression => {
217
      const head = createLiteral(exportedName)
28✔
218
      const tail = expr
28✔
219
      return createPairCallExpression(head, tail)
28✔
220
    }
221
  )
222
}
223

224
const removeDirectives = (
53✔
225
  nodes: Array<es.Directive | es.Statement | es.ModuleDeclaration>
226
): Array<es.Statement | es.ModuleDeclaration> => {
227
  return nodes.filter(
27✔
228
    (
229
      node: es.Directive | es.Statement | es.ModuleDeclaration
230
    ): node is es.Statement | es.ModuleDeclaration => !isDirective(node)
79✔
231
  )
232
}
233

234
const removeModuleDeclarations = (
53✔
235
  nodes: Array<es.Statement | es.ModuleDeclaration>
236
): es.Statement[] => {
237
  const statements: es.Statement[] = []
27✔
238
  nodes.forEach((node: es.Statement | es.ModuleDeclaration): void => {
27✔
239
    if (isStatement(node)) {
79✔
240
      statements.push(node)
29✔
241
      return
29✔
242
    }
243
    // If there are declaration nodes that are child nodes of the
244
    // ModuleDeclaration nodes, we add them to the processed statements
245
    // array so that the declarations are still part of the resulting
246
    // program.
247
    switch (node.type) {
50✔
248
      case 'ImportDeclaration':
50!
249
        break
21✔
250
      case 'ExportNamedDeclaration':
251
        if (node.declaration) {
19✔
252
          statements.push(node.declaration)
14✔
253
        }
254
        break
19✔
255
      case 'ExportDefaultDeclaration':
256
        if (isDeclaration(node.declaration)) {
10✔
257
          statements.push(node.declaration)
2✔
258
        }
259
        break
10✔
260
      case 'ExportAllDeclaration':
261
        throw new Error('Not implemented yet.')
×
262
    }
263
  })
264
  return statements
27✔
265
}
266

267
/**
268
 * Transforms the given program into a function declaration. This is done
269
 * so that every imported module has its own scope (since functions have
270
 * their own scope).
271
 *
272
 * @param program         The program to be transformed.
273
 * @param currentFilePath The file path of the current program.
274
 */
275
export const transformProgramToFunctionDeclaration = (
53✔
276
  program: es.Program,
277
  currentFilePath: string
278
): es.FunctionDeclarationWithId => {
279
  const moduleDeclarations = program.body.filter(isModuleDeclaration)
27✔
280
  const currentDirPath = pathlib.resolve(currentFilePath, '..')
27✔
281

282
  // Create variables to hold the imported statements.
283
  const invokedFunctionResultVariableNameToImportSpecifiersMap =
284
    getInvokedFunctionResultVariableNameToImportSpecifiersMap(moduleDeclarations, currentDirPath)
27✔
285

286
  const accessImportStatements = createAccessImportStatements(
27✔
287
    invokedFunctionResultVariableNameToImportSpecifiersMap
288
  )
289

290
  // Create the return value of all exports for the function.
291
  const { [defaultExportLookupName]: defaultExport, ...exportExpressions } = getExportExpressions(
27!
292
    moduleDeclarations,
293
    invokedFunctionResultVariableNameToImportSpecifiersMap
294
  )
295
  const namedExports = createListCallExpression(createReturnListArguments(exportExpressions))
27✔
296
  const returnStatement = createReturnStatement(
27✔
297
    createPairCallExpression(defaultExport ?? createLiteral(null), namedExports)
81✔
298
  )
299

300
  // Assemble the function body.
301
  const programStatements = removeModuleDeclarations(removeDirectives(program.body))
27✔
302
  const functionBody = [...accessImportStatements, ...programStatements, returnStatement]
27✔
303

304
  // Determine the function name based on the absolute file path.
305
  const functionName = transformFilePathToValidFunctionName(currentFilePath)
27✔
306

307
  // Set the equivalent variable names of imported modules as the function parameters.
308
  const functionParams = Object.keys(invokedFunctionResultVariableNameToImportSpecifiersMap).map(
27✔
309
    createIdentifier
310
  )
311

312
  return createFunctionDeclaration(functionName, functionParams, functionBody)
27✔
313
}
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

© 2025 Coveralls, Inc