mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Test262 group C: early errors, undeclared reads, async-iterator relaxation, compareArray harness (#2500)
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8514-async-iterator-protocol-shapes.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8515-js-undeclared-read.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+61
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+70
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+34
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+18
@@ -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();
|
||||
@@ -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);
|
||||
Generated
Vendored
+6
-4
@@ -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 }
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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") &&
|
||||
|
||||
Reference in New Issue
Block a user