fix(lint): reject parser-invalid variable defaults (#5314)

This commit is contained in:
Rudy Mizrahi Celekli
2026-10-24 08:15:52 +00:00
committed by GitHub
parent 004409e570
commit cb40dfb456
2 changed files with 76 additions and 1 deletions
@@ -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 = `<div data-composition-id="main" data-no-timeline data-width="1920" data-height="1080" data-duration="2" ${placement === "html" ? "" : attribute}></div>`;
const html =
placement === "template"
? `<template>${root}</template>`
: `<html ${placement === "html" ? attribute : ""}><body>${root}</body></html>`;
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 = `<html data-composition-variables='${JSON.stringify([declaration])}'><body><div data-composition-id="main" data-no-timeline data-width="1920" data-height="1080" data-duration="2"></div></body></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 = `<html><body><div data-composition-id="x" data-composition-variables='[{"id":12345}]'></div></body></html>`;
const result = await lintHyperframeHtml(html);
+16 -1
View File
@@ -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)) &&