mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Let implicit JS instances that fall through return undefined
A JavaScript function specialized per call site takes its declared
return type from JSDoc. bcryptjs's `hash` and `compare` document
`@returns {!Promise}` but return nothing when a callback is passed, so
their instances ended with the "the checker proved every path returns"
trap and every callback-style call aborted with SC9002. Non-generic
JavaScript functions already widen such a return with an undefined arm;
implicit instances now do the same.
The widened `Promise<unknown> | undefined` result is then awaited by
TypeScript callers, which was refused: an await of a promise-of-dynamic
or unit union now goes through the dynamic await, which adopts the
promise or hops once for the unit, as Node does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f8fa60afec
commit
20f15f89fd
@@ -13,6 +13,7 @@ import {
|
||||
} from "../../ir/ir.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import { typeKey } from "../type-mapper.js";
|
||||
import { functionCanReturnUndefined } from "../function-completion.js";
|
||||
import { PoisonError, dynFallbackType, neverTaintedJsType, newFnCtx } from "./lowerer.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { findGenericMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
@@ -1428,6 +1429,16 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType |
|
||||
return null;
|
||||
if (mapped !== null && promiseCarriesDyn(lowerer, mapped) && !hasExplicitJsDocReturn(info.decl))
|
||||
return null;
|
||||
// A JavaScript body that can complete without returning yields
|
||||
// undefined there, whatever its JSDoc claims (bcryptjs's `hash`
|
||||
// returns a promise only when no callback is passed), exactly as
|
||||
// declaredReturnType widens non-generic JavaScript functions.
|
||||
if (
|
||||
mapped !== null &&
|
||||
isJsSourceFile(info.decl.getSourceFile()) &&
|
||||
functionCanReturnUndefined(info.decl)
|
||||
)
|
||||
return lowerer.withUndefinedArmOf(mapped) ?? mapped;
|
||||
return mapped;
|
||||
} catch (e) {
|
||||
if (!(e instanceof PoisonError)) throw e;
|
||||
|
||||
@@ -1722,6 +1722,18 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
const promiseArm = def.arms[promiseTag]!;
|
||||
const inner = promiseArm.kind === "promise" ? promiseArm.inner : VOID;
|
||||
const units = def.arms.filter(isUnitType);
|
||||
// A checked-dynamic inner: the whole operand is a dyn value (the
|
||||
// promise boxes by reference, the unit as itself), and the dyn
|
||||
// await adopts the promise or hops once for the unit.
|
||||
if (inner.kind === "dyn" && lowerer.dynConvertible(value.type)) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "async.awaitDyn",
|
||||
args: [lowerer.coerceToExpected(value, DYN)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (inner.kind === "union" || inner.kind === "dyn") {
|
||||
// A union inner would need an arm-wise re-tag into the result
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// A package function whose JSDoc promises a Promise but that returns
|
||||
// nothing when a callback is passed (bcryptjs's hash and compare). The
|
||||
// call that falls through completes normally, as in Node.
|
||||
import { hash } from "callback-hash";
|
||||
|
||||
hash("pw", 4, (err, value) => {
|
||||
console.log("callback", err, value);
|
||||
});
|
||||
console.log("promise", await hash("pw", "salt"));
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
export declare function hash(input: string, salt: number | string): Promise<string>;
|
||||
export declare function hash(
|
||||
input: string,
|
||||
salt: number | string,
|
||||
callback: (err: Error | null, value?: string) => void,
|
||||
): void;
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {number|string} salt
|
||||
* @param {function(Error, string=)=} callback
|
||||
* @param {function(number)=} progress
|
||||
* @returns {!Promise}
|
||||
*/
|
||||
export function hash(input, salt, callback, progress) {
|
||||
function run(done) {
|
||||
if (progress) progress(1);
|
||||
done(null, `${input}:${salt}`);
|
||||
}
|
||||
if (callback) {
|
||||
if (typeof callback !== "function") throw Error("Illegal callback: " + typeof callback);
|
||||
run(callback);
|
||||
} else
|
||||
return new Promise(function (resolve, reject) {
|
||||
run(function (err, res) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(res);
|
||||
});
|
||||
});
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "callback-hash", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
|
||||
Reference in New Issue
Block a user