From 0fa4202ca928c4f46e093ac8fec6291c25a072ed Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Wed, 2 Sep 2026 09:30:26 -0700 Subject: [PATCH] Preserve unresolved linked CSS selectors AI assistance disclosure: Codex implemented and verified this fix under maintainer direction. --- cli/engine/browser/injected/index.mjs | 7 ++++++- cli/engine/detect-antipatterns-browser.js | 7 ++++++- tests/detect-antipatterns-browser.test.mjs | 5 +++++ tests/fixtures/antipatterns/linked-url-patterns.css | 13 +++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/cli/engine/browser/injected/index.mjs b/cli/engine/browser/injected/index.mjs index 3dfc3c266..de4acfd70 100644 --- a/cli/engine/browser/injected/index.mjs +++ b/cli/engine/browser/injected/index.mjs @@ -1332,7 +1332,12 @@ if (IS_BROWSER) { } const cssText = rule.cssText || ''; if (rule.selectorText) { - if (selectorNodesForLiveDom(document, rule.selectorText)?.length > 0) parts.push(cssText); + const matches = selectorNodesForLiveDom(document, rule.selectorText); + // A null result means the DOM selector API cannot resolve the CSSOM + // selector (commonly a hostless pseudo-element), not that it is + // unused. Keep those valid stylesheet rules; only a definitive empty + // result proves that no live element can receive the declaration. + if (matches === null || matches.length > 0) parts.push(cssText); continue; } let nested = []; diff --git a/cli/engine/detect-antipatterns-browser.js b/cli/engine/detect-antipatterns-browser.js index b9424f582..abc720d22 100644 --- a/cli/engine/detect-antipatterns-browser.js +++ b/cli/engine/detect-antipatterns-browser.js @@ -8233,7 +8233,12 @@ if (IS_BROWSER) { } const cssText = rule.cssText || ''; if (rule.selectorText) { - if (selectorNodesForLiveDom(document, rule.selectorText)?.length > 0) parts.push(cssText); + const matches = selectorNodesForLiveDom(document, rule.selectorText); + // A null result means the DOM selector API cannot resolve the CSSOM + // selector (commonly a hostless pseudo-element), not that it is + // unused. Keep those valid stylesheet rules; only a definitive empty + // result proves that no live element can receive the declaration. + if (matches === null || matches.length > 0) parts.push(cssText); continue; } let nested = []; diff --git a/tests/detect-antipatterns-browser.test.mjs b/tests/detect-antipatterns-browser.test.mjs index 967416518..5d6fe56b3 100644 --- a/tests/detect-antipatterns-browser.test.mjs +++ b/tests/detect-antipatterns-browser.test.mjs @@ -1409,6 +1409,11 @@ describe('detectUrl — browser-only fixtures', () => { assert.equal(stripes.length, 1, JSON.stringify({ linkedFindings, linkedCssom })); assert.equal(stripes[0].severity, 'advisory'); assert.equal(stripes[0].advisory, true); + assert.equal( + linkedFindings.some(finding => finding.type === 'bounce-easing'), + true, + JSON.stringify({ linkedFindings, linkedCssom }), + ); assert.equal(linkedFindings.some(finding => finding.type === 'codex-grid-background'), false); assert.equal(linkedFindings.some(finding => finding.type === 'layout-transition'), false); await linkedPage.close(); diff --git a/tests/fixtures/antipatterns/linked-url-patterns.css b/tests/fixtures/antipatterns/linked-url-patterns.css index b5304d589..9db4551d8 100644 --- a/tests/fixtures/antipatterns/linked-url-patterns.css +++ b/tests/fixtures/antipatterns/linked-url-patterns.css @@ -6,6 +6,19 @@ } } +/* A valid hostless pseudo-element selector cannot be queried through the DOM + selector API. It must remain in the corpus rather than count as unused. */ +main > ::before { + content: "Rendered pseudo-element text"; + display: block; + width: 160px; + animation: bounce-linked-pseudo 1s ease-in-out infinite; +} + +@keyframes bounce-linked-pseudo { + 50% { transform: translateY(2px); } +} + /* Shipped but unused selectors must remain outside live URL findings, even inside grouping rules or when their check does not serialize a selector. */ @supports (display: grid) {