• 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

95.6
/src/validator/validator.ts
1
import type es from 'estree';
2

3
import { ConstAssignment, UndefinedVariable } from '../errors/errors';
4
import { NoAssignmentToForVariable } from '../errors/validityErrors';
5
import { parse } from '../parser/parser';
6
import type { Context, Node, NodeWithInferredType } from '../types';
7
import { getSourceVariableDeclaration } from '../utils/ast/helpers';
8
import { ancestor, base, type FullWalkerCallback } from '../utils/ast/walkers';
9
import {
10
  getFunctionDeclarationNamesInProgram,
11
  getIdentifiersInNativeStorage,
12
  getIdentifiersInProgram,
13
  getNativeIds,
14
  type NativeIds,
15
} from '../utils/uniqueIds';
16

17
class Declaration {
18
  public accessedBeforeDeclaration: boolean = false;
114,938✔
19
  constructor(public isConstant: boolean) {}
114,938✔
20
}
21

22
export function validateAndAnnotate(
23
  program: es.Program,
24
  context: Context,
25
): NodeWithInferredType<es.Program> {
26
  const accessedBeforeDeclarationMap = new Map<Node, Map<string, Declaration>>();
1,696✔
27
  const scopeHasCallExpressionMap = new Map<Node, boolean>();
1,696✔
28
  function processBlock(node: es.Program | es.BlockStatement) {
29
    const initialisedIdentifiers = new Map<string, Declaration>();
45,998✔
30
    for (const statement of node.body) {
45,998✔
31
      if (statement.type === 'VariableDeclaration') {
88,010✔
32
        initialisedIdentifiers.set(
7,071✔
33
          getSourceVariableDeclaration(statement).id.name,
34
          new Declaration(statement.kind === 'const'),
35
        );
36
      } else if (statement.type === 'FunctionDeclaration') {
80,939✔
37
        if (statement.id === null) {
34,127!
38
          throw new Error(
×
39
            'Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.',
40
          );
41
        }
42
        initialisedIdentifiers.set(statement.id.name, new Declaration(true));
34,127✔
43
      }
44
    }
45
    scopeHasCallExpressionMap.set(node, false);
45,998✔
46
    accessedBeforeDeclarationMap.set(node, initialisedIdentifiers);
45,998✔
47
  }
48
  function processFunction(node: es.FunctionDeclaration | es.ArrowFunctionExpression) {
49
    accessedBeforeDeclarationMap.set(
45,755✔
50
      node,
51
      new Map((node.params as es.Identifier[]).map(id => [id.name, new Declaration(false)])),
73,383✔
52
    );
53
    scopeHasCallExpressionMap.set(node, false);
45,755✔
54
  }
55

56
  // initialise scope of variables
57
  ancestor(program as Node, {
1,696✔
58
    Program: processBlock,
59
    BlockStatement: processBlock,
60
    FunctionDeclaration: processFunction,
61
    ArrowFunctionExpression: processFunction,
62
    ForStatement(forStatement: es.ForStatement, _ancestors: Node[]) {
63
      const init = forStatement.init!;
358✔
64
      if (init.type === 'VariableDeclaration') {
358✔
65
        accessedBeforeDeclarationMap.set(
357✔
66
          forStatement,
67
          new Map([
68
            [getSourceVariableDeclaration(init).id.name, new Declaration(init.kind === 'const')],
69
          ]),
70
        );
71
        scopeHasCallExpressionMap.set(forStatement, false);
357✔
72
      }
73
    },
74
  });
75

76
  function validateIdentifier(id: es.Identifier, ancestors: Node[]) {
77
    const name = id.name;
443,670✔
78
    const lastAncestor: Node = ancestors[ancestors.length - 2];
443,670✔
79
    for (let i = ancestors.length - 1; i >= 0; i--) {
443,670✔
80
      const a = ancestors[i];
2,816,360✔
81
      const map = accessedBeforeDeclarationMap.get(a);
2,816,360✔
82
      if (map?.has(name)) {
2,816,360✔
83
        map.get(name)!.accessedBeforeDeclaration = true;
353,127✔
84
        if (lastAncestor.type === 'AssignmentExpression' && lastAncestor.left === id) {
353,127✔
85
          if (map.get(name)!.isConstant) {
736✔
86
            context.errors.push(new ConstAssignment(lastAncestor, name));
1✔
87
          }
88
          if (a.type === 'ForStatement' && a.init !== lastAncestor && a.update !== lastAncestor) {
736✔
89
            context.errors.push(new NoAssignmentToForVariable(lastAncestor));
4✔
90
          }
91
        }
92
        break;
353,127✔
93
      }
94
    }
95
  }
96
  const customWalker = {
1,696✔
97
    ...base,
98
    VariableDeclarator(node: es.VariableDeclarator, st: never, c: FullWalkerCallback<never>) {
99
      // don't visit the id
100
      if (node.init) {
7,428!
101
        c(node.init, st, 'Expression');
7,428✔
102
      }
103
    },
104
  };
105
  ancestor(
1,696✔
106
    program,
107
    {
108
      VariableDeclaration(node: NodeWithInferredType<es.VariableDeclaration>, ancestors: Node[]) {
109
        const lastAncestor = ancestors[ancestors.length - 2];
7,428✔
110
        const { name } = getSourceVariableDeclaration(node).id;
7,428✔
111
        const accessedBeforeDeclaration = accessedBeforeDeclarationMap
7,428✔
112
          .get(lastAncestor)!
113
          .get(name)!.accessedBeforeDeclaration;
114
        node.typability = accessedBeforeDeclaration ? 'Untypable' : 'NotYetTyped';
7,428✔
115
      },
116
      Identifier: validateIdentifier,
117
      FunctionDeclaration(node: NodeWithInferredType<es.FunctionDeclaration>, ancestors: Node[]) {
118
        // a function declaration can be typed if there are no function calls in the same scope before it
119
        const lastAncestor = ancestors[ancestors.length - 2];
34,127✔
120
        node.typability = scopeHasCallExpressionMap.get(lastAncestor) ? 'Untypable' : 'NotYetTyped';
34,127✔
121
      },
122
      Pattern(node: es.Pattern, ancestors: Node[]) {
123
        if (node.type === 'Identifier') {
108,273✔
124
          validateIdentifier(node, ancestors);
108,252✔
125
        } else if (node.type === 'MemberExpression') {
21✔
126
          if (node.object.type === 'Identifier') {
16✔
127
            validateIdentifier(node.object, ancestors);
12✔
128
          }
129
        }
130
      },
131
      CallExpression(call: es.CallExpression, ancestors: Node[]) {
132
        for (let i = ancestors.length - 1; i >= 0; i--) {
156,352✔
133
          const a = ancestors[i];
728,059✔
134
          if (scopeHasCallExpressionMap.has(a)) {
728,059✔
135
            scopeHasCallExpressionMap.set(a, true);
156,352✔
136
            break;
156,352✔
137
          }
138
        }
139
      },
140
    },
141
    customWalker,
142
  );
143

144
  /*
145
  simple(program, {
146
    VariableDeclaration(node: TypeAnnotatedNode<es.VariableDeclaration>) {
147
      console.log(getVariableDecarationName(node) + " " + node.typability);
148
    },
149
    FunctionDeclaration(node: TypeAnnotatedNode<es.FunctionDeclaration>) {
150
      console.log(node.id!.name + " " + node.typability);
151
    }
152
  })
153

154
   */
155
  return program;
1,696✔
156
}
157

158
export function checkProgramForUndefinedVariables(program: es.Program, context: Context) {
159
  const usedIdentifiers = new Set<string>([
1,057✔
160
    ...getIdentifiersInProgram(program),
161
    ...getIdentifiersInNativeStorage(context.nativeStorage),
162
  ]);
163
  const globalIds = getNativeIds(program, usedIdentifiers);
1,057✔
164
  return checkForUndefinedVariables(program, context, globalIds, false);
1,057✔
165
}
166

167
export function checkForUndefinedVariables(
168
  program: es.Program,
169
  context: Context,
170
  globalIds: NativeIds,
171
  skipUndefined: boolean,
172
) {
173
  const preludes = context.prelude
1,820✔
174
    ? getFunctionDeclarationNamesInProgram(parse(context.prelude, context)!)
175
    : new Set<string>();
176

177
  const env = context.runtime.environments[0].head || {};
1,820!
178

179
  const builtins = context.nativeStorage.builtins;
1,820✔
180
  const identifiersIntroducedByNode = new Map<es.Node, Set<string>>();
1,820✔
181
  function processBlock(node: es.Program | es.BlockStatement) {
182
    const identifiers = new Set<string>();
45,234✔
183
    for (const statement of node.body) {
45,234✔
184
      if (statement.type === 'VariableDeclaration') {
86,488✔
185
        const {
186
          id: { name },
7,033✔
187
        } = getSourceVariableDeclaration(statement);
188
        identifiers.add(name);
7,033✔
189
      } else if (statement.type === 'FunctionDeclaration') {
79,455✔
190
        if (statement.id === null) {
33,395!
191
          throw new Error(
×
192
            'Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.',
193
          );
194
        }
195
        identifiers.add(statement.id.name);
33,395✔
196
      } else if (statement.type === 'ImportDeclaration') {
46,060✔
197
        for (const specifier of statement.specifiers) {
10✔
198
          identifiers.add(specifier.local.name);
10✔
199
        }
200
      }
201
    }
202
    identifiersIntroducedByNode.set(node, identifiers);
45,234✔
203
  }
204
  function processFunction(
205
    node: es.FunctionDeclaration | es.ArrowFunctionExpression,
206
    _ancestors: es.Node[],
207
  ) {
208
    identifiersIntroducedByNode.set(
44,821✔
209
      node,
210
      new Set(
211
        node.params.map(id =>
212
          id.type === 'Identifier'
71,765✔
213
            ? id.name
214
            : ((id as es.RestElement).argument as es.Identifier).name,
215
        ),
216
      ),
217
    );
218
  }
219
  const identifiersToAncestors = new Map<es.Identifier, es.Node[]>();
1,820✔
220
  ancestor(program, {
1,820✔
221
    Program: processBlock,
222
    BlockStatement: processBlock,
223
    FunctionDeclaration: processFunction,
224
    ArrowFunctionExpression: processFunction,
225
    ForStatement(forStatement: es.ForStatement) {
226
      const init = forStatement.init!;
354✔
227
      if (init.type === 'VariableDeclaration') {
354✔
228
        const {
229
          id: { name },
353✔
230
        } = getSourceVariableDeclaration(init);
231
        identifiersIntroducedByNode.set(forStatement, new Set([name]));
353✔
232
      }
233
    },
234
    Identifier(identifier: es.Identifier, ancestors: es.Node[]) {
235
      identifiersToAncestors.set(identifier, [...ancestors]);
394,374✔
236
    },
237
    Pattern(node: es.Pattern, ancestors: es.Node[]) {
238
      if (node.type === 'Identifier') {
113,294✔
239
        identifiersToAncestors.set(node, [...ancestors]);
113,283✔
240
      } else if (node.type === 'MemberExpression') {
11✔
241
        if (node.object.type === 'Identifier') {
6✔
242
          identifiersToAncestors.set(node.object, [...ancestors]);
3✔
243
        }
244
      }
245
    },
246
  });
247
  const nativeInternalNames = new Set(Object.values(globalIds).map(({ name }) => name));
18,200✔
248

249
  for (const [identifier, ancestors] of identifiersToAncestors) {
1,820✔
250
    const name = identifier.name;
442,915✔
251
    const isCurrentlyDeclared = ancestors.some(a => identifiersIntroducedByNode.get(a)?.has(name));
1,560,113✔
252
    if (isCurrentlyDeclared) {
442,915✔
253
      continue;
352,692✔
254
    }
255
    const isPreviouslyDeclared = context.nativeStorage.previousProgramsIdentifiers.has(name);
90,223✔
256
    if (isPreviouslyDeclared) {
90,223✔
257
      continue;
213✔
258
    }
259
    const isBuiltin = builtins.has(name);
90,010✔
260
    if (isBuiltin) {
90,010✔
261
      continue;
88,241✔
262
    }
263
    const isPrelude = preludes.has(name);
1,769✔
264
    if (isPrelude) {
1,769✔
265
      continue;
30✔
266
    }
267
    const isInEnv = name in env;
1,739✔
268
    if (isInEnv) {
1,739!
NEW
269
      continue;
×
270
    }
271
    const isNativeId = nativeInternalNames.has(name);
1,739✔
272
    if (!isNativeId && !skipUndefined) {
1,739✔
273
      throw new UndefinedVariable(name, identifier);
12✔
274
    }
275
  }
276
}
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