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

source-academy / js-slang / 24833433462

23 Apr 2026 11:47AM UTC coverage: 78.535% (+0.1%) from 78.391%
24833433462

Pull #1893

github

web-flow
Merge 9b995e587 into 715603479
Pull Request #1893: Error Handling and Stringify Changes

3126 of 4197 branches covered (74.48%)

Branch coverage included in aggregate %.

801 of 976 new or added lines in 76 files covered. (82.07%)

20 existing lines in 11 files now uncovered.

7056 of 8768 relevant lines covered (80.47%)

179672.42 hits per line

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

93.1
/src/modules/preprocessor/transformers/transformProgramToFunctionDeclaration.ts
1
import { posix as posixPath } from 'path';
52✔
2
import type es from 'estree';
3

4
import { defaultExportLookupName } from '../../../stdlib/localImport.prelude';
5
import assert from '../../../utils/assert';
6
import * as create from '../../../utils/ast/astCreator';
7
import { getModuleDeclarationSource } from '../../../utils/ast/helpers';
8
import {
9
  isDeclaration,
10
  isDirective,
11
  isImportDeclaration,
12
  isModuleDeclaration,
13
  isStatement,
14
} from '../../../utils/ast/typeGuards';
15
import { isSourceModule } from '../../utils';
16
import {
17
  createImportedNameDeclaration,
18
  createListCallExpression,
19
  createPairCallExpression,
20
} from '../constructors/contextSpecificConstructors';
21
import {
22
  transformFilePathToValidFunctionName,
23
  transformFunctionNameToInvokedFunctionResultVariableName,
24
} from '../filePaths';
25
import { InternalRuntimeError } from '../../../errors/base';
26

27
type ImportSpecifier = es.ImportSpecifier | es.ImportDefaultSpecifier | es.ImportNamespaceSpecifier;
28

29
export const getInvokedFunctionResultVariableNameToImportSpecifiersMap = (
56✔
30
  nodes: es.ModuleDeclaration[],
31
  currentDirPath: string,
32
): Record<string, ImportSpecifier[]> => {
33
  const invokedFunctionResultVariableNameToImportSpecifierMap: Record<string, ImportSpecifier[]> =
34
    {};
1,030✔
35
  nodes.forEach((node: es.ModuleDeclaration): void => {
1,030✔
36
    // Only ImportDeclaration nodes specify imported names.
37
    if (!isImportDeclaration(node)) return;
76✔
38

39
    const importSource = getModuleDeclarationSource(node);
37✔
40

41
    // Only handle import declarations for non-Source modules.
42
    if (isSourceModule(importSource)) {
37✔
43
      return;
15✔
44
    }
45
    // Different import sources can refer to the same file. For example,
46
    // both './b.js' & '../dir/b.js' can refer to the same file if the
47
    // current file path is '/dir/a.js'. To ensure that every file is
48
    // processed only once, we resolve the import source against the
49
    // current file path to get the absolute file path of the file to
50
    // be imported. Since the absolute file path is guaranteed to be
51
    // unique, it is also the canonical file path.
52
    const importFilePath = posixPath.resolve(currentDirPath, importSource);
22✔
53
    // Even though we limit the chars that can appear in Source file
54
    // paths, some chars in file paths (such as '/') cannot be used
55
    // in function names. As such, we substitute illegal chars with
56
    // legal ones in a manner that gives us a bijective mapping from
57
    // file paths to function names.
58
    const importFunctionName = transformFilePathToValidFunctionName(importFilePath);
22✔
59
    // In the top-level environment of the resulting program, for every
60
    // imported file, we will end up with two different names; one for
61
    // the function declaration, and another for the variable holding
62
    // the result of invoking the function. The former is represented
63
    // by 'importFunctionName', while the latter is represented by
64
    // 'invokedFunctionResultVariableName'. Since multiple files can
65
    // import the same file, yet we only want the code in each file to
66
    // be evaluated a single time (and share the same state), we need to
67
    // evaluate the transformed functions (of imported files) only once
68
    // in the top-level environment of the resulting program, then pass
69
    // the result (the exported names) into other transformed functions.
70
    // Having the two different names helps us to achieve this objective.
71
    const invokedFunctionResultVariableName =
22✔
72
      transformFunctionNameToInvokedFunctionResultVariableName(importFunctionName);
73
    // If this is the file ImportDeclaration node for the canonical
74
    // file path, instantiate the entry in the map.
75
    if (
22✔
76
      invokedFunctionResultVariableNameToImportSpecifierMap[invokedFunctionResultVariableName] ===
77
      undefined
78
    ) {
79
      invokedFunctionResultVariableNameToImportSpecifierMap[invokedFunctionResultVariableName] = [];
19✔
80
    }
81
    invokedFunctionResultVariableNameToImportSpecifierMap[invokedFunctionResultVariableName].push(
22✔
82
      ...node.specifiers,
83
    );
84
  });
85
  return invokedFunctionResultVariableNameToImportSpecifierMap;
1,030✔
86
};
87

88
const getIdentifier = (node: es.Declaration): es.Identifier | null => {
56✔
89
  switch (node.type) {
21!
90
    case 'FunctionDeclaration':
91
      assert(
9✔
92
        node.id !== null,
93
        'Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.',
94
      );
95
      return node.id;
9✔
96
    case 'VariableDeclaration':
97
      const id = node.declarations[0].id;
12✔
98
      // In Source, variable names are Identifiers.
99
      assert(
12✔
100
        id.type === 'Identifier',
101
        `Expected variable name to be an Identifier, but was ${id.type} instead.`,
102
      );
103
      return id;
12✔
104
    case 'ClassDeclaration':
NEW
105
      throw new InternalRuntimeError('Exporting of class is not supported.', node);
×
106
  }
107
};
108

109
const getExportedNameToIdentifierMap = (
56✔
110
  nodes: es.ModuleDeclaration[],
111
): Record<string, es.Identifier> => {
112
  const exportedNameToIdentifierMap: Record<string, es.Identifier> = {};
32✔
113
  nodes.forEach((node: es.ModuleDeclaration): void => {
32✔
114
    // Only ExportNamedDeclaration nodes specify exported names.
115
    if (node.type !== 'ExportNamedDeclaration') {
53✔
116
      return;
29✔
117
    }
118
    if (node.declaration) {
24✔
119
      const identifier = getIdentifier(node.declaration);
19✔
120
      if (identifier === null) {
19!
121
        return;
×
122
      }
123
      // When an ExportNamedDeclaration node has a declaration, the
124
      // identifier is the same as the exported name (i.e., no renaming).
125
      const exportedName = identifier.name;
19✔
126
      exportedNameToIdentifierMap[exportedName] = identifier;
19✔
127
    } else {
128
      // When an ExportNamedDeclaration node does not have a declaration,
129
      // it contains a list of names to export, i.e., export { a, b as c, d };.
130
      // Exported names can be renamed using the 'as' keyword. As such, the
131
      // exported names and their corresponding identifiers might be different.
132
      node.specifiers.forEach((node: es.ExportSpecifier): void => {
5✔
133
        const exportedName = node.exported.name;
15✔
134
        const identifier = node.local;
15✔
135
        exportedNameToIdentifierMap[exportedName] = identifier;
15✔
136
      });
137
    }
138
  });
139
  return exportedNameToIdentifierMap;
32✔
140
};
141

142
const getDefaultExportExpression = (
56✔
143
  nodes: es.ModuleDeclaration[],
144
  exportedNameToIdentifierMap: Partial<Record<string, es.Identifier>>,
145
): es.Expression | null => {
146
  let defaultExport:
147
    | es.MaybeNamedFunctionDeclaration
148
    | es.MaybeNamedClassDeclaration
149
    | es.Expression
150
    | null = null;
32✔
151

152
  // Handle default exports which are parsed as ExportNamedDeclaration AST nodes.
153
  // 'export { name as default };' is equivalent to 'export default name;' but
154
  // is represented by an ExportNamedDeclaration node instead of an
155
  // ExportedDefaultDeclaration node.
156
  //
157
  // NOTE: If there is a named export representing the default export, its entry
158
  // in the map must be removed to prevent it from being treated as a named export.
159
  if (exportedNameToIdentifierMap['default'] !== undefined) {
32✔
160
    defaultExport = exportedNameToIdentifierMap['default'];
1✔
161
    delete exportedNameToIdentifierMap['default'];
1✔
162
  }
163

164
  nodes.forEach((node: es.ModuleDeclaration): void => {
32✔
165
    // Only ExportDefaultDeclaration nodes specify the default export.
166
    if (node.type !== 'ExportDefaultDeclaration') {
53✔
167
      return;
43✔
168
    }
169
    // This should never occur because multiple default exports should have
170
    // been caught by the Acorn parser when parsing into an AST.
171
    assert(defaultExport === null, 'Encountered multiple default exports!');
10✔
172
    if (isDeclaration(node.declaration)) {
10✔
173
      const identifier = getIdentifier(node.declaration);
2✔
174
      if (identifier === null) {
2!
175
        return;
×
176
      }
177
      // When an ExportDefaultDeclaration node has a declaration, the
178
      // identifier is the same as the exported name (i.e., no renaming).
179
      defaultExport = identifier;
2✔
180
    } else {
181
      // When an ExportDefaultDeclaration node does not have a declaration,
182
      // it has an expression.
183
      defaultExport = node.declaration;
8✔
184
    }
185
  });
186
  return defaultExport;
32✔
187
};
188

189
export const createAccessImportStatements = (
56✔
190
  invokedFunctionResultVariableNameToImportSpecifiersMap: Record<string, ImportSpecifier[]>,
191
): es.VariableDeclaration[] => {
192
  const importDeclarations: es.VariableDeclaration[] = [];
1,030✔
193
  for (const [invokedFunctionResultVariableName, importSpecifiers] of Object.entries(
1,030✔
194
    invokedFunctionResultVariableNameToImportSpecifiersMap,
195
  )) {
196
    importSpecifiers.forEach((importSpecifier: ImportSpecifier): void => {
19✔
197
      let importDeclaration;
198
      switch (importSpecifier.type) {
27!
199
        case 'ImportSpecifier':
200
          importDeclaration = createImportedNameDeclaration(
23✔
201
            invokedFunctionResultVariableName,
202
            importSpecifier.local,
203
            importSpecifier.imported.name,
204
          );
205
          break;
23✔
206
        case 'ImportDefaultSpecifier':
207
          importDeclaration = createImportedNameDeclaration(
4✔
208
            invokedFunctionResultVariableName,
209
            importSpecifier.local,
210
            defaultExportLookupName,
211
          );
212
          break;
4✔
213
        case 'ImportNamespaceSpecifier':
214
          // In order to support namespace imports, Source would need to first support objects.
215
          throw new Error('Namespace imports are not supported.');
×
216
      }
217
      importDeclarations.push(importDeclaration);
27✔
218
    });
219
  }
220
  return importDeclarations;
1,030✔
221
};
222

223
const createReturnListArguments = (
56✔
224
  exportedNameToIdentifierMap: Record<string, es.Identifier>,
225
): Array<es.Expression | es.SpreadElement> => {
226
  return Object.entries(exportedNameToIdentifierMap).map(
32✔
227
    ([exportedName, identifier]: [string, es.Identifier]): es.CallExpression => {
228
      const head = create.literal(exportedName);
33✔
229
      const tail = identifier;
33✔
230
      return createPairCallExpression(head, tail);
33✔
231
    },
232
  );
233
};
234

235
const removeDirectives = (
56✔
236
  nodes: Array<es.Directive | es.Statement | es.ModuleDeclaration>,
237
): Array<es.Statement | es.ModuleDeclaration> => {
238
  return nodes.filter(
32✔
239
    (
240
      node: es.Directive | es.Statement | es.ModuleDeclaration,
241
    ): node is es.Statement | es.ModuleDeclaration => !isDirective(node),
82✔
242
  );
243
};
244

245
const removeModuleDeclarations = (
56✔
246
  nodes: Array<es.Statement | es.ModuleDeclaration>,
247
): es.Statement[] => {
248
  const statements: es.Statement[] = [];
32✔
249
  nodes.forEach((node: es.Statement | es.ModuleDeclaration): void => {
32✔
250
    if (isStatement(node)) {
82✔
251
      statements.push(node);
29✔
252
      return;
29✔
253
    }
254
    // If there are declaration nodes that are child nodes of the
255
    // ModuleDeclaration nodes, we add them to the processed statements
256
    // array so that the declarations are still part of the resulting
257
    // program.
258
    switch (node.type) {
53!
259
      case 'ImportDeclaration':
260
        break;
19✔
261
      case 'ExportNamedDeclaration':
262
        if (node.declaration) {
24✔
263
          statements.push(node.declaration);
19✔
264
        }
265
        break;
24✔
266
      case 'ExportDefaultDeclaration':
267
        if (isDeclaration(node.declaration)) {
10✔
268
          statements.push(node.declaration);
2✔
269
        }
270
        break;
10✔
271
      case 'ExportAllDeclaration':
272
        throw new Error('Not implemented yet.');
×
273
    }
274
  });
275
  return statements;
32✔
276
};
277

278
/**
279
 * Transforms the given program into a function declaration. This is done
280
 * so that every imported module has its own scope (since functions have
281
 * their own scope).
282
 *
283
 * @param program         The program to be transformed.
284
 * @param currentFilePath The file path of the current program.
285
 */
286
export const transformProgramToFunctionDeclaration = (
56✔
287
  program: es.Program,
288
  currentFilePath: string,
289
): es.FunctionDeclaration => {
290
  const moduleDeclarations = program.body.filter(isModuleDeclaration);
32✔
291
  const currentDirPath = posixPath.resolve(currentFilePath, '..');
32✔
292

293
  // Create variables to hold the imported statements.
294
  const invokedFunctionResultVariableNameToImportSpecifiersMap =
295
    getInvokedFunctionResultVariableNameToImportSpecifiersMap(moduleDeclarations, currentDirPath);
32✔
296
  const accessImportStatements = createAccessImportStatements(
32✔
297
    invokedFunctionResultVariableNameToImportSpecifiersMap,
298
  );
299

300
  // Create the return value of all exports for the function.
301
  const exportedNameToIdentifierMap = getExportedNameToIdentifierMap(moduleDeclarations);
32✔
302
  const defaultExportExpression = getDefaultExportExpression(
32✔
303
    moduleDeclarations,
304
    exportedNameToIdentifierMap,
305
  );
306
  const defaultExport = defaultExportExpression ?? create.literal(null);
32✔
307
  const namedExports = createListCallExpression(
32✔
308
    createReturnListArguments(exportedNameToIdentifierMap),
309
  );
310
  const returnStatement = create.returnStatement(
32✔
311
    createPairCallExpression(defaultExport, namedExports),
312
  );
313

314
  // Assemble the function body.
315
  const programStatements = removeModuleDeclarations(removeDirectives(program.body));
32✔
316
  const functionBody = [...accessImportStatements, ...programStatements, returnStatement];
32✔
317

318
  // Determine the function name based on the absolute file path.
319
  const functionName = transformFilePathToValidFunctionName(currentFilePath);
32✔
320

321
  // Set the equivalent variable names of imported modules as the function parameters.
322
  const functionParams = Object.keys(invokedFunctionResultVariableNameToImportSpecifiersMap).map(
32✔
323
    name => create.identifier(name),
13✔
324
  );
325

326
  return create.functionDeclaration(
32✔
327
    create.identifier(functionName),
328
    functionParams,
329
    create.blockStatement(functionBody),
330
  );
331
};
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