mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-04 08:04:37 +08:00
web/text singletons: URLSearchParams ToString, match reads, Array.from
URLSearchParams crossing an implicit ToString (`${sp}`, "" + sp, String(sp))
had no lowering: ensureString now answers sp.toString, the serialized pair
list. Record inits also accept shorthand members (`new URLSearchParams({ model })`)
like the explicit `{ model: model }` spelling.
`re.exec(s)?.index` keeps the optional chain: the read is the match row's
index/input member (the chain lowering binds the guarded receiver first),
so the questionDotToken guard no longer diverts it.
Array.from(s) over a string whose binding the checker narrowed through a
guard read as the narrowed union and fenced; the site type now decides, and
the read is coerced to that string (code points, fresh array).
Corpus 8423-8425 pin all three against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e59c117e3b
commit
1a3caa88eb
@@ -228,11 +228,30 @@ function lowerSearchParamsInit(lowerer: Lowerer, arg: ts.Expression, loc: SrcLoc
|
||||
if (ts.isObjectLiteralExpression(arg)) {
|
||||
let acc: IrExpr = { kind: "libCall", fn: "sp.new", args: [], type: SEARCH_PARAMS_T, loc };
|
||||
for (const prop of arg.properties.nodes) {
|
||||
// `{ model }` appends the binding's value under its own name, like
|
||||
// the `{ model: model }` spelling.
|
||||
if (ts.isShorthandPropertyAssignment(prop)) {
|
||||
const keyExpr: IrExpr = {
|
||||
kind: "strLit",
|
||||
value: (prop.name as ts.Identifier | ts.StringLiteral).text,
|
||||
type: STRING,
|
||||
loc: locOf(prop.name),
|
||||
};
|
||||
const value = lowerer.coerceInto(prop.name, lowerer.lowerShorthandValue(prop), STRING);
|
||||
acc = {
|
||||
kind: "libCall",
|
||||
fn: "sp.with",
|
||||
args: [acc, keyExpr, value],
|
||||
type: SEARCH_PARAMS_T,
|
||||
loc,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
if (!ts.isPropertyAssignment(prop) || prop.name === undefined) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
prop,
|
||||
"URLSearchParams record inits with spreads, accessors, or shorthand entries",
|
||||
"URLSearchParams record inits with spreads or accessors",
|
||||
);
|
||||
}
|
||||
let key: string;
|
||||
|
||||
@@ -1085,6 +1085,19 @@ export function lowerArrayFromCall(
|
||||
// and materializes holes while keeping element reference identity.
|
||||
if ((args.length === 1 || args.length === 2) && !ts.isObjectLiteralExpression(args[0]!)) {
|
||||
const source = lowerer.lowerExpr(args[0]!);
|
||||
// Array.from(s): the string iterator's code points, into a fresh
|
||||
// array (scr_str_code_points already builds one, like Array.from).
|
||||
// The checker can narrow the argument to a string while the binding
|
||||
// keeps its nullable storage (`value.includes(x) && Array.from(value)`),
|
||||
// so the site type decides and the read is coerced to that string.
|
||||
if (
|
||||
args.length === 1 &&
|
||||
(source.type.kind === "string" ||
|
||||
lowerer.mapTypeOf(lowerer.typeOf(args[0]!))?.kind === "string")
|
||||
) {
|
||||
const str = source.type.kind === "string" ? source : lowerer.coerceToExpected(source, STRING);
|
||||
return strCharsCall(str, loc);
|
||||
}
|
||||
let src =
|
||||
source.type.kind === "string" && args.length === 2 ? strCharsCall(source, loc) : source;
|
||||
if (src.type.kind === "array") {
|
||||
|
||||
@@ -648,6 +648,10 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr
|
||||
if (e.type.kind === "string") return e;
|
||||
if (e.type.kind === "url")
|
||||
return { kind: "libCall", fn: "url.href", args: [e], type: STRING, loc: e.loc };
|
||||
// String(sp) / `${new URLSearchParams(...)}`: URLSearchParams.toString,
|
||||
// the serialized list (`?` stripped, pairs encoded).
|
||||
if (e.type.kind === "searchParams")
|
||||
return { kind: "libCall", fn: "sp.toString", args: [e], type: STRING, loc: e.loc };
|
||||
// String(date) / `${date}` / "" + date: Date's ToPrimitive prefers
|
||||
// toString().
|
||||
if (e.type.kind === "date")
|
||||
|
||||
@@ -698,7 +698,9 @@ export function lowerPropertyAccessExpr(
|
||||
};
|
||||
}
|
||||
}
|
||||
if ((expr.name.text === "index" || expr.name.text === "input") && !expr.questionDotToken) {
|
||||
// `re.exec(s)?.index` takes the same read: the chain lowering binds the
|
||||
// guarded receiver (non-nullish inside the guard) before re-dispatching.
|
||||
if (expr.name.text === "index" || expr.name.text === "input") {
|
||||
const represented = tryLowerExpression(lowerer, expr.expression);
|
||||
if (
|
||||
represented &&
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8423-urlsearchparams-implicit-string.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8424-regex-match-index-chain.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8425-array-from-string-nullable.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// URLSearchParams crossing an implicit ToString: a template span, string
|
||||
// concatenation, String() and an explicit toString() all serialize the
|
||||
// pair list in insertion order, percent-encoded exactly as Node does. The
|
||||
// record init accepts shorthand members (`{ model }`) like `{ model: model }`.
|
||||
const model = "m 1";
|
||||
const sp = new URLSearchParams({ model, autoload: "false", q: "a&b" });
|
||||
console.log(`${sp}`);
|
||||
console.log("prefix:" + sp);
|
||||
console.log(String(sp));
|
||||
console.log(sp.toString());
|
||||
console.log(sp.get("model"), sp.get("q"), sp.get("missing"));
|
||||
const empty = new URLSearchParams({});
|
||||
console.log(`[${empty}]`, JSON.stringify(empty.toString()));
|
||||
const pairs = [...sp];
|
||||
console.log(pairs.length, pairs[0]![0], pairs[0]![1]);
|
||||
@@ -0,0 +1,12 @@
|
||||
// A match row read through an optional chain: `re.exec(s)?.index` and the
|
||||
// `?.input` beside it take the same member reads as the guarded form.
|
||||
const re = /b(c)/g;
|
||||
const text = "abcz";
|
||||
const m = re.exec(text);
|
||||
console.log(m !== null ? m.index : -1);
|
||||
console.log(re.exec(text)?.index);
|
||||
console.log(re.exec(text)?.input);
|
||||
console.log(/(z)/.exec(text)?.index);
|
||||
console.log(/q/.exec(text)?.index);
|
||||
const groups = /(?<lead>b)/.exec(text)?.groups;
|
||||
console.log(groups?.lead);
|
||||
@@ -0,0 +1,19 @@
|
||||
// Array.from over a string the checker narrowed through a guard, and over
|
||||
// code points (surrogate pairs stay whole), each a fresh array.
|
||||
const values: (string | undefined)[] = ["abc", undefined, "a\u{1F600}b"];
|
||||
for (const value of values) {
|
||||
if (value === undefined) {
|
||||
console.log("skip");
|
||||
continue;
|
||||
}
|
||||
const chars = Array.from(value);
|
||||
console.log(chars.length, chars.join("|"));
|
||||
}
|
||||
const maybe: string | undefined = "xy";
|
||||
if (maybe !== undefined) console.log(Array.from(maybe).length);
|
||||
const plain: string = "q";
|
||||
console.log(Array.from(plain), Array.from(plain).length);
|
||||
function guarded(v: string | undefined): boolean {
|
||||
return v !== undefined && Array.from(v).length > 1;
|
||||
}
|
||||
console.log(guarded("ab"), guarded("a"), guarded(undefined));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4163, "san": 7431 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2579, "san": 9345 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2724, "san": 5297 }
|
||||
Reference in New Issue
Block a user