Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/content/limitations/type-checking.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ Some declarations differ from TypeScript's standard library or `@types/node`: `J
A program that passes its project's TypeScript check may still use unsupported operations.
A second preflight using the project's declarations distinguishes those sites from ordinary type errors and reports scriptc diagnostics with rewrite hints.
Declared APIs without a compiler implementation, such as unsupported console members, produce `SC2020`.

TypeScript keeps the type of an assignment across a call, so after `x = undefined` a read of `x` is typed `undefined` even when a function called in between assigns `x` again.
scriptc reads the stored value there.
A function without a declared return type that returns such a read is inferred to return `undefined`; scriptc refuses that return with `SC1090`, and a declared return type compiles.
3 changes: 3 additions & 0 deletions packages/compiler/src/frontend/lowering/lower-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,9 @@ export function lowerLambda(
// statement branches instead of a forbidden void-valued ternary.
let stripped: ts.Expression = bodyExpr;
while (ts.isParenthesizedExpression(stripped)) stripped = stripped.expression;
// A body whose value the checker typed `undefined` from a
// narrowing a call may have undone would be dropped here.
lowerer.refuseDroppedStaleReturn(bodyExpr);
if (ts.isVoidExpression(stripped) || ts.isConditionalExpression(stripped)) {
body = [lowerer.lowerExprStatement(stripped)];
} else {
Expand Down
20 changes: 20 additions & 0 deletions packages/compiler/src/frontend/lowering/lowerer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5429,6 +5429,9 @@ export class Lowerer {
"narrowing.effects",
"narrowing.capturedMutable",
"narrowing.staleBindings",
"narrowing.inherited",
"narrowing.writes",
"narrowing.resolving",
"narrowing.identifierWrites",
"narrowing.widened",
"narrowing.plainContainers",
Expand Down Expand Up @@ -16716,6 +16719,7 @@ export class Lowerer {
private lowerReturnOf(node: ts.Expression, loc: SrcLoc): IrStmt {
const expected = this.ctx.returnType;
if (expected.kind === "void") {
this.refuseDroppedStaleReturn(node);
let e = this.lowerExpr(node);
const flattened = this.maybePromiseReturn(node, e, expected);
if (flattened) return flattened;
Expand Down Expand Up @@ -16760,6 +16764,22 @@ export class Lowerer {
return { kind: "return", value: this.lowerReturnValue(node), loc };
}

/** A function returns nothing when the checker inferred its return type
* from a read it narrowed to `undefined`. A call may have stored a value
* there since (widenedType restored the read), and returning nothing
* would drop it; the caller's types were inferred from the same
* narrowing, so only a declared return type can carry the value. */
refuseDroppedStaleReturn(node: ts.Expression): void {
const stale = this.narrowing.restoredNothingRead(node);
if (stale === null) return;
this.unsupported(
"SC1090",
node,
`returning '${ts.getTextOfNode(stale)}' from a function whose return type TypeScript infers as 'undefined' (it types the read by the last assignment in this function, and a call made since can have assigned it from another function)`,
"declare the function's return type, so the result is the value stored at the return",
);
}

maybeNarrow(expr: IrExpr, node: ts.Node): IrExpr {
return maybeNarrow(this, expr, node);
}
Expand Down
Loading
Loading