mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Resolve const aliases when deciding a child stream's setEncoding state
An untyped child-stream 'data' listener takes strings when a setEncoding call on the same stream precedes it. A const alias of an access path (`const out = child.stdout`) now resolves to that path, so a setEncoding through one alias and a listener through another, or through the stream itself, agree. A setEncoding on another receiver that can hold the same stream (a let copy, a parameter, a call result) leaves the state unclear and reports the existing SC1090. Receivers that name different members of one root, or two const roots each initialized by a call or new, are treated as different streams.
This commit is contained in:
@@ -541,25 +541,64 @@ export function lowerChildWriterMethodCall(
|
||||
}
|
||||
|
||||
/** A receiver spelled as a plain access path (`child.stdout`, `out`): its
|
||||
* root symbol and member names, or null for other forms. */
|
||||
* root symbol and member names, or null for other forms. A `const` alias
|
||||
* of an access path (`const out = child.stdout`) resolves to that path,
|
||||
* so every spelling of one stream gets the same key. */
|
||||
function accessPathOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
depth = 0,
|
||||
): { root: ts.Symbol; path: string } | null {
|
||||
if (depth > 16) return null;
|
||||
if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr)) {
|
||||
return accessPathOf(lowerer, expr.expression);
|
||||
return accessPathOf(lowerer, expr.expression, depth + 1);
|
||||
}
|
||||
if (ts.isIdentifier(expr)) {
|
||||
const root = lowerer.checker.getSymbolAtLocation(expr);
|
||||
return root ? { root, path: "" } : null;
|
||||
if (!root) return null;
|
||||
const init = constInitializerOf(root);
|
||||
const aliased = init === null ? null : accessPathOf(lowerer, init, depth + 1);
|
||||
return aliased ?? { root, path: "" };
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
|
||||
const base = accessPathOf(lowerer, expr.expression);
|
||||
const base = accessPathOf(lowerer, expr.expression, depth + 1);
|
||||
return base === null ? null : { root: base.root, path: `${base.path}.${expr.name.text}` };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The initializer of a symbol declared once as a `const` variable, else null. */
|
||||
function constInitializerOf(symbol: ts.Symbol): ts.Expression | null {
|
||||
const decls = symbol.declarations;
|
||||
if (decls.length !== 1) return null;
|
||||
const decl = decls[0]!;
|
||||
if (
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
!ts.isIdentifier(decl.name) ||
|
||||
decl.initializer === undefined ||
|
||||
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) === 0
|
||||
)
|
||||
return null;
|
||||
return decl.initializer;
|
||||
}
|
||||
|
||||
/** Whether two resolved access paths name different objects: the same root
|
||||
* with different members, or two `const` roots each initialized by a call
|
||||
* or `new` (such as two `spawn()` results). */
|
||||
function distinctPaths(
|
||||
a: { root: ts.Symbol; path: string },
|
||||
b: { root: ts.Symbol; path: string },
|
||||
): boolean {
|
||||
if (a.root === b.root) return a.path !== b.path;
|
||||
const fresh = (root: ts.Symbol): boolean => {
|
||||
let init = constInitializerOf(root);
|
||||
while (init !== null && (ts.isParenthesizedExpression(init) || ts.isAwaitExpression(init)))
|
||||
init = init.expression;
|
||||
return init !== null && (ts.isCallExpression(init) || ts.isNewExpression(init));
|
||||
};
|
||||
return fresh(a.root) && fresh(b.root);
|
||||
}
|
||||
|
||||
/** Whether an unknown-typed 'data' listener registered by `call` receives
|
||||
* strings: a `setEncoding` statement on the same receiver path precedes
|
||||
* the registration in its own block or an enclosing block of the same
|
||||
@@ -594,10 +633,7 @@ function childStreamEncodedAt(
|
||||
}
|
||||
node = parent;
|
||||
}
|
||||
const samePath = (
|
||||
a: { root: ts.Symbol; path: string } | null,
|
||||
b: { root: ts.Symbol; path: string },
|
||||
): boolean => a !== null && a.root === b.root && a.path === b.path;
|
||||
const receiverType = lowerer.checker.getNonNullableType(lowerer.typeOf(receiver));
|
||||
let before = false;
|
||||
let elsewhere = false;
|
||||
const visit = (node: ts.Node): void => {
|
||||
@@ -605,11 +641,21 @@ function childStreamEncodedAt(
|
||||
if (
|
||||
ts.isCallExpression(node) &&
|
||||
ts.isPropertyAccessExpression(node.expression) &&
|
||||
node.expression.name.text === "setEncoding" &&
|
||||
samePath(accessPathOf(lowerer, node.expression.expression), key)
|
||||
node.expression.name.text === "setEncoding"
|
||||
) {
|
||||
if (ts.isExpressionStatement(node.parent) && preceding.has(node.parent)) before = true;
|
||||
else elsewhere = true;
|
||||
const target = node.expression.expression;
|
||||
const path = accessPathOf(lowerer, target);
|
||||
if (path !== null && path.root === key.root && path.path === key.path) {
|
||||
if (ts.isExpressionStatement(node.parent) && preceding.has(node.parent)) before = true;
|
||||
else elsewhere = true;
|
||||
} else if (
|
||||
(path === null || !distinctPaths(path, key)) &&
|
||||
lowerer.checker.isTypeAssignableTo(receiverType, lowerer.typeOf(target))
|
||||
) {
|
||||
// Another spelling that can hold this stream (a parameter, a `let`
|
||||
// copy, a call result): its setEncoding may be this stream's.
|
||||
elsewhere = true;
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
|
||||
@@ -60,6 +60,28 @@ function runRaw(script: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
// setEncoding through one const alias of the stream and the listener
|
||||
// through another: both name child.stdout, so chunks are strings.
|
||||
function runAliased(script: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn("/bin/sh", ["-c", script], { stdio: "pipe" });
|
||||
const encoded = child.stdout!;
|
||||
const listened = child.stdout!;
|
||||
let stdout = "";
|
||||
const kinds: string[] = [];
|
||||
encoded.setEncoding("utf8");
|
||||
listened.on("data", (c: unknown) => {
|
||||
kinds.push(typeof c);
|
||||
stdout += String(c);
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
report(code, kinds, stdout, "");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
runEncoded("printf 'h\\303\\251llo'; echo warn >&2")
|
||||
.then(() => runRaw("printf raw; echo oops >&2; exit 2"))
|
||||
.then(() => runAliased("printf alias"))
|
||||
.then(() => console.log("done"));
|
||||
|
||||
@@ -47,3 +47,8 @@ fork(staticWorker, [], { stdio: ["ignore", "ignore", "ignore", "ignore"] });
|
||||
const pipedOut = piped.stdout!;
|
||||
pipedOut.on("data", (chunk: unknown) => console.log(String(chunk)));
|
||||
pipedOut.setEncoding("utf8");
|
||||
// A setEncoding through a copy that can hold the same stream (a `let`
|
||||
// binding here) leaves the state unclear as well.
|
||||
let pipedErr = piped.stderr!;
|
||||
pipedErr.setEncoding("utf8");
|
||||
piped.stderr!.on("data", (chunk: unknown) => console.log(String(chunk)));
|
||||
|
||||
@@ -98,4 +98,11 @@ child-process.ts:48:21 - error SC1090: untyped child-stream 'data' listeners who
|
||||
47 | const pipedOut = piped.stdout!;
|
||||
48 | pipedOut.on("data", (chunk: unknown) => console.log(String(chunk)));
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
49 | pipedOut.setEncoding("utf8");
|
||||
49 | pipedOut.setEncoding("utf8");
|
||||
|
||||
child-process.ts:54:26 - error SC1090: untyped child-stream 'data' listeners whose setEncoding state is unclear at their registration sites (declare the chunk as 'Buffer' or 'string') are not supported yet
|
||||
|
||||
53 | pipedErr.setEncoding("utf8");
|
||||
54 | piped.stderr!.on("data", (chunk: unknown) => console.log(String(chunk)));
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
55 |
|
||||
Reference in New Issue
Block a user