Files
scriptc/packages/compiler/src/frontend/early-errors.ts
T
Chris Tate 352842f020 Scope early errors to function bodies and class strictness, and match the upstream compareArray reads
- A function body or class static block treats its top-level function
  declarations as var-scoped: duplicates are legal in strict code, and a
  let/const/class of the same name is SC0006.
- Strictness is the code around a node: a function declaration's own
  "use strict" no longer makes its declaring context strict.
- Every part of a class is strict; object literal methods and accessors
  are strict only by their own prologue or their surroundings, and an
  arrow function's block body can carry "use strict".
- The bare compareArray reads b.length, then a.length, and a.length again
  before every element pair, converting it as the upstream `i < a.length`
  does, so a string length compares its elements.
- Tests that relied on the old handling of undeclared names now use
  constructs that are still refused or still compile: the installed-entry
  diagnostics fixture reports a syntax error (a read of an undeclared name
  is now Node's run-time ReferenceError), and static-objects-order's
  loops.js stores from its for-in head into `undefined` instead of an
  undeclared name (a store into an undeclared name refuses its package with
  SC1091).
2026-10-29 08:37:25 -05:00

385 lines
15 KiB
TypeScript

/* ECMAScript early errors the TypeScript checker does not model.
*
* Static semantics reject a program before it runs, and until this pass
* existed scriptc borrowed that rejection from whatever tsc happened to
* report (`Let x` declarations at an odd position, a redeclared block name
* TypeScript refuses). Test262's negative-parse suites show where the
* borrowing stops: 1,198 programs the spec makes SyntaxErrors compile — the
* constructs below are the ones the corpus covers, in descending variant
* count (plan §7.7 D-4 option (c)).
*
* One code per construct, so C1's checker relaxation stays safe: relaxing a
* tsc code that doubles as an early error removes the ONLY rejection some
* parse-negative test had, which is why these codes live beside
* JS_RELAXED_TSC_CODES in tsc-codes.ts and never depend on it.
*
* Scope: JavaScript sources only. A TypeScript source is type-checked, and
* its checker diagnostics (TS2451 and friends) are the gate; re-reporting
* the same collision under a second code would only duplicate them.
*
* The pass runs at preflight, before any lowering, and reports through
* SC0xxx — the preflight band — so a rejected program fails the same way a
* tsc error does. */
import * as ts from "./ts7/adapter.js";
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
import type { SrcLoc } from "../ir/ir.js";
import { isJsSourceFileName } from "./tsc-codes.js";
/** The early-error codes. SC0005 and SC0006 are the Block/CaseBlock static
* semantics (a statement list may not declare one lexical name twice, and a
* lexical name may not be `var`-declared in the same list); SC0007 is a
* declaration where the grammar admits only a Statement. */
export const EARLY_ERROR_CODES = ["SC0005", "SC0006", "SC0007"] as const;
const NO_DUPLICATE_LEXICAL = "SC0005";
const LEXICAL_VAR_COLLISION = "SC0006";
const NOT_A_STATEMENT = "SC0007";
/** Whether the code around a node is strict — the one input this pass needs
* from outside: Node's own module classification (program.ts's
* isNodeEsmFile7: fixed extensions, package type, then syntax markers). The
* checker's external-module bit cannot answer it, because
* moduleDetection=force gives every file module scope, CommonJS included. */
export type IsStrictCode = (sf: ts.SourceFile, node: ts.Node) => boolean;
export function earlyErrorDiagnostics7(
files: readonly ts.SourceFile[],
isModuleCode: (sf: ts.SourceFile) => boolean,
): ScrDiagnostic[] {
const diags: ScrDiagnostic[] = [];
const isStrict: IsStrictCode = (sf, node) => isStrictCode(sf, node, isModuleCode);
for (const sf of files) {
if (!isJsSourceFileName(sf.fileName)) continue;
// Iterative walk: a deeply nested program must not overflow the stack
// before the lowering's own nesting fence runs.
const stack: ts.Node[] = [sf];
while (stack.length > 0) {
const node = stack.pop()!;
checkStatementListScope(sf, node, diags, isStrict);
checkStatementSlot(node, diags, isStrict);
ts.forEachChild(node, (child) => {
stack.push(child);
return undefined;
});
}
}
return diags;
}
function locOf(node: ts.Node): SrcLoc {
const sf = ts.getSourceFileOfNode(node);
return { file: sf.fileName, start: node.getStart(sf), end: node.end };
}
function report(diags: ScrDiagnostic[], code: EarlyErrorCode, node: ts.Node, message: string) {
diags.push({ code, message, loc: locOf(node) });
}
type EarlyErrorCode = (typeof EARLY_ERROR_CODES)[number];
/* ── SC0005 / SC0006: Block and CaseBlock static semantics ───────────────
*
* Block : { StatementList }
* CaseBlock : { CaseClauses_opt DefaultClause CaseClauses_opt }
*
* * It is a Syntax Error if the LexicallyDeclaredNames of StatementList
* contains any duplicate entries.
* * It is a Syntax Error if any element of the LexicallyDeclaredNames of
* StatementList also occurs in the VarDeclaredNames of StatementList.
*
* A case block is ONE lexical scope across its clauses, and VarDeclaredNames
* reach into nested blocks (a `var` anywhere below the block belongs to it)
* but never into a function or class body. The duplicate rule's one carve-out
* is Annex B's B.3.3.1: two plain function declarations of the same name in
* the same block are legal in non-strict code.
*
* A function body and a class static block have the same two rules over
* their top-level declarations (FunctionStatementList and
* ClassStaticBlockStatementList), but there a function declaration is
* var-scoped: it is one of the VarDeclaredNames, so two of them are legal in
* strict code too, while `let f; function f() {}` still collides. */
function checkStatementListScope(
sf: ts.SourceFile,
node: ts.Node,
diags: ScrDiagnostic[],
isStrict: IsStrictCode,
): void {
let statements: readonly ts.Statement[] | null = null;
if (ts.isBlock(node)) statements = node.statements.nodes;
else if (ts.isCaseBlock(node)) {
statements = node.clauses.nodes.flatMap((clause) => clause.statements.nodes);
}
if (statements === null) return;
if (statements.length < 2) return;
const functionBody = isFunctionBody(node);
const lexical = new Map<string, ts.Node[]>();
for (const stmt of statements) {
for (const [name, decl] of lexicalDeclarationsOf(stmt, functionBody)) {
const list = lexical.get(name);
if (list === undefined) lexical.set(name, [decl]);
else list.push(decl);
}
}
if (lexical.size === 0) return;
const varNames = new Set<string>();
for (const stmt of statements) {
collectVarDeclaredNames(stmt, varNames);
if (functionBody && ts.isFunctionDeclaration(stmt) && stmt.name !== undefined)
varNames.add(stmt.name.text);
}
for (const [name, decls] of lexical) {
if (varNames.has(name)) {
report(
diags,
LEXICAL_VAR_COLLISION,
decls[0]!,
functionBody
? `the lexical declaration '${name}' collides with a 'var' or function declaration of the same name in this function body`
: `the lexical declaration '${name}' collides with a 'var' declaration of the same name in this block`,
);
continue;
}
if (decls.length < 2) continue;
// Annex B B.3.3.1: duplicate plain function declarations are the web's
// legacy, and only non-strict code keeps them.
if (decls.every((d) => ts.isFunctionDeclaration(d) && !isEnhancedFunction(d))) {
if (!isStrict(sf, decls[0]!)) continue;
}
report(
diags,
NO_DUPLICATE_LEXICAL,
decls[1]!,
`the declaration '${name}' is already declared in this block's statement list`,
);
}
}
/** Whether a block is the body of a function or a class static block, whose
* top-level function declarations are var-scoped. */
function isFunctionBody(node: ts.Node): boolean {
const parent = node.parent;
if (parent === undefined || !ts.isBlock(node)) return false;
if (ts.isClassStaticBlockDeclaration(parent)) return true;
return ts.isFunctionLike(parent) && (parent as { body?: ts.Node }).body === node;
}
/** The names a DIRECT statement of a statement list declares lexically
* (let/const/using, class, and every function declaration form outside a
* function body), paired with the node to point the diagnostic at. */
function lexicalDeclarationsOf(
stmt: ts.Statement,
functionBody: boolean,
): Array<[string, ts.Node]> {
const names: Array<[string, ts.Node]> = [];
if (ts.isVariableStatement(stmt)) {
if ((stmt.declarationList.flags & ts.NodeFlags.BlockScoped) === 0) return names;
for (const decl of stmt.declarationList.declarations.nodes)
collectBindingNames(decl.name, names);
return names;
}
if (ts.isClassDeclaration(stmt) || (ts.isFunctionDeclaration(stmt) && !functionBody)) {
const name = stmt.name;
if (name !== undefined && ts.isIdentifier(name)) names.push([name.text, stmt]);
}
return names;
}
function collectBindingNames(name: ts.BindingName, into: Array<[string, ts.Node]>): void {
if (ts.isIdentifier(name)) {
into.push([name.text, name]);
return;
}
for (const element of name.elements.nodes) {
if (ts.isOmittedExpression(element)) continue;
if (ts.isBindingElement(element) && element.name !== undefined)
collectBindingNames(element.name, into);
}
}
/** VarDeclaredNames of a statement: every `var`-declared name below it,
* stopping at function and class boundaries (their declarations are not
* var-scoped here). */
function collectVarDeclaredNames(root: ts.Node, into: Set<string>): void {
const stack: ts.Node[] = [root];
while (stack.length > 0) {
const node = stack.pop()!;
if (ts.isFunctionLike(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node))
continue;
if (ts.isVariableDeclarationList(node) && (node.flags & ts.NodeFlags.BlockScoped) === 0) {
const names: Array<[string, ts.Node]> = [];
for (const decl of node.declarations.nodes) collectBindingNames(decl.name, names);
for (const [name] of names) into.add(name);
}
ts.forEachChild(node, (child) => {
stack.push(child);
return undefined;
});
}
}
/** `async function` and `function*` are Annex B's allowance never — only the
* plain form is. */
function isEnhancedFunction(decl: ts.FunctionDeclaration): boolean {
if (decl.asteriskToken !== undefined) return true;
return (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Async) !== 0;
}
/* ── SC0007: a declaration where the grammar admits only a Statement ─────
*
* Every `Statement` slot below admits Statement productions only, and only
* Annex B widens three of them to a plain FunctionDeclaration in non-strict
* code: the body of an `if`/`else` and a labelled statement. Async functions,
* generators, classes and lexical declarations are never admitted, and a
* LABELLED function is never admitted as an if/else body (`IsLabelledFunction`
* is an explicit early error there, so `if (x) l: function f(){}` fails even
* in sloppy code). */
type StatementSlot = "if" | "loop" | "with" | "label";
function checkStatementSlot(node: ts.Node, diags: ScrDiagnostic[], isStrict: IsStrictCode): void {
if (ts.isIfStatement(node)) {
checkStatementPosition(node.thenStatement, "if", diags, isStrict);
if (node.elseStatement !== undefined)
checkStatementPosition(node.elseStatement, "if", diags, isStrict);
return;
}
if (
ts.isWhileStatement(node) ||
ts.isDoStatement(node) ||
ts.isForStatement(node) ||
ts.isForInStatement(node) ||
ts.isForOfStatement(node)
) {
checkStatementPosition(node.statement, "loop", diags, isStrict);
return;
}
if (ts.isWithStatement(node)) {
checkStatementPosition(node.statement, "with", diags, isStrict);
return;
}
if (ts.isLabeledStatement(node)) checkStatementPosition(node.statement, "label", diags, isStrict);
}
function checkStatementPosition(
stmt: ts.Statement,
slot: StatementSlot,
diags: ScrDiagnostic[],
isStrict: IsStrictCode,
): void {
const sf = ts.getSourceFileOfNode(stmt);
// A labelled function is an early error as an if-clause, loop, or 'with'
// body in BOTH modes (Node agrees); only a plain labelled STATEMENT is
// Annex B's, and that is the `slot === "label"` case, whose terminal
// function the function branches below handle.
const labelTerminal =
slot === "label" || !ts.isLabeledStatement(stmt) ? null : labelledFunctionOf(stmt);
if (labelTerminal !== null) {
report(
diags,
NOT_A_STATEMENT,
labelTerminal,
"a labelled function declaration cannot be the body of a loop, a 'with' statement, or an 'if' clause",
);
return;
}
if (ts.isVariableStatement(stmt)) {
if ((stmt.declarationList.flags & ts.NodeFlags.BlockScoped) !== 0)
report(
diags,
NOT_A_STATEMENT,
stmt,
"a lexical declaration cannot appear in a single-statement position",
);
return;
}
if (ts.isClassDeclaration(stmt)) {
report(
diags,
NOT_A_STATEMENT,
stmt,
"a class declaration cannot appear in a single-statement position",
);
return;
}
if (!ts.isFunctionDeclaration(stmt)) return;
if (isEnhancedFunction(stmt)) {
report(
diags,
NOT_A_STATEMENT,
stmt,
"an async function or generator declaration can only appear at the top level or inside a block",
);
return;
}
// A plain function declaration: Annex B admits it as an if/else body and as
// a labelled statement, in non-strict code only, and never as a loop's or a
// 'with' statement's body.
if (slot === "loop" || slot === "with") {
report(
diags,
NOT_A_STATEMENT,
stmt,
"a function declaration can only appear at the top level, inside a block, or as the body of an if statement",
);
return;
}
if (isStrict(sf, stmt))
report(
diags,
NOT_A_STATEMENT,
stmt,
"strict mode code may not declare a function in a single-statement position",
);
}
/** The function declaration at the end of a `label:` chain, when `stmt` is a
* labelled statement around one. */
function labelledFunctionOf(stmt: ts.Statement): ts.FunctionDeclaration | null {
let cur: ts.Statement = stmt;
while (ts.isLabeledStatement(cur)) cur = cur.statement;
return ts.isFunctionDeclaration(cur) ? cur : null;
}
/* ── strictness of the code around a node ───────────────────────────────
*
* A directive prologue's "use strict" applies to the function (or script) it
* opens, an arrow function's block body included, and every part of a class is strict by
* construction. Object literal methods and accessors are ordinary functions:
* strict only by their own prologue or their surroundings. Module sources are
* strict whether they say so or not, and Node's classification — not the
* checker's forced external-module scope — decides which sources are modules.
*
* The walk starts at the node's parent: a function declaration's own "use
* strict" makes its body strict, not the code that declares it. */
function isStrictCode(
sf: ts.SourceFile,
node: ts.Node,
isModuleCode: (sf: ts.SourceFile) => boolean,
): boolean {
if (isModuleCode(sf)) return true;
for (let n: ts.Node | undefined = node.parent; n !== undefined; n = n.parent) {
if (ts.isSourceFile(n)) return hasUseStrictPrologue(n.statements.nodes);
if (ts.isClassDeclaration(n) || ts.isClassExpression(n)) return true;
if (ts.isFunctionLike(n)) {
const body = n.body;
if (body !== undefined && ts.isBlock(body) && hasUseStrictPrologue(body.statements.nodes))
return true;
}
}
return false;
}
function hasUseStrictPrologue(statements: readonly ts.Statement[]): boolean {
for (const stmt of statements) {
if (!ts.isExpressionStatement(stmt)) return false;
const expr = stmt.expression;
if (!ts.isStringLiteral(expr)) return false;
if (expr.text === "use strict") return true;
}
return false;
}