From cb40dfb4562f07eebc96d993b96d2917702ebea4 Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Sat, 24 Oct 2026 08:15:52 +0000 Subject: [PATCH] fix(lint): reject parser-invalid variable defaults (#5314) --- packages/lint/src/rules/composition.test.ts | 60 +++++++++++++++++++++ packages/lint/src/rules/composition.ts | 17 +++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/packages/lint/src/rules/composition.test.ts b/packages/lint/src/rules/composition.test.ts index 90a4b05c71..8cbf698c51 100644 --- a/packages/lint/src/rules/composition.test.ts +++ b/packages/lint/src/rules/composition.test.ts @@ -1,6 +1,7 @@ // fallow-ignore-file code-duplication import { describe, it, expect } from "vitest"; import { lintHyperframeHtml } from "../hyperframeLinter.js"; +import { parseCompositionVariables } from "@hyperframes/parsers/composition"; describe("composition rules", () => { describe("canonical timing contract", () => { @@ -1222,6 +1223,65 @@ describe("composition rules", () => { }); describe("invalid_composition_variables_declaration", () => { + const invalidDeclarations = [ + { id: "setting", type: "string", label: "Setting", default: 3 }, + { id: "setting", type: "number", label: "Setting", default: "3" }, + { id: "setting", type: "boolean", label: "Setting", default: "false" }, + { id: "setting", type: "color", label: "Setting", default: 3 }, + { id: "setting", type: "font", label: "Setting", default: 3 }, + { id: "setting", type: "image", label: "Setting", default: 3 }, + { id: "setting", type: "enum", label: "Setting", default: 3, options: [] }, + { id: "setting", type: "enum", label: "Setting", default: "light" }, + { id: "setting", type: "enum", label: "Setting", default: "light", options: "light" }, + ]; + + for (const placement of ["html", "root", "template"]) { + it.each(invalidDeclarations)( + `reports parser-rejected declaration %j on ${placement}`, + async (declaration) => { + const attribute = `data-composition-variables='${JSON.stringify([declaration])}'`; + const root = `
`; + const html = + placement === "template" + ? `${root}` + : `${root}`; + const element = document.createElement("div"); + element.setAttribute("data-composition-variables", JSON.stringify([declaration])); + expect(parseCompositionVariables(element)).toEqual([]); + const result = await lintHyperframeHtml(html, { + isSubComposition: placement === "template", + }); + const findings = result.findings.filter( + (finding) => finding.code === "invalid_composition_variables_declaration", + ); + expect(findings).toHaveLength(1); + expect(findings[0]?.message).toMatch(/\[0\]/); + expect(findings[0]?.severity).toBe("error"); + }, + ); + } + + it.each([ + { id: "setting", type: "string", label: "Setting", default: "" }, + { id: "setting", type: "number", label: "Setting", default: 0 }, + { id: "setting", type: "boolean", label: "Setting", default: false }, + { id: "setting", type: "color", label: "Setting", default: "#fff" }, + { id: "setting", type: "font", label: "Setting", default: "Arial" }, + { id: "setting", type: "image", label: "Setting", default: "assets/a.png" }, + { id: "setting", type: "enum", label: "Setting", default: "light", options: [] }, + ])("preserves parser-admitted declaration %j", async (declaration) => { + const element = document.createElement("div"); + element.setAttribute("data-composition-variables", JSON.stringify([declaration])); + expect(parseCompositionVariables(element)).toEqual([declaration]); + const html = ``; + const result = await lintHyperframeHtml(html); + expect( + result.findings.filter( + (finding) => finding.code === "invalid_composition_variables_declaration", + ), + ).toEqual([]); + }); + it("checks a declaration on the composition root too", async () => { const html = ``; const result = await lintHyperframeHtml(html); diff --git a/packages/lint/src/rules/composition.ts b/packages/lint/src/rules/composition.ts index ee80b1478c..ef647ba35b 100644 --- a/packages/lint/src/rules/composition.ts +++ b/packages/lint/src/rules/composition.ts @@ -11,7 +11,11 @@ import { truncateSnippet, WINDOW_TIMELINE_ASSIGN_PATTERN, } from "../utils"; -import { COMPOSITION_VARIABLE_TYPES, isSafeMediaUrl } from "@hyperframes/parsers/composition"; +import { + COMPOSITION_VARIABLE_TYPES, + isCompositionVariable, + isSafeMediaUrl, +} from "@hyperframes/parsers/composition"; import { COMPOSITION_ATTRIBUTES, readClipTiming } from "@hyperframes/parsers/composition-contract"; import { resolveCompositionDuration } from "@hyperframes/parsers/composition-duration"; import { @@ -482,6 +486,17 @@ function variablesDeclarationFindings( }); continue; } + if (!isCompositionVariable(e)) { + findings.push({ + code: "invalid_composition_variables_declaration", + severity: "error", + message: `data-composition-variables entry [${i}] has a default that does not match its type, or an enum without an options array.`, + fixHint: + "Use number defaults for number variables, boolean defaults for boolean variables, and string defaults for other types. Enum variables also require an options array.", + snippet: truncateSnippet(tag.raw), + }); + continue; + } const id = String(e.id); if ( (e.type === "image" || varSrcIds.has(id)) &&