Test262 group C: early errors, undeclared reads, async-iterator relaxation, compareArray harness (#2500)

This commit is contained in:
Chris Tate
2026-10-29 15:17:08 -05:00
committed by GitHub
37 changed files with 1278 additions and 15 deletions
@@ -98,7 +98,7 @@ function coverageNotes(): string[] {
"Entries with status 'unsupported' describe where the named code is raised: forms of the construct outside the supported subset are refused with that code — not that every form of the named feature is refused. Supported forms appear as their own static entries where a table projects them.",
`The engine-free fetch projection targets Node ${fetchTarget.node} with bundled Undici ${fetchTarget.undici}. Each projected row names the differential evidence that guards it; changing the pinned Node or Undici version is an explicit profile update.`,
"The fetch profile also contains a runtime-reflected census of the selected fetch, abort, Headers, and readable-stream interfaces plus RequestInit/ResponseInit dictionary reads. Static and unsupported census rows are projected here; its explicitly out-of-scope metadata rows and adjacent-interface exclusions remain in the profile so absence is deliberate rather than ambiguous.",
"Process-level diagnostic codes are not surface entries: SC0001-SC0004 are preflight gates, SC1110 is a comptime evaluation failure, SC3001/SC3002 are backend/target tier refusals, SC9001/SC9002 are internal errors.",
"Process-level diagnostic codes are not surface entries: SC0001-SC0007 are preflight gates (SC0005-SC0007 are the ECMAScript early errors), SC1110 is a comptime evaluation failure, SC3001/SC3002 are backend/target tier refusals, SC9001/SC9002 are internal errors.",
"Entry statuses are projected for the desktop targets. The mobile targets (aarch64-apple-ios, aarch64-apple-ios-simulator, aarch64-linux-android) compile library-mode archives only: the library-admissible surface (what SC4005's async_free requirement and the library link set admit) is supported there, the executable lane refuses those triples with SC3002, and no entry outside the library-admissible surface carries a mobile support claim. iOS archives build for iOS 15.0 on darwin hosts; Android archives build against NDK API level 26.",
"No scheduling metadata is published; entry ids are the stable diff keys across releases.",
];
@@ -19,6 +19,9 @@ export const DIAGNOSTIC_CODES = {
SC0002: "project configuration incompatible with scriptc",
SC0003: "malformed imported JSON module",
SC0004: "TypeScript checker panic",
SC0005: "duplicate lexical declaration in a block or switch statement list",
SC0006: "lexical declaration colliding with a var in the same block",
SC0007: "declaration in a single-statement position",
SC1010: "package imports outside the supported set",
SC1012: "default exports/imports (residual edges)",
SC1013: "namespace imports (residual edges)",
@@ -40,6 +43,7 @@ export const DIAGNOSTIC_CODES = {
SC1071: "generators",
SC1080: "'this' outside a class method",
SC1090: "this syntax (generic construct fence)",
SC1091: "write to an undeclared name in a JavaScript source",
SC1100: "operations on 'unknown' values",
SC1101: "converting typed values to 'unknown'",
SC1110: "comptime evaluation failure",
@@ -7,8 +7,12 @@
* Code bands:
* SC0xxx preflight gates: TypeScript errors passed through (SC0001),
* project-config incompatibilities (SC0002), malformed imported
* JSON modules (SC0003), and upstream tsgo checker panics
* surfaced as source-anchored diagnostics (SC0004)
* JSON modules (SC0003), upstream tsgo checker panics surfaced
* as source-anchored diagnostics (SC0004), and the ECMAScript
* early errors the checker does not model: a duplicated or
* `var`-colliding lexical declaration in a block or switch
* statement list (SC0005/SC0006) and a declaration where the
* grammar admits only a Statement (SC0007)
* SC1xxx supported-TypeScript-not-yet: valid TS outside the current subset
* SC2xxx scriptc type rules (types we cannot compile yet)
* SC3xxx backend/target coverage: the program is valid, but the selected
@@ -687,6 +691,11 @@ export const UNSUPPORTED: Record<string, UnsupportedEntry> = {
// remains is `this` outside any method (top level, plain functions).
SC1080: { feature: "'this' outside a class method", milestone: "later" },
SC1090: { feature: "this syntax", milestone: "later" }, // generic fallback; message names the construct
SC1091: {
feature: "writing an undeclared name in a JavaScript source",
milestone: "later",
hint: "a write to an undeclared name has no native lowering: sloppy mode makes it a property on Node's global object, which a native binary has none of, and strict mode makes it throw; declare the name, or keep the module in a Node island (sloppy-mode programs route there)",
},
SC1100: {
feature: "operations on 'unknown' values",
milestone: "later",
@@ -0,0 +1,43 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { checkPreflight, loadProgram } from "./program-node.js";
/** The preflight diagnostics of a CommonJS script (sloppy unless it says
* "use strict"). */
function diagnostics(source: string) {
const directory = mkdtempSync(join(tmpdir(), "scriptc-early-errors-"));
writeFileSync(join(directory, "main.cjs"), source);
const load = loadProgram(join(directory, "main.cjs"));
try {
return checkPreflight(load).map((diagnostic) => diagnostic.code);
} finally {
load.dispose();
rmSync(directory, { recursive: true, force: true });
}
}
test.for([
// A function body's top-level function declarations are var-scoped, so
// duplicates are legal in strict code too.
'function outer() { "use strict"; function d() {} function d() {} }\nouter();',
"class C { static { function s() {} function s() {} } }\nnew C();",
// Object literal methods and accessors are sloppy unless they say otherwise.
"var o = { m() { if (true) function f() {} } };\no.m();",
"var o = { get g() { { function h() {} function h() {} } return 1; } };\no.g;",
// A function's own directive makes its body strict, not the code declaring it.
'if (true) function k() { "use strict"; }',
'{ function z() { "use strict"; } function z() {} }',
])("Node accepts: %s", (source) => {
expect(diagnostics(source)).toEqual([]);
});
test.for([
["function outer() { let d; function d() {} }\nouter();", "SC0006"],
['(() => { "use strict"; if (true) function f() {} })();', "SC0007"],
["class C { m() { if (true) function f() {} } }\nnew C().m();", "SC0007"],
['var o = { m() { "use strict"; { function h() {} function h() {} } } };\no.m();', "SC0005"],
])("Node refuses: %s", ([source, code]) => {
expect(diagnostics(source!)).toContain(code);
});
@@ -0,0 +1,384 @@
/* 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;
}
@@ -94,3 +94,23 @@ test.for([
invalid,
);
});
test("JavaScript async generators may delegate over a runtime async-iterator shape", () => {
expect(
diagnostics(
`export async function* delegated() { yield* { [Symbol.asyncIterator]: 42 }; }
export async function* looped() { for await (const value of { [Symbol.asyncIterator]() { return "text"; } }) console.dir(value); }`,
'import { delegated, looped } from "./library.js"; delegated(); looped();',
),
).toEqual([]);
});
test("TypeScript keeps the async-iterator demand", () => {
expect(
diagnostics(
"export async function* delegated() { yield* { [Symbol.asyncIterator]: 42 }; }",
'import { delegated } from "./library.js"; delegated();',
"ts",
).some((diagnostic) => diagnostic.message.includes("Symbol.asyncIterator")),
).toBe(true);
});
@@ -1043,6 +1043,27 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
);
}
}
// A `.js` READ of a name NOTHING declares. Node resolves such a name at
// run time: the access throws the catchable `ReferenceError: <name> is not
// defined` (a `typeof` test lowers earlier, in expressions/typeof.ts, and
// answers "undefined" — exactly Node's rule), so the read compiles to that
// throw and never yields a value. The value is checked-dynamic (the
// checker's error-any), which every use site carries or fences — and the
// read always unwinds before any of them observes it. A WRITE to the same
// name is not a read: it reaches rejectUnresolved and its SC1091 story.
// Typed sources keep the compile error (preflight's JS relaxation is
// `isJsSourceFileName`-gated, so no TS file reaches this branch).
// In shorthand position (`{ x }`) `resolveValueSymbol` answers the
// PROPERTY symbol, so the declaration test there is the checker's
// shorthand-VALUE symbol.
const parent = expr.parent;
const undeclared =
parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === expr
? lowerer.checker.getShorthandAssignmentValueSymbol(parent) === undefined
: lowerer.resolveValueSymbol(expr) === null;
if (undeclared && isJsSourceFile(ts.getSourceFileOfNode(expr))) {
return nsUndefRead(lowerer, expr.text, expr, contextualUndefReadType(lowerer, expr) ?? DYN);
}
// Preflight guarantees no unresolved identifiers; anything else here
// is a blocked declaration's binding (the SC2004 cascade) or a
// binding form we don't model yet.
@@ -493,6 +493,24 @@ export function lowerDynTypeofTest(
// preserving both operands' evaluation order without a narrowing test.
if (!ts.isStringLiteral(b)) continue;
const declared = lowerer.mapTypeOf(lowerer.typeOf(a.expression));
// `typeof undeclared === "..."` in a JavaScript source: nothing declares
// the name, so Node answers "undefined" without ever reading it — the
// test is that constant's comparison, never a read (a read throws).
{
let operand = a.expression;
while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
if (
ts.isIdentifier(operand) &&
isJsSourceFile(ts.getSourceFileOfNode(operand)) &&
lowerer.resolveValueSymbol(operand) === null
)
return {
kind: "boolLit",
value: (b.text === "undefined") !== negated,
type: BOOL,
loc,
};
}
let value: IrExpr;
if (declared?.kind === "dyn") {
value = lowerer.lowerExpr(a.expression);
@@ -363,6 +363,7 @@ import {
overridesDtsPath,
npmStaticDepSf7,
requireSpecOf,
isJsWriteTarget7,
resolveImport,
workspacePackageOfPath,
type CycleEdge,
@@ -15872,6 +15873,18 @@ export class Lowerer {
node: ts.Node,
fallback: string,
): never {
// A `.js` WRITE to a name nothing declares. Sloppy mode resolves it by
// creating a property on the global object — process-wide state a native
// binary has no object for, so those programs run in a Node island
// (plan D-5). Until the write is routed there it is refused by name
// rather than by the generic no-lowering text.
if (symbol === null && isJsWriteTarget7(node) && isJsSourceFile(ts.getSourceFileOfNode(node))) {
this.unsupported(
"SC1091",
node,
`writing the undeclared name '${name}' in a JavaScript source`,
);
}
if (this.isBlockedBinding(symbol)) {
this.pushDiag(blockedBindingUseDiag(name, locOf(node)));
throw new PoisonError();
+112
View File
@@ -136,6 +136,7 @@ import {
JS_RELAXED_TSC_CODES,
} from "./tsc-codes.js";
import { trackedFileExists, trackedReadFile, trackedRealpath } from "./input-tracker.js";
import { earlyErrorDiagnostics7 } from "./early-errors.js";
import { forkTargetPaths } from "./fork-target.js";
import { workerTargetPaths } from "./worker-target.js";
import {
@@ -2758,6 +2759,107 @@ function isAssignmentTarget7(node: ts.Node): boolean {
);
}
/** True when the identifier is a reference position that WRITES: an
* assignment (or compound-assignment) target, a `++`/`--` operand, a
* for-in/for-of head, or an element of a destructuring pattern — through
* parentheses and pattern containers, exactly the shapes a sloppy-mode
* implicit global can be introduced by. */
export function isJsWriteTarget7(node: ts.Node): boolean {
let cur: ts.Node = node;
for (;;) {
const parent: ts.Node | undefined = cur.parent;
if (parent === undefined) return false;
if (ts.isParenthesizedExpression(parent)) {
cur = parent;
continue;
}
if (ts.isBinaryExpression(parent))
return (
parent.left === cur &&
parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment
);
if (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent))
return (
parent.operand === cur &&
(parent.operator === ts.SyntaxKind.PlusPlusToken ||
parent.operator === ts.SyntaxKind.MinusMinusToken)
);
if (ts.isForInStatement(parent) || ts.isForOfStatement(parent))
return parent.initializer === cur;
if (ts.isArrayLiteralExpression(parent) || ts.isObjectLiteralExpression(parent)) {
cur = parent;
continue;
}
if (
(ts.isShorthandPropertyAssignment(parent) && parent.name === cur) ||
(ts.isPropertyAssignment(parent) && parent.initializer === cur) ||
((ts.isSpreadElement(parent) || ts.isSpreadAssignment(parent)) && parent.expression === cur)
) {
cur = parent;
continue;
}
return false;
}
}
/** Nothing binds `node`: the tsc had no symbol, and a shorthand pattern's
* name has no VALUE binding either (its property symbol is the target, not a
* declaration of the name being written). */
function undeclaredJsName7(
checker: ReturnType<ts.Program["getTypeChecker"]>,
node: ts.Identifier,
): boolean {
const parent = node.parent;
if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === node)
return checker.getShorthandAssignmentValueSymbol(parent) === undefined;
return checker.getSymbolAtLocation(node) === undefined;
}
/** A `.js` WRITE to a name nothing declares (plan D-2/D-5). Sloppy mode
* resolves such a write by creating a property on Node's global object —
* process-wide state a native binary has no object for — so the program is
* refused at build time, under SC1091, whose text names the Node-island
* route those semantics need. A READ of the same name is admitted instead:
* it throws Node's own catchable `ReferenceError` at the access
* (lowering/expressions/identifiers.ts). TypeScript input never reaches
* either path — an undeclared name is a compile error there. */
function jsUndeclaredWriteDiags(
program: ts.Program,
files: readonly ts.SourceFile[],
): ScrDiagnostic[] {
const diags: ScrDiagnostic[] = [];
const checker = program.getTypeChecker();
for (const sf of files) {
if (!isJsSourceFileName(sf.fileName)) continue;
const stack: ts.Node[] = [sf];
while (stack.length > 0) {
const node = stack.pop()!;
// An empty name is a parse-recovery artifact (`@import x = require(…)`
// in a JSDoc prologue leaves one behind), never a reference a program
// writes; the syntax error that created it is already reported.
if (
ts.isIdentifier(node) &&
node.text !== "" &&
isJsWriteTarget7(node) &&
undeclaredJsName7(checker, node)
)
diags.push(
unsupportedDiag(
"SC1091",
{ file: sf.fileName, start: node.getStart(sf), end: node.end },
`writing the undeclared name '${node.text}' in a JavaScript source`,
),
);
ts.forEachChild(node, (child) => {
stack.push(child);
return undefined;
});
}
}
return diags;
}
/** True when `node` is inside the try block of a try statement in the same
* function (a catch clause or finally block does not count). A class field
* initializer runs when the class is instantiated, so it is a boundary too. */
@@ -6589,6 +6691,11 @@ function preflight7(load: LoadResult): {
};
visit(entry);
// ECMAScript's sloppy-mode WRITE to an undeclared name, which the JS
// relaxation above admits for READS (plan D-2): refused by name, with the
// island route in the text (SC1091).
for (const d of jsUndeclaredWriteDiags(program, order)) diags.push(d);
// The startup crash Node's RESOLUTION phase reports: modules resolve
// their request specifiers in source order, children linking depth-first
// as each resolves (ModuleJob.syncLink) — a preorder walk over the
@@ -6680,6 +6787,11 @@ function preflight7(load: LoadResult): {
esmNamedImportLinkCheck(program, entry, order, diags),
);
// ECMAScript early errors the TypeScript checker does not model (plan
// §7.7 D-4): a JavaScript source the spec makes a SyntaxError fails here,
// before any lowering, exactly where a tsc parse error fails.
diags.push(...earlyErrorDiagnostics7(order, (sf) => isNodeEsmFile7(sf, program)));
return {
diags,
moduleOrder: order,
@@ -36,6 +36,21 @@ export const ADOPTED_OPTIONS = [
"reactNamespace",
] as const;
/* ── The early-error boundary (read this before relaxing a code) ────────
*
* Some of the codes below do double duty: TypeScript reports them for a
* JavaScript construct that ECMAScript ALSO makes a SyntaxError (a
* redeclared block binding, a declaration in a statement-only position).
* Relaxing such a code removes that construct's only rejection, so a
* parse-negative program would start compiling and running.
*
* frontend/early-errors.ts owns those rejections now, under their own
* preflight codes — SC0005 (duplicate lexical declaration in a block or
* switch statement list), SC0006 (lexical declaration colliding with a
* `var` in the same block), SC0007 (declaration in a single-statement
* position). A relaxation that needs one of those rules to keep firing must
* land with the matching rule in that pass, not rely on this table. */
export const JS_RELAXED_TSC_CODES: ReadonlySet<number> = new Set([
// implicit any (params, variables, elements, this, returns, rest,
// construct signatures, index reads over shapes with no index
@@ -109,6 +124,16 @@ export const JS_RELAXED_TSC_CODES: ReadonlySet<number> = new Set([
2488,
2721,
2722,
// the async-iterator protocol demand (2504: "must have a
// '[Symbol.asyncIterator]()' method that returns an async iterator").
// Whether a value is async-iterable is a runtime fact of untyped JS, exactly
// like the sync demand (2488) relaxed above: `for await` over a non-iterable
// value and `yield*` in an async generator both lower through the runtime
// protocol openers, which read the method, call it, and throw TypeError at
// the site when it is missing, uncallable, or returns a non-object — the
// same protocol failure Node reports. 2505 is the sync sibling on the typed
// lowering lanes and stays enforced.
2504,
// an UNUSED @ts-expect-error: the directive annotated an error visible
// under the project's OWN tsconfig world — scriptc's world deliberately
// differs (adopted options, hidden declaration twins), and an unused
@@ -150,6 +175,20 @@ export const JS_RELAXED_TSC_CODES: ReadonlySet<number> = new Set([
// ride the checked-dynamic tree; unsupported operations report a compile-time diagnostic).
18046,
2571,
// UNDECLARED NAMES in a JavaScript source: Node resolves them at run time,
// never at compile time — a READ throws the catchable
// `ReferenceError: <name> is not defined` at the access (`typeof` of one
// answers "undefined"), and a sloppy-mode WRITE creates a global. The
// lowerer turns the read into `scr_undef_global_read` (exact Node text and
// behavior) and refuses the write with SC1091, which names the Node-island
// route the sloppy semantics need (the plan's D-2/D-5). TypeScript input
// keeps the compile error: an undeclared name there is a bug, not a
// runtime path. 2552 is 2304's did-you-mean spelling, and 18004 is the
// same unresolved name in SHORTHAND position (`{ x }`), which the checker
// reports against the property instead of the reference.
2304,
2552,
18004,
]);
/* Arithmetic on an `any` operand (TS2362/2363/2365 fire when the other
+9 -1
View File
@@ -9,7 +9,7 @@
"Entries with status 'unsupported' describe where the named code is raised: forms of the construct outside the supported subset are refused with that code — not that every form of the named feature is refused. Supported forms appear as their own static entries where a table projects them.",
"The engine-free fetch projection targets Node 24.22.0 with bundled Undici 7.29.1. Each projected row names the differential evidence that guards it; changing the pinned Node or Undici version is an explicit profile update.",
"The fetch profile also contains a runtime-reflected census of the selected fetch, abort, Headers, and readable-stream interfaces plus RequestInit/ResponseInit dictionary reads. Static and unsupported census rows are projected here; its explicitly out-of-scope metadata rows and adjacent-interface exclusions remain in the profile so absence is deliberate rather than ambiguous.",
"Process-level diagnostic codes are not surface entries: SC0001-SC0004 are preflight gates, SC1110 is a comptime evaluation failure, SC3001/SC3002 are backend/target tier refusals, SC9001/SC9002 are internal errors.",
"Process-level diagnostic codes are not surface entries: SC0001-SC0007 are preflight gates (SC0005-SC0007 are the ECMAScript early errors), SC1110 is a comptime evaluation failure, SC3001/SC3002 are backend/target tier refusals, SC9001/SC9002 are internal errors.",
"Entry statuses are projected for the desktop targets. The mobile targets (aarch64-apple-ios, aarch64-apple-ios-simulator, aarch64-linux-android) compile library-mode archives only: the library-admissible surface (what SC4005's async_free requirement and the library link set admit) is supported there, the executable lane refuses those triples with SC3002, and no entry outside the library-admissible surface carries a mobile support claim. iOS archives build for iOS 15.0 on darwin hosts; Android archives build against NDK API level 26.",
"No scheduling metadata is published; entry ids are the stable diff keys across releases."
],
@@ -170,6 +170,14 @@
"status": "unsupported",
"code": "SC1090"
},
{
"id": "diagnostic.sc1091",
"kind": "diagnostic-fence",
"name": "writing an undeclared name in a JavaScript source",
"status": "unsupported",
"code": "SC1091",
"note": "a write to an undeclared name has no native lowering: sloppy mode makes it a property on Node's global object, which a native binary has none of, and strict mode makes it throw; declare the name, or keep the module in a Node island (sloppy-mode programs route there)"
},
{
"id": "diagnostic.sc1100",
"kind": "diagnostic-fence",
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8514-async-iterator-protocol-shapes.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8515-js-undeclared-read.js"
],
"diags": []
}
@@ -0,0 +1,61 @@
{
"order": [
"<repo>/tests/diagnostics/js-block-redeclaration.js"
],
"diags": [
{
"code": "SC0001",
"message": "Class declaration cannot implement overload list for 'collide'.",
"loc": {
"file": "<repo>/tests/diagnostics/js-block-redeclaration.js",
"start": 386,
"end": 393
}
},
{
"code": "SC0001",
"message": "Function with bodies can only merge with classes that are ambient.",
"loc": {
"file": "<repo>/tests/diagnostics/js-block-redeclaration.js",
"start": 414,
"end": 421
}
},
{
"code": "SC0001",
"message": "Type '2' is not comparable to type '1'.",
"loc": {
"file": "<repo>/tests/diagnostics/js-block-redeclaration.js",
"start": 491,
"end": 492
}
},
{
"code": "SC0001",
"message": "Cannot initialize outer scoped variable 'scoped' in the same scope as block scoped declaration 'scoped'.",
"loc": {
"file": "<repo>/tests/diagnostics/js-block-redeclaration.js",
"start": 502,
"end": 508
}
},
{
"code": "SC0006",
"message": "the lexical declaration 'scoped' collides with a 'var' declaration of the same name in this block",
"loc": {
"file": "<repo>/tests/diagnostics/js-block-redeclaration.js",
"start": 461,
"end": 467
}
},
{
"code": "SC0005",
"message": "the declaration 'collide' is already declared in this block's statement list",
"loc": {
"file": "<repo>/tests/diagnostics/js-block-redeclaration.js",
"start": 399,
"end": 426
}
}
]
}
@@ -0,0 +1,70 @@
{
"order": [
"<repo>/tests/diagnostics/js-single-statement-declaration.js"
],
"diags": [
{
"code": "SC0001",
"message": "A label is not allowed here.",
"loc": {
"file": "<repo>/tests/diagnostics/js-single-statement-declaration.js",
"start": 405,
"end": 406
}
},
{
"code": "SC0001",
"message": "A label is not allowed here.",
"loc": {
"file": "<repo>/tests/diagnostics/js-single-statement-declaration.js",
"start": 471,
"end": 473
}
},
{
"code": "SC0007",
"message": "an async function or generator declaration can only appear at the top level or inside a block",
"loc": {
"file": "<repo>/tests/diagnostics/js-single-statement-declaration.js",
"start": 505,
"end": 529
}
},
{
"code": "SC0007",
"message": "a labelled function declaration cannot be the body of a loop, a 'with' statement, or an 'if' clause",
"loc": {
"file": "<repo>/tests/diagnostics/js-single-statement-declaration.js",
"start": 475,
"end": 501
}
},
{
"code": "SC0007",
"message": "an async function or generator declaration can only appear at the top level or inside a block",
"loc": {
"file": "<repo>/tests/diagnostics/js-single-statement-declaration.js",
"start": 435,
"end": 459
}
},
{
"code": "SC0007",
"message": "a class declaration cannot appear in a single-statement position",
"loc": {
"file": "<repo>/tests/diagnostics/js-single-statement-declaration.js",
"start": 408,
"end": 424
}
},
{
"code": "SC0007",
"message": "a function declaration can only appear at the top level, inside a block, or as the body of an if statement",
"loc": {
"file": "<repo>/tests/diagnostics/js-single-statement-declaration.js",
"start": 384,
"end": 404
}
}
]
}
@@ -0,0 +1,34 @@
{
"order": [
"<repo>/tests/diagnostics/js-single-statement-strict.js"
],
"diags": [
{
"code": "SC0001",
"message": "A label is not allowed here.",
"loc": {
"file": "<repo>/tests/diagnostics/js-single-statement-strict.js",
"start": 355,
"end": 356
}
},
{
"code": "SC0007",
"message": "strict mode code may not declare a function in a single-statement position",
"loc": {
"file": "<repo>/tests/diagnostics/js-single-statement-strict.js",
"start": 358,
"end": 379
}
},
{
"code": "SC0007",
"message": "strict mode code may not declare a function in a single-statement position",
"loc": {
"file": "<repo>/tests/diagnostics/js-single-statement-strict.js",
"start": 336,
"end": 354
}
}
]
}
@@ -0,0 +1,18 @@
{
"order": [
"<repo>/tests/diagnostics/sloppy-implicit-global-write.js"
],
"diags": [
{
"code": "SC1091",
"message": "writing the undeclared name 'implicitGlobal' in a JavaScript source is not supported yet",
"loc": {
"file": "<repo>/tests/diagnostics/sloppy-implicit-global-write.js",
"start": 306,
"end": 320
},
"milestone": "later",
"hint": "a write to an undeclared name has no native lowering: sloppy mode makes it a property on Node's global object, which a native binary has none of, and strict mode makes it throw; declare the name, or keep the module in a Node island (sloppy-mode programs route there)"
}
]
}
@@ -0,0 +1,96 @@
// Async iteration over values whose [Symbol.asyncIterator] shape only exists
// at run time. JavaScript puts no static demand on the receiver: the protocol
// reads the method, calls it, and throws TypeError when the method is missing,
// is not callable, or returns a non-object. The checker must not refuse the
// source for the shapes inference cannot prove.
function report(label, fn) {
return Promise.resolve()
.then(fn)
.then(
(value) => console.log(label, "ok", value),
(error) => console.log(label, error instanceof TypeError, error.name),
);
}
async function* delegate(value) {
yield* value;
}
async function drain(value) {
const values = [];
for await (const item of value) values.push(item);
return values.join(",");
}
async function main() {
// A well-formed async iterator written inline still delegates.
await report("inline", () =>
drain({
async *[Symbol.asyncIterator]() {
yield "a";
yield "b";
},
}),
);
// The method is a data property, not a function.
await report("method-not-callable", () => drain({ [Symbol.asyncIterator]: 42 }));
// The method returns a number instead of an iterator object.
await report("result-not-object", () => drain({ [Symbol.asyncIterator]() { return 42; } }));
// The returned iterator's next is not callable.
await report("next-not-callable", () =>
drain({
[Symbol.asyncIterator]() {
return { next: "nope" };
},
}),
);
// No async iterator at all and no sync fallback either.
await report("no-protocol", () => drain({ value: 1 }));
// yield* delegates through the same protocol, so it throws the same way.
await report("yield-star-not-callable", async () => {
let seen = "";
for await (const value of delegate({ [Symbol.asyncIterator]: true })) seen += value;
return seen;
});
await report("yield-star-result-not-object", async () => {
let seen = "";
for await (const value of delegate({ [Symbol.asyncIterator]() { return "text"; } })) seen += value;
return seen;
});
// A non-callable async iterator wins over a working sync iterator: the
// protocol only falls back when the method is undefined.
await report("async-shadows-sync", () =>
drain({
[Symbol.asyncIterator]: 0,
*[Symbol.iterator]() {
yield "never";
},
}),
);
// A value with only a sync iterable iterates through the sync fallback.
await report("sync-only", () =>
drain({
*[Symbol.iterator]() {
yield "s1";
yield "s2";
},
}),
);
// yield* over arrays and strings keeps working.
await report("yield-star-array", async () => {
let seen = "";
for await (const value of delegate([1, 2])) seen += value;
return seen;
});
}
main();
+40
View File
@@ -0,0 +1,40 @@
// A JavaScript READ of a name nothing declares resolves at run time, exactly
// as Node does: the access throws a catchable ReferenceError whose message
// names the identifier, `typeof` answers "undefined" without throwing, and a
// call through the same name throws at the callee read (plan D-2 / C5). The
// undeclared name is lowered to the runtime's global-read helper, which
// throws Node's "<name> is not defined".
'use strict';
const describe = (label, fn) => {
try {
console.log(`${label}: ${String(fn())}`);
} catch (e) {
console.log(`${label}: threw ${e.name}: ${e.message}`);
}
};
console.log("typeof:", typeof neverDeclared);
describe("bare read", () => neverDeclared);
describe("call", () => neverDeclared(1, 2));
describe("member read", () => neverDeclared.length);
describe("arithmetic", () => neverDeclared + 1);
describe("comparison", () => neverDeclared === undefined);
describe("shorthand property", () => {
const wrapper = { neverDeclared };
return String(wrapper.neverDeclared);
});
describe("destructuring default", () => {
const pick = ({ value = unresolvableReference } = {}) => value;
return pick({});
});
describe("typeof test", () => typeof neverDeclared === "undefined");
// `typeof` never throws, so a guarded read is the one safe shape.
if (typeof maybeConfigured !== "undefined") console.log("configured:", maybeConfigured);
else console.log("configured: unset");
// A declared binding keeps the ordinary lowered path.
let declared = 3;
describe("declared", () => declared * 2);
@@ -0,0 +1,18 @@
// ECMAScript early errors the TypeScript checker does not model: a statement
// list may not declare one lexical name twice, and a lexical name may not be
// `var`-declared in the same list (Block : { StatementList }, and a switch's
// case block is one such list). Both are SC0005/SC0006, reported at
// preflight — the constructs come from Test262's negative-parse suites.
{
class collide {}
async function collide() {}
}
switch (1) {
case 1:
let scoped = 1;
break;
case 2:
var scoped = 2;
break;
}
@@ -0,0 +1,10 @@
// ECMAScript early errors the TypeScript checker does not model: the body of
// a loop, a `with` statement, or an if clause admits only a Statement, and
// Annex B's function-declaration allowance never covers an async function, a
// generator, a class, or a lexical declaration. SC0007, reported at
// preflight — the shapes come from Test262's negative-parse suites.
while (false) function inLoop() {}
l: class inLabel {}
if (true) async function inIf() {}
if (false) il: function labelledInIf() {}
do function* inDoWhile() {} while (false);
@@ -0,0 +1,8 @@
// The strict half of SC0007: Annex B's allowance for a plain function
// declaration in an if/else body or a labelled statement is non-strict code
// only, so a "use strict" prologue takes it away — Node agrees on both
// shapes. The sloppy fixture beside this one pins the refusals that hold in
// both modes.
"use strict";
if (true) function inIf() {}
l: function inLabel() {}
@@ -0,0 +1,6 @@
// A sloppy-mode implicit-global WRITE in a JavaScript source. Node resolves
// the write by creating a property on the global object — process-wide state
// a native binary has none of — so the write is refused by name (SC1091) and
// names the Node-island route the sloppy semantics need (plan D-2/D-5).
implicitGlobal = 42;
console.log(implicitGlobal);
@@ -1,13 +1,15 @@
// Methods that store from a for-in or for-of head where sloppy mode and
// strict mode differ: into an undeclared name (sloppy mode creates a
// global) and into a property of a string (sloppy mode drops the store).
// strict mode differ: into the read-only global `undefined` and into a
// property of a string (sloppy mode drops both stores; strict mode throws).
// A store into an undeclared name is refused for the whole package (SC1091),
// so it cannot appear here.
var shapes = {};
shapes.Bag = function (size) {
this.size = size;
};
shapes.Bag.prototype.lastKey = function () {
for (bagLastKey in { first: 1, last: 2 });
return bagLastKey + " " + this.size;
for (undefined in { first: 1, last: 2 });
return typeof undefined + " " + this.size;
};
shapes.Cell = function (label) {
this.label = label;
@@ -0,0 +1,41 @@
js-block-redeclaration.js:7:9 - error SC0001: Class declaration cannot implement overload list for 'collide'.
6 | {
7 | class collide {}
| ^~~~~~~
8 | async function collide() {}
js-block-redeclaration.js:8:3 - error SC0005: the declaration 'collide' is already declared in this block's statement list
7 | class collide {}
8 | async function collide() {}
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~
9 | }
js-block-redeclaration.js:8:18 - error SC0001: Function with bodies can only merge with classes that are ambient.
7 | class collide {}
8 | async function collide() {}
| ^~~~~~~
9 | }
js-block-redeclaration.js:13:9 - error SC0006: the lexical declaration 'scoped' collides with a 'var' declaration of the same name in this block
12 | case 1:
13 | let scoped = 1;
| ^~~~~~
14 | break;
js-block-redeclaration.js:15:8 - error SC0001: Type '2' is not comparable to type '1'.
14 | break;
15 | case 2:
| ^
16 | var scoped = 2;
js-block-redeclaration.js:16:9 - error SC0001: Cannot initialize outer scoped variable 'scoped' in the same scope as block scoped declaration 'scoped'.
15 | case 2:
16 | var scoped = 2;
| ^~~~~~
17 | break;
@@ -0,0 +1,48 @@
js-single-statement-declaration.js:6:15 - error SC0007: a function declaration can only appear at the top level, inside a block, or as the body of an if statement
5 | // preflight — the shapes come from Test262's negative-parse suites.
6 | while (false) function inLoop() {}
| ^~~~~~~~~~~~~~~~~~~~
7 | l: class inLabel {}
js-single-statement-declaration.js:7:1 - error SC0001: A label is not allowed here.
6 | while (false) function inLoop() {}
7 | l: class inLabel {}
| ^
8 | if (true) async function inIf() {}
js-single-statement-declaration.js:7:4 - error SC0007: a class declaration cannot appear in a single-statement position
6 | while (false) function inLoop() {}
7 | l: class inLabel {}
| ^~~~~~~~~~~~~~~~
8 | if (true) async function inIf() {}
js-single-statement-declaration.js:8:11 - error SC0007: an async function or generator declaration can only appear at the top level or inside a block
7 | l: class inLabel {}
8 | if (true) async function inIf() {}
| ^~~~~~~~~~~~~~~~~~~~~~~~
9 | if (false) il: function labelledInIf() {}
js-single-statement-declaration.js:9:12 - error SC0001: A label is not allowed here.
8 | if (true) async function inIf() {}
9 | if (false) il: function labelledInIf() {}
| ^~
10 | do function* inDoWhile() {} while (false);
js-single-statement-declaration.js:9:16 - error SC0007: a labelled function declaration cannot be the body of a loop, a 'with' statement, or an 'if' clause
8 | if (true) async function inIf() {}
9 | if (false) il: function labelledInIf() {}
| ^~~~~~~~~~~~~~~~~~~~~~~~~~
10 | do function* inDoWhile() {} while (false);
js-single-statement-declaration.js:10:4 - error SC0007: an async function or generator declaration can only appear at the top level or inside a block
9 | if (false) il: function labelledInIf() {}
10 | do function* inDoWhile() {} while (false);
| ^~~~~~~~~~~~~~~~~~~~~~~~
11 |
@@ -0,0 +1,20 @@
js-single-statement-strict.js:7:11 - error SC0007: strict mode code may not declare a function in a single-statement position
6 | "use strict";
7 | if (true) function inIf() {}
| ^~~~~~~~~~~~~~~~~~
8 | l: function inLabel() {}
js-single-statement-strict.js:8:1 - error SC0001: A label is not allowed here.
7 | if (true) function inIf() {}
8 | l: function inLabel() {}
| ^
9 |
js-single-statement-strict.js:8:4 - error SC0007: strict mode code may not declare a function in a single-statement position
7 | if (true) function inIf() {}
8 | l: function inLabel() {}
| ^~~~~~~~~~~~~~~~~~~~~
9 |
@@ -0,0 +1,8 @@
sloppy-implicit-global-write.js:5:1 - error SC1091: writing the undeclared name 'implicitGlobal' in a JavaScript source is not supported yet
4 | // names the Node-island route the sloppy semantics need (plan D-2/D-5).
5 | implicitGlobal = 42;
| ^~~~~~~~~~~~~~
6 | console.log(implicitGlobal);
hint: a write to an undeclared name has no native lowering: sloppy mode makes it a property on Node's global object, which a native binary has none of, and strict mode makes it throw; declare the name, or keep the module in a Node island (sloppy-mode programs route there)
@@ -0,0 +1 @@
{ "plain": 8004 }
@@ -0,0 +1 @@
{ "plain": 3300 }
+3 -1
View File
@@ -132,7 +132,9 @@ test("explicit installed JavaScript entry keeps diagnostics in its own modules",
mkdirSync(dirname(entry), { recursive: true });
writeFileSync(join(dirname(entry), "package.json"), '{"type":"module"}\n');
writeFileSync(entry, 'import { value } from "./value.js"; console.log(value);\n');
writeFileSync(join(dirname(entry), "value.js"), "export const value = missingName;\n");
// A syntax error: a read of an undeclared name is Node's run-time
// ReferenceError in JavaScript, not a diagnostic.
writeFileSync(join(dirname(entry), "value.js"), "export const value = ;\n");
const { coverage } = analyze(entry);
expect(coverage.preflightFailed).toBe(true);
expect(coverage.diagnostics.map((d) => d.code)).toEqual(["SC0001"]);
+3 -3
View File
@@ -104,9 +104,9 @@ test(
expect(rewrittenIfAny(debug, join(pkg, "coerce.js")) ?? "").not.toContain(
"class __ns_shapes_Box",
);
// loops.js's methods store from for-in and for-of heads into an
// undeclared name and into a property of a string, which throws in a
// class body.
// loops.js's methods store from for-in and for-of heads into the
// read-only global `undefined` and into a property of a string, which
// throws in a class body.
const loops = rewrittenIfAny(debug, join(pkg, "loops.js")) ?? "";
expect(loops).not.toContain("class __ns_shapes_Bag");
expect(loops).not.toContain("class __ns_shapes_Cell");
+10 -2
View File
@@ -250,14 +250,22 @@ test("built-in error assertions are admitted without permitting constructor alia
expect(exclusion(alias, metadata(alias), "strict")).toBe("harness:assert-surface");
});
test("the compareArray include only admits the implemented assertion form", () => {
test("the compareArray include admits the assertion and the bare call form only", () => {
const accepted = source(
"includes: [compareArray.js]",
"assert.compareArray([1, NaN], [1, NaN]);",
);
expect(exclusion(accepted, metadata(accepted), "strict")).toBeUndefined();
const globalHelper = source("includes: [compareArray.js]", "compareArray([1], [1]);");
expect(exclusion(globalHelper, metadata(globalHelper), "strict")).toBe(
expect(exclusion(globalHelper, metadata(globalHelper), "strict")).toBeUndefined();
// assert.js supplies compareArray, so a test needs no include to call it.
const withoutInclude = source("description: intl", "assert(!compareArray([1], [2]));");
expect(exclusion(withoutInclude, metadata(withoutInclude), "strict")).toBeUndefined();
const valueReference = source(
"includes: [compareArray.js]",
"const compare = compareArray; assert(compare([1], [1]));",
);
expect(exclusion(valueReference, metadata(valueReference), "strict")).toBe(
"harness:compareArray-surface",
);
});
+37
View File
@@ -189,6 +189,43 @@ assert.notSameValue(1, "1");
source: "assert.compareArray([1, 2], [1, 3]);",
},
{ name: "array signed zero mismatch", status: "fail", source: "assert.compareArray([0], [-0]);" },
{
name: "bare compareArray returns true for matching contents",
status: "pass",
source: "assert(compareArray([1, NaN, -0], [1, NaN, -0]));",
},
{
name: "bare compareArray returns false for a length or element mismatch",
status: "pass",
source: "assert(!compareArray([1], [1, 2])); assert(!compareArray([1, 2], [1, 3]));",
},
{
name: "bare compareArray compares operands without a length as equal",
status: "pass",
source: "assert(compareArray(1, 2)); assert(compareArray({}, {}));",
},
{
name: "bare compareArray compares the elements of a string length",
status: "pass",
source:
'assert(!compareArray({ length: "2", 0: 1, 1: 2 }, { length: "2", 0: 1, 1: 3 })); assert(compareArray({ length: "1", 0: 1 }, { length: "1", 0: 1 }));',
},
{
name: "bare compareArray reads the lengths in the upstream order",
status: "pass",
source: `
var log = [];
var a = { get length() { log.push("a"); return 1; }, 0: 1 };
var b = { get length() { log.push("b"); return 1; }, 0: 1 };
assert(compareArray(a, b));
assert.compareArray(log, ["b", "a", "a", "a"]);
`,
},
{
name: "bare compareArray does not throw on a mismatch",
status: "fail",
source: "assert(compareArray([1], [2]));",
},
{ name: "assert requires true, not truthiness", status: "fail", source: "assert(1);" },
{
name: "exact built-in exception",
+24
View File
@@ -60,6 +60,30 @@ export function assert(value: unknown, message?: unknown): void {
if (value !== true) throw new Test262Error(text(message, "Expected true"));
}
function lengthOf(value: unknown): unknown {
return (value as { length?: unknown }).length;
}
function elementAt(value: unknown, index: number): unknown {
return (value as Record<number, unknown>)[index];
}
/** The bare `compareArray(a, b)` the upstream `harness/assert.js` defines
* alongside `assert`. It RETURNS a boolean (the throwing assertion is
* `assert.compareArray`), and it has no shape checks of its own. It makes the
* upstream reads in the upstream order: `b.length`, then `a.length`, then
* `a.length` again before every element pair, `b[i]` before `a[i]`. A null
* operand throws TypeError there and here, and two operands without a length
* of their own compare equal. The loop bound converts the length the way the
* upstream `i < a.length` does, so a length of "2" compares two elements. */
export function compareArray(actual: unknown, expected: unknown): boolean {
if (lengthOf(expected) !== lengthOf(actual)) return false;
for (let i = 0; i < Number(lengthOf(actual)); i++) {
if (!same(elementAt(expected, i), elementAt(actual, i))) return false;
}
return true;
}
export namespace assert {
export function sameValue(actual: unknown, expected: unknown, message?: unknown): void {
if (!same(actual, expected)) throw new Test262Error(text(message, "Expected SameValue"));
+28 -1
View File
@@ -169,7 +169,11 @@ export function exclusion(source, meta, variant) {
// Supported assertion calls; aliases, mutations, and reflection stay out.
} else reason = "harness:assert-surface";
} else if (meta.includes.includes("compareArray.js") && ts.isIdentifier(node) && node.text === "compareArray" &&
!(ts.isCallExpression(node.parent) && node.parent.expression === node) &&
!(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node && ts.isIdentifier(node.parent.expression) && node.parent.expression.text === "assert")) {
// assert.js defines the bare compareArray; only its direct call form is
// hosted (a value reference or property read would need the upstream
// function object and its format expando).
reason = "harness:compareArray-surface";
} else if (ts.isIdentifier(node) && node.text === "Test262Error") {
if (!ts.isNewExpression(node.parent) || node.parent.expression !== node) reason = "harness:Test262Error-surface";
@@ -192,7 +196,9 @@ export function exclusion(source, meta, variant) {
}
export function prepare(source, asyncTest = false, marker = completion, variant = "strict", includes = []) {
const names = asyncTest ? "assert, Test262Error, $DONE" : "assert, Test262Error";
const names = asyncTest
? "assert, compareArray, Test262Error, $DONE"
: "assert, compareArray, Test262Error";
const end = asyncTest ? "" : `;console.log(${JSON.stringify(marker)});\n`;
const helpers =
(includes.includes("propertyHelper.js") ? `${propertyHelperSource}\n` : "") +
@@ -255,8 +261,29 @@ function earlyErrorDiagnostics(source, variant) {
!(variant === "sloppy" && (diagnostic.code === 1214 || diagnostic.code === 1215)));
}
// The compiler's OWN early errors (frontend/early-errors.ts): static
// semantics tsc does not model, reported at preflight under their own codes.
// A negative-parse test only asks for SOME SyntaxError, so one of these
// inside the test's source is a match — the same standard as the tsc
// diagnostics above, minus tsc's ability to tell us where it points.
const ownEarlyErrorCodes = new Set(["SC0005", "SC0006", "SC0007"]);
export function matchesParseNegative(outcome, source, variant = "strict") {
if (outcome.status !== "compile-refusal") return false;
{
const prepared = prepare(source, false, completion, variant);
const start = prepared.indexOf(source);
const end = start + source.length;
const suffix = variant === "sloppy" ? "/main.cjs" : "/main.js";
const own = outcome.diagnostics?.some(
(reported) =>
ownEarlyErrorCodes.has(reported.code) &&
reported.loc?.file?.endsWith(suffix) &&
reported.loc.start >= start &&
reported.loc.start < end,
);
if (own) return true;
}
const matches = (parsed) => outcome.diagnostics?.some((reported) =>
reported.code === "SC0001" &&
reported.loc?.file?.endsWith(variant === "sloppy" ? "/main.cjs" : "/main.js") &&