diff --git a/cli/engine/engines/regex/detect-text.mjs b/cli/engine/engines/regex/detect-text.mjs
index 8bce32525..1545ea060 100644
--- a/cli/engine/engines/regex/detect-text.mjs
+++ b/cli/engine/engines/regex/detect-text.mjs
@@ -42,6 +42,7 @@ function shouldRunPageAnalyzers(content, filePath) {
}
const JS_SOURCE_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
+const STYLESHEET_EXTS = new Set(['.css', '.scss', '.sass', '.less']);
const REGEX_PREFIX_KEYWORDS = new Set(['await', 'case', 'default', 'delete', 'do', 'else', 'in', 'instanceof', 'new', 'of', 'return', 'throw', 'typeof', 'void', 'yield']);
const BLOCK_BRACE_PREFIX_KEYWORDS = new Set(['do', 'else', 'finally', 'try']);
@@ -256,6 +257,27 @@ function stripCssComments(content) {
return content.replace(/\/\*[\s\S]*?\*\//g, comment => comment.replace(/[^\n]/g, ' '));
}
+function blankHtmlComments(text) {
+ return text.replace(//g, comment => comment.replace(/[^\n]/g, ' '));
+}
+
+function blankAstroFrontmatterComments(text) {
+ if (!text.startsWith('---')) return text;
+ const close = text.indexOf('\n---', 3);
+ if (close === -1) return text;
+ return stripJsComments(text.slice(0, close)) + text.slice(close);
+}
+
+function blankCommentsForMatchers(text, ext) {
+ if (PAGE_ANALYZER_EXTS.has(ext)) {
+ let next = stripCssComments(blankHtmlComments(text));
+ if (ext === '.astro') next = blankAstroFrontmatterComments(next);
+ return next;
+ }
+ if (STYLESHEET_EXTS.has(ext)) return stripCssComments(text);
+ return text;
+}
+
function firstOverusedGoogleFont(text) {
return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || '';
}
@@ -1028,14 +1050,13 @@ function detectText(content, filePath, options = {}) {
const ext = extFromFilePath(filePath);
const commentStrippedSource = JS_SOURCE_EXTS.has(ext) ? stripJsComments(content, {
jsx: ext === '.js' || ext === '.jsx' || ext === '.tsx',
- }) : content;
+ }) : blankCommentsForMatchers(content, ext);
const source = stripCssInJsComments(commentStrippedSource, ext);
const lines = source.split('\n');
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
// Enable block context for CSS files where related properties span multiple lines
- const cssLike = new Set(['.css', '.scss', '.sass', '.less']);
- findings.push(...runRegexMatchers(lines, filePath, 0, cssLike.has(ext) || null, {
+ findings.push(...runRegexMatchers(lines, filePath, 0, STYLESHEET_EXTS.has(ext) || null, {
profile,
phase: 'source',
}));
@@ -1050,7 +1071,7 @@ function detectText(content, filePath, options = {}) {
scanCssTextForPseudoStripe(text).map(hit =>
finding(hit.id, filePath, hit.snippet, lineOffset + text.slice(0, hit.index).split('\n').length));
- if (cssLike.has(ext)) {
+ if (STYLESHEET_EXTS.has(ext)) {
findings.push(...scanInsetStripeCss(content, filePath));
findings.push(...pseudoStripeFindings(content, 0));
}
@@ -1078,7 +1099,8 @@ function detectText(content, filePath, options = {}) {
}, () => extractStyleBlocks(content, ext))
: extractStyleBlocks(content, ext);
for (const block of styleBlocks) {
- const blockLines = block.content.split('\n');
+ const blockContent = stripCssComments(block.content);
+ const blockLines = blockContent.split('\n');
findings.push(...runRegexMatchers(blockLines, filePath, block.startLine - 1, true, {
profile,
phase: 'style-block',
@@ -1089,8 +1111,8 @@ function detectText(content, filePath, options = {}) {
// 1-based, so the offset is startLine - 2; startLine - 1 double-counted and
// reported every selector one line low. runRegexMatchers keeps startLine - 1
// because it indexes its split lines from zero.
- findings.push(...scanInsetStripeCss(block.content, filePath, block.startLine - 2));
- findings.push(...pseudoStripeFindings(block.content, block.startLine - 2));
+ findings.push(...scanInsetStripeCss(blockContent, filePath, block.startLine - 2));
+ findings.push(...pseudoStripeFindings(blockContent, block.startLine - 2));
}
// Extract and scan CSS-in-JS template literals
diff --git a/tests/detect-antipatterns.test.js b/tests/detect-antipatterns.test.js
index 2c7141365..b78b51a0f 100644
--- a/tests/detect-antipatterns.test.js
+++ b/tests/detect-antipatterns.test.js
@@ -382,6 +382,119 @@ describe('detectText — broken images in source comments', () => {
expect(findings.filter(r => r.antipattern === 'broken-image')).toHaveLength(0);
});
+
+ test('ignores img tags in Astro style block comments', () => {
+ const source = [
+ '---',
+ 'const title = "Hero";',
+ '---',
+ '',
+ ].join('\n');
+
+ const findings = detectText(source, 'hero.astro');
+
+ expect(findings.filter(r => r.antipattern === 'broken-image')).toHaveLength(0);
+ });
+
+ test('ignores img tags in Astro HTML comments', () => {
+ const source = [
+ '---',
+ 'const title = "Hero";',
+ '---',
+ '',
+ '
',
+ ].join('\n');
+
+ const findings = detectText(source, 'hero.astro');
+
+ expect(findings.filter(r => r.antipattern === 'broken-image')).toHaveLength(0);
+ });
+
+ test('ignores img tags in Astro frontmatter line comments', () => {
+ const source = [
+ '---',
+ '// ',
+ 'const site = "https://example.com";',
+ '---',
+ '
',
+ ].join('\n');
+
+ const findings = detectText(source, 'hero.astro');
+
+ expect(findings.filter(r => r.antipattern === 'broken-image')).toHaveLength(0);
+ });
+
+ test('ignores img tags in CSS block comments', () => {
+ const source = [
+ '/*',
+ ' * Example markup: ',
+ ' */',
+ '.hero { color: red; }',
+ ].join('\n');
+
+ const findings = detectText(source, 'hero.css');
+
+ expect(findings.filter(r => r.antipattern === 'broken-image')).toHaveLength(0);
+ });
+
+ test('still detects real img tags after an HTML comment in Astro', () => {
+ const source = [
+ '---',
+ 'const title = "Hero";',
+ '---',
+ '',
+ '
',
+ ].join('\n');
+
+ const findings = detectText(source, 'hero.astro');
+
+ const broken = findings.filter(r => r.antipattern === 'broken-image');
+ expect(broken).toHaveLength(1);
+ expect(broken[0].line).toBe(5);
+ });
+
+ test('does not blank https URLs in Astro frontmatter', () => {
+ const source = [
+ '---',
+ 'const site = "https://example.com/logo.png";',
+ '---',
+ '
',
+ ].join('\n');
+
+ const findings = detectText(source, 'hero.astro');
+
+ expect(findings.filter(r => r.antipattern === 'broken-image')).toHaveLength(1);
+ });
+
+ test('keeps same-line img visible after a bare https URL in Astro markup', () => {
+ const source = '
https://example.com
Intro copy
', + '