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

source-academy / js-slang / 24713997105

21 Apr 2026 09:09AM UTC coverage: 78.523% (+0.1%) from 78.391%
24713997105

Pull #1893

github

web-flow
Merge 6359230cf into 1402017f8
Pull Request #1893: Error Handling and Stringify Changes

3115 of 4184 branches covered (74.45%)

Branch coverage included in aggregate %.

793 of 967 new or added lines in 76 files covered. (82.01%)

20 existing lines in 11 files now uncovered.

7049 of 8760 relevant lines covered (80.47%)

173719.17 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 TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc