Fix: compile closest() selectors once per document in the static scanner (#575) (#595)

StaticElement.closest() handed the raw selector string to css-select's
is() on every ancestor step, recompiling the same selector N times for
an element N levels deep. StaticDocument now caches one compiled
matcher per selector (failed compiles cached as rethrowers so bad
selectors still return null). Findings are byte-identical across the
fixture corpus; scan time drops to ~62% on the fixtures and ~7x faster
on deep-DOM pages.

Prepared with AI assistance (Cursor agent), directed by @abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Abdul Wahab
2026-08-14 16:15:01 -07:00
committed by GitHub
co-authored by Cursor
parent c88d815e05
commit 886cd669ef
3 changed files with 63 additions and 3 deletions
+18 -2
View File
@@ -835,10 +835,11 @@ class StaticElement {
}
}
closest(selector) {
const matcher = this._doc.matcherFor(selector);
let cur = this.node;
while (cur && cur.type === 'tag') {
try {
if (this._doc.is(cur, selector)) return this._doc.wrap(cur);
if (matcher(cur)) return this._doc.wrap(cur);
} catch {
return null;
}
@@ -862,9 +863,10 @@ class StaticDocument {
this.root = root;
this.selectAll = modules.selectAll;
this.selectOne = modules.selectOne;
this.is = modules.is;
this.compile = modules.compile;
this.domutils = modules.domutils;
this._wrappers = new WeakMap();
this._compiledSelectors = new Map();
this._styleMap = new WeakMap();
this._hoverStyleMap = new WeakMap();
this._accentDashPseudo = new WeakSet();
@@ -882,6 +884,20 @@ class StaticDocument {
}
return wrapped;
}
matcherFor(selector) {
let matcher = this._compiledSelectors.get(selector);
if (!matcher) {
try {
matcher = this.compile(selector);
} catch (err) {
// Cache the failure as a rethrower so a bad selector still reaches
// closest()'s catch on every call, first and repeat alike.
matcher = () => { throw err; };
}
this._compiledSelectors.set(selector, matcher);
}
return matcher;
}
querySelectorAll(selector) {
try {
return this.selectAll(selector, this.root.children || []).map(node => this.wrap(node));
@@ -134,7 +134,7 @@ async function detectHtml(filePath, options = {}) {
parseDocument: htmlparser2.parseDocument,
selectAll: cssSelect.selectAll,
selectOne: cssSelect.selectOne,
is: cssSelect.is,
compile: cssSelect.compile,
csstree,
domutils,
};
+44
View File
@@ -10,6 +10,10 @@ import {
buildImportGraph, resolveImport,
detectFrameworkConfig, isPortListening, FRAMEWORK_CONFIGS,
} from '../cli/engine/detect-antipatterns.mjs';
import * as htmlparser2 from 'htmlparser2';
import * as cssSelect from 'css-select';
import * as domutils from 'domutils';
import { StaticDocument } from '../cli/engine/engines/static-html/css-cascade.mjs';
import { filterByScopes } from '../cli/engine/registry/antipatterns.mjs';
import {
checkColors,
@@ -1236,6 +1240,46 @@ describe('detectHtml — static HTML/CSS engine', () => {
});
});
describe('StaticDocument.closest — compiled selector cache', () => {
test('compiles each selector once per document', () => {
let compileCount = 0;
const compile = (sel) => {
compileCount++;
return cssSelect.compile(sel);
};
const root = htmlparser2.parseDocument(
'<html><body><div class="target-ancestor">' +
'<div><div><div><div><div><div><div><div><div><span>deep</span></div></div></div></div></div></div></div></div></div>' +
'</div><div><div><div><div><div><div><div><div><div><span>deep2</span></div></div></div></div></div></div></div></div></div></div></body></html>',
);
const doc = new StaticDocument(root, {
selectAll: cssSelect.selectAll,
selectOne: cssSelect.selectOne,
compile,
domutils,
});
const deep = doc.querySelectorAll('span')[0];
const deep2 = doc.querySelectorAll('span')[1];
expect(deep.closest('.target-ancestor').node.attribs.class).toBe('target-ancestor');
deep.closest('.target-ancestor');
deep2.closest('.target-ancestor');
expect(compileCount).toBe(1);
});
test('invalid selector returns null on repeat calls', () => {
const root = htmlparser2.parseDocument('<html><body><p>x</p></body></html>');
const doc = new StaticDocument(root, {
selectAll: cssSelect.selectAll,
selectOne: cssSelect.selectOne,
compile: cssSelect.compile,
domutils,
});
const p = doc.querySelector('p');
expect(p.closest('p:has-invalid(')).toBeNull();
expect(p.closest('p:has-invalid(')).toBeNull();
});
});
// ---------------------------------------------------------------------------
// Side-tab as absolutely-positioned pseudo-element stripe
// ---------------------------------------------------------------------------