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

source-academy / js-slang / 23995741899

05 Apr 2026 06:14AM UTC coverage: 77.093% (+0.002%) from 77.091%
23995741899

push

github

web-flow
Upgrade to TypeScript 6 and Prettier improvements (#1936)

* Upgrade TypeScript to v6

* Fix import source

* Fix tsconfig

* Fix preexisting type errors

* Remove scm-slang

* Bump node types

* Fix tsconfig

* Fix node types specifier

* Enable trailing commas

* Enable semicolons

* Check and commit files with changed line numbers

* Update Yarn to 4.13.0

* Remove unneeded sicp package deps

3112 of 4282 branches covered (72.68%)

Branch coverage included in aggregate %.

3761 of 5218 new or added lines in 152 files covered. (72.08%)

26 existing lines in 9 files now uncovered.

7136 of 9011 relevant lines covered (79.19%)

175254.05 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';
50✔
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

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

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

38
    const importSource = getModuleDeclarationSource(node);
38✔
39

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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