From 589f7d7f88c71ba4034489f8189d34ac55d83fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 30 Jun 2026 20:15:24 +0000 Subject: [PATCH 1/3] fix(lint): catch visible markup comments --- packages/lint/src/rules/core.test.ts | 55 ++++++++++++++++++++++++++ packages/lint/src/rules/core.ts | 58 ++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/packages/lint/src/rules/core.test.ts b/packages/lint/src/rules/core.test.ts index e30f8f74b1..a575ffed1c 100644 --- a/packages/lint/src/rules/core.test.ts +++ b/packages/lint/src/rules/core.test.ts @@ -370,6 +370,61 @@ body { expect(finding).toBeUndefined(); }); + it("reports error when CSS block comment syntax leaks into visible markup", async () => { + const html = compositionWithBodyPrefix( + "", + ` + /* Main Content Block */ +
Hello
+`, + ); + const result = await lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "visible_markup_comment"); + + expect(finding).toBeDefined(); + expect(finding?.severity).toBe("error"); + expect(finding?.message).toContain("visible HTML markup"); + expect(finding?.snippet).toContain("Main Content Block"); + }); + + it("does not report block comments inside style or script blocks", async () => { + const html = ` + + + + + +
+ + +`; + const result = await lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "visible_markup_comment"); + + expect(finding).toBeUndefined(); + }); + + it("does not report block comments in attributes, html comments, or code examples", async () => { + const html = compositionWithBodyPrefix( + "", + ` + +
+
/* visible code sample */
+`, + ); + const result = await lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "visible_markup_comment"); + + expect(finding).toBeUndefined(); + }); + it("reports error when a stray style close tag is left in the document head", async () => { const html = compositionWithHead(` + + /* Main Content Block */ +
Hello
+`, + ); + const result = await lintHyperframeHtml(html); + const finding = result.findings.find((f) => f.code === "visible_markup_comment"); + + expect(finding).toBeDefined(); + expect(finding?.snippet).toContain("Main Content Block"); + }); + it("does not report block comments inside style or script blocks", async () => { const html = ` + /* tab name */ Particle Field +
@@ -410,13 +431,17 @@ body { expect(finding).toBeUndefined(); }); - it("does not report block comments in attributes, html comments, or code examples", async () => { + it("does not report block comments in attributes, html comments, or protected text contexts", async () => { const html = compositionWithBodyPrefix( "", `
/* visible code sample */
+ /* visible inline code sample */ + + + /* svg label */ `, ); const result = await lintHyperframeHtml(html); diff --git a/packages/lint/src/rules/core.ts b/packages/lint/src/rules/core.ts index 1c33377a7e..fd4e759a9b 100644 --- a/packages/lint/src/rules/core.ts +++ b/packages/lint/src/rules/core.ts @@ -70,7 +70,7 @@ const ORPHAN_CSS_RULE_PATTERN = /(?:^|\s)(?:\/\*[\s\S]*?\*\/\s*)?(?:@[a-z-]+[^{}<]*|[.#][\w-]+[^{}<]*|[a-z][\w-]*(?:\s+[.#:[\w-][^{}<]*)?)\s*\{[^{}]*:[^{}]*\}/i; const VISIBLE_MARKUP_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g; const VISIBLE_MARKUP_COMMENT_PROTECTED_BLOCK_PATTERN = - /<(style|script|template|pre|code|textarea)\b[^>]*>[\s\S]*?<\/\1(?:\s[^>]*)?>/gi; + /<(style|script|template|title|noscript|pre|code|textarea|text)\b[^>]*>[\s\S]*?<\/\1(?:\s[^>]*)?>/gi; interface SourceRange { start: number; From 872d091df3d7a79f1001f6a1f72ba287010154c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 30 Jun 2026 20:29:40 +0000 Subject: [PATCH 3/3] fix(lint): harden visible comment scan --- packages/lint/src/rules/core.test.ts | 1 + packages/lint/src/rules/core.ts | 31 +++++++++++++++++++--------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/lint/src/rules/core.test.ts b/packages/lint/src/rules/core.test.ts index 3d2f7994eb..5f601e8583 100644 --- a/packages/lint/src/rules/core.test.ts +++ b/packages/lint/src/rules/core.test.ts @@ -437,6 +437,7 @@ body { `
+
/* visible code sample */
/* visible inline code sample */ diff --git a/packages/lint/src/rules/core.ts b/packages/lint/src/rules/core.ts index fd4e759a9b..fec78abee4 100644 --- a/packages/lint/src/rules/core.ts +++ b/packages/lint/src/rules/core.ts @@ -137,9 +137,7 @@ function findLeakedTextBeforeCompositionRoot( function findProtectedVisibleMarkupRanges(source: string): SourceRange[] { const ranges: SourceRange[] = []; - VISIBLE_MARKUP_COMMENT_PROTECTED_BLOCK_PATTERN.lastIndex = 0; - let match: RegExpExecArray | null; - while ((match = VISIBLE_MARKUP_COMMENT_PROTECTED_BLOCK_PATTERN.exec(source)) !== null) { + for (const match of source.matchAll(VISIBLE_MARKUP_COMMENT_PROTECTED_BLOCK_PATTERN)) { ranges.push({ start: match.index, end: match.index + match[0].length }); } return ranges; @@ -150,17 +148,30 @@ function isInsideSourceRange(index: number, ranges: SourceRange[]): boolean { } function isInsideHtmlTag(source: string, index: number): boolean { - const lastOpen = source.lastIndexOf("<", index); - if (lastOpen === -1) return false; - const lastClose = source.lastIndexOf(">", index); - return lastOpen > lastClose; + let inTag = false; + let quote: '"' | "'" | null = null; + for (let i = 0; i < index; i++) { + const char = source[i]; + if (!inTag) { + if (char === "<") inTag = true; + continue; + } + if (quote) { + if (char === quote) quote = null; + continue; + } + if (char === '"' || char === "'") { + quote = char; + } else if (char === ">") { + inTag = false; + } + } + return inTag; } function findVisibleMarkupCommentLeak(source: string): string | null { const protectedRanges = findProtectedVisibleMarkupRanges(source); - VISIBLE_MARKUP_COMMENT_PATTERN.lastIndex = 0; - let match: RegExpExecArray | null; - while ((match = VISIBLE_MARKUP_COMMENT_PATTERN.exec(source)) !== null) { + for (const match of source.matchAll(VISIBLE_MARKUP_COMMENT_PATTERN)) { if (isInsideHtmlTag(source, match.index)) continue; if (isInsideSourceRange(match.index, protectedRanges)) continue; return match[0];