From 50aec8b38aa918873ab90a6b3a83c9f5fdb85a35 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Tue, 30 Jun 2026 01:16:42 -0700 Subject: [PATCH 1/2] fix(lint): stop overlapping_gsap_tweens flagging distinct unresolved targets The GSAP parser assigns the sentinel `__unresolved__` to any tween whose target it cannot statically resolve to a concrete element (a computed variable, a helper call, etc.). The overlap check compared tweens by that target string, so two tweens aimed at completely different elements via unresolvable selectors (e.g. `#s0 .hl .w` and `#s1 .hl .w` produced by a helper) both collapsed to `__unresolved__` and were reported as overlapping, a false positive. An unresolved target is an unknown element: two of them are not provably the same element, so an overlap between them cannot be asserted. Skip overlap analysis when the target is the sentinel. Genuine overlaps on a resolved element are still flagged. --- packages/lint/src/rules/gsap.test.ts | 26 ++++++++++++++++++++++++++ packages/lint/src/rules/gsap.ts | 9 +++++++++ 2 files changed, 35 insertions(+) diff --git a/packages/lint/src/rules/gsap.test.ts b/packages/lint/src/rules/gsap.test.ts index e9b46325a0..d121bd773a 100644 --- a/packages/lint/src/rules/gsap.test.ts +++ b/packages/lint/src/rules/gsap.test.ts @@ -952,6 +952,32 @@ describe("GSAP rules", () => { expect(finding).toBeDefined(); }); + it("does NOT report overlapping_gsap_tweens for distinct unresolved-target tweens", async () => { + // Each tween targets a DIFFERENT element via a target the parser cannot resolve + // statically (a helper call). Both collapse to the `__unresolved__` sentinel, but + // they are not the same element, so an overlap must not be asserted between them. + const html = ` + +
+
a
+
b
+
+ + +`; + const result = await lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "overlapping_gsap_tweens"); + expect(finding).toBeUndefined(); + }); + it("warns when an opacity exit ends at a clip start boundary without a hard kill", async () => { const html = ` diff --git a/packages/lint/src/rules/gsap.ts b/packages/lint/src/rules/gsap.ts index ef792db500..f4ffcc549b 100644 --- a/packages/lint/src/rules/gsap.ts +++ b/packages/lint/src/rules/gsap.ts @@ -50,6 +50,12 @@ type CompositionRange = { const SCENE_BOUNDARY_EPSILON_SECONDS = 0.05; +// Sentinel the GSAP parser assigns to a tween whose target it cannot statically +// resolve to a concrete element (a computed variable, a helper call, etc.). It is +// NOT an identity: two distinct unresolved selectors are not the same element, so +// overlap analysis must never treat them as one. +const UNRESOLVED_TARGET = "__unresolved__"; + // ── GSAP parsing utilities ───────────────────────────────────────────────── function countClassUsage(tags: OpenTag[]): Map { @@ -546,6 +552,9 @@ export const gsapRules: LintRule[] = [ const left = gsapWindows[i]; if (!left) continue; if (left.end <= left.position) continue; + // Unresolved targets are unknown elements: two of them are not provably + // the same element, so an overlap between them cannot be asserted. + if (left.targetSelector === UNRESOLVED_TARGET) continue; for (let j = i + 1; j < gsapWindows.length; j++) { const right = gsapWindows[j]; if (!right) continue; From 28a4a1c6fe7054fd2430f77de2490694034f91c1 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Tue, 30 Jun 2026 10:27:45 -0700 Subject: [PATCH 2/2] fix(lint): guard gsap_exit_missing_hard_kill against the unresolved-target sentinel The overlap rule already skips tweens whose target collapses to the __unresolved__ sentinel, but the sibling exit rule in the same file did not. A scene-boundary exit on an unresolved target could emit a finding like GSAP exit on "__unresolved__" ... with a meaningless tl.set("__unresolved__", ...) fix hint. An unresolved target is an unknown element: you cannot assert a missing hard kill on it, so skip the window early in the loop, mirroring the overlap rule. Exits on resolved selectors are still flagged. --- packages/lint/src/rules/gsap.test.ts | 31 ++++++++++++++++++++++++++++ packages/lint/src/rules/gsap.ts | 3 +++ 2 files changed, 34 insertions(+) diff --git a/packages/lint/src/rules/gsap.test.ts b/packages/lint/src/rules/gsap.test.ts index d121bd773a..d60992b2d3 100644 --- a/packages/lint/src/rules/gsap.test.ts +++ b/packages/lint/src/rules/gsap.test.ts @@ -1005,6 +1005,37 @@ describe("GSAP rules", () => { expect(finding?.message).toContain("3.00s"); }); + it("does NOT report gsap_exit_missing_hard_kill for an unresolved-target boundary exit", async () => { + // The exit tween targets an element via a value the parser cannot resolve (a helper + // call), so it collapses to the `__unresolved__` sentinel. You cannot assert a missing + // hard kill on an unknown element, and a `tl.set("__unresolved__", ...)` hint is + // meaningless. The resolved-target exit in the same timeline is still flagged. + const html = ` + +
+
+

First beat

+
+
+

Second beat

+
+
+ + +`; + const result = await lintHyperframeHtml(html); + const exitFindings = result.findings.filter((f) => f.code === "gsap_exit_missing_hard_kill"); + expect(exitFindings).toHaveLength(1); + expect(exitFindings[0]?.selector).toBe("#headline"); + }); + it("does not warn when a boundary exit has a matching hard kill", async () => { const html = ` diff --git a/packages/lint/src/rules/gsap.ts b/packages/lint/src/rules/gsap.ts index f4ffcc549b..03fd0e6219 100644 --- a/packages/lint/src/rules/gsap.ts +++ b/packages/lint/src/rules/gsap.ts @@ -582,6 +582,9 @@ export const gsapRules: LintRule[] = [ // gsap_exit_missing_hard_kill if (clipStartBoundaries.length > 0) { for (const win of gsapWindows) { + // Unresolved targets are unknown elements: you cannot assert a missing + // hard kill on one, and a `tl.set("__unresolved__", ...)` hint is meaningless. + if (win.targetSelector === UNRESOLVED_TARGET) continue; if (!isSceneBoundaryExit(win)) continue; const boundary = findMatchingSceneBoundary(win.end, clipStartBoundaries); if (boundary == null) continue;