Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
105 changes: 105 additions & 0 deletions packages/ts-transformers/src/transformers/opaque-ref/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ function originatesFromIgnoredParameter(
const inner = (expr: ts.Expression): boolean => {
if (ts.isIdentifier(expr)) {
const symbol = checker.getSymbolAtLocation(expr);

// Don't filter identifiers without symbols here - they might be synthetic
// identifiers created by transformers (like map callback parameters), or
// they might be legitimate identifiers that lost their symbols. Let
// filterRelevantDataFlows handle this with more context about all the
// dataflows being analyzed together.
if (!symbol) {
return false;
}

return isIgnoredSymbol(symbol);
}
if (
Expand Down Expand Up @@ -85,6 +95,101 @@ export function filterRelevantDataFlows(
analysis: DataFlowAnalysis,
context: TransformationContext,
): NormalizedDataFlow[] {
// Check if we have identifiers without symbols (synthetic identifiers created by transformers)
const hasSyntheticRoot = (expr: ts.Expression): boolean => {
let current = expr;
while (
ts.isPropertyAccessExpression(current) ||
ts.isElementAccessExpression(current)
) {
current = current.expression;
}
if (ts.isIdentifier(current)) {
const symbol = context.checker.getSymbolAtLocation(current);
// No symbol means it's likely a synthetic identifier
return !symbol;
}
return false;
};

const syntheticDataFlows = dataFlows.filter((df) =>
hasSyntheticRoot(df.expression)
);
const nonSyntheticDataFlows = dataFlows.filter((df) =>
!hasSyntheticRoot(df.expression)
);

// If we have both synthetic and non-synthetic dataflows, we need to determine
// if we're inside or outside the map callback
if (syntheticDataFlows.length > 0 && nonSyntheticDataFlows.length > 0) {
// Check if any dataflow is in a scope with parameters from a map callback
// If so, we're inside a callback being transformed and should keep all dataflows
const isInMapCallbackScope = dataFlows.some((df) => {
const scope = analysis.graph.scopes.find((s) => s.id === df.scopeId);
if (!scope) return false;

// Check if any parameter in this scope comes from a builder or array-map
return scope.parameters.some((param) => {
if (!param.declaration) return false;
const callKind = getOpaqueCallKindForParameter(
param.declaration,
context.checker,
);
return callKind === "builder" || callKind === "array-map";
});
});

// If we're inside a map callback scope, keep all dataflows
if (isInMapCallbackScope) {
// Keep all dataflows - we're inside a callback with both synthetic params and captures
return dataFlows.filter((dataFlow) => {
if (
originatesFromIgnoredParameter(
dataFlow.expression,
dataFlow.scopeId,
analysis,
context.checker,
)
) {
return false;
}
return true;
});
}

// Heuristic: Check if the non-synthetic dataflows have symbols in the outer
// scope If they reference outer-scope variables (like cells), we're at the
// outer scope and should filter synthetic params.
const nonSyntheticHaveOuterScopeSymbols = nonSyntheticDataFlows.every(
(df) => {
let rootExpr: ts.Expression = df.expression;
while (
ts.isPropertyAccessExpression(rootExpr) ||
ts.isElementAccessExpression(rootExpr)
) {
rootExpr = rootExpr.expression;
}
if (ts.isIdentifier(rootExpr)) {
const symbol = context.checker.getSymbolAtLocation(rootExpr);
// If it has a symbol, it's likely from outer scope
return !!symbol;
}
return false;
},
);

// If all non-synthetic dataflows have outer-scope symbols AND we have 2 or
// more of them, we're likely analyzing an outer-scope expression that
// contains nested map callbacks
if (
nonSyntheticHaveOuterScopeSymbols && nonSyntheticDataFlows.length >= 2
) {
return dataFlows.filter((df) => !hasSyntheticRoot(df.expression));
}

// Otherwise, we're inside a map callback with captures - keep all dataflows
}

return dataFlows.filter((dataFlow) => {
if (
originatesFromIgnoredParameter(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as __ctHelpers from "commontools";
import { cell, recipe, UI } from "commontools";
export default recipe("MapNestedConditional", (_state) => {
const items = cell([{ name: "apple" }, { name: "banana" }]);
const showList = cell(true);
return {
[UI]: (<div>
{__ctHelpers.derive({ showList, items }, ({ showList: showList, items: items }) => showList && (<div>
{items.mapWithPattern(__ctHelpers.recipe({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {
element: {
$ref: "#/$defs/__object"
},
params: {
type: "object",
properties: {}
}
},
required: ["element", "params"],
$defs: {
__object: {
type: "object",
properties: {
name: {
type: "string"
}
},
required: ["name"]
}
}
} as const satisfies __ctHelpers.JSONSchema, ({ element, params: {} }) => (<div>
{__ctHelpers.derive(element.name, _v1 => _v1 && <span>{_v1}</span>)}
</div>)), {})}
</div>))}
</div>),
};
});
// @ts-ignore: Internals
function h(...args: any[]) { return __ctHelpers.h.apply(null, args); }
// @ts-ignore: Internals
h.fragment = __ctHelpers.h.fragment;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// <cts-enable />
import { cell, recipe, UI } from "commontools";

export default recipe("MapNestedConditional", (_state) => {
const items = cell([{ name: "apple" }, { name: "banana" }]);
const showList = cell(true);

return {
[UI]: (
<div>
{showList && (
<div>
{items.map((item) => (
<div>
{item.name && <span>{item.name}</span>}
</div>
))}
</div>
)}
</div>
),
};
});