Refactor how cosmetic filters with pseudo-elements are parsed

Related issue:
- https://github.com/uBlockOrigin/uBlock-issues/issues/1247#issuecomment-953284365

Distinguish between selectors which can be querySelector-ed
and/or used ni a stylesheet.
This commit is contained in:
Raymond Hill 2021-10-27 18:09:02 -04:00
parent 97a33c9572
commit ef07171f5a
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
2 changed files with 44 additions and 57 deletions

View File

@ -387,7 +387,7 @@ FilterContainer.prototype.compileGenericHideSelector = function(
parser,
writer
) {
const { raw, compiled, pseudoclass } = parser.result;
const { raw, compiled } = parser.result;
if ( compiled === undefined ) {
const who = writer.properties.get('name') || '?';
logger.writeOne({
@ -432,7 +432,7 @@ FilterContainer.prototype.compileGenericHideSelector = function(
// https://github.com/uBlockOrigin/uBlock-issues/issues/131
// Support generic procedural filters as per advanced settings.
// TODO: prevent double compilation.
if ( compiled !== raw && pseudoclass === false ) {
if ( compiled !== raw ) {
if ( µb.hiddenSettings.allowGenericProceduralFilters === true ) {
return this.compileSpecificSelector(parser, '', false, writer);
}
@ -1031,11 +1031,6 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
continue;
}
}
if ( pfilter.pseudo !== undefined ) {
injectedHideFilters.push(pfilter.selector);
proceduralSet.delete(json);
continue;
}
}
if ( proceduralSet.size !== 0 ) {
out.proceduralFilters = Array.from(proceduralSet);

View File

@ -129,7 +129,6 @@ const Parser = class {
exception: false,
raw: '',
compiled: '',
pseudoclass: false,
};
this.reset();
}
@ -289,7 +288,6 @@ const Parser = class {
analyzeExtPattern() {
this.result.exception = this.isException();
this.result.compiled = undefined;
this.result.pseudoclass = false;
let selector = this.strFromSpan(this.patternSpan);
if ( selector === '' ) {
@ -1323,7 +1321,11 @@ Parser.prototype.SelectorCompiler = class {
style.remove();
return stylesheet;
})();
this.rePseudoElement = /:(?::?after|:?before|:-?[a-z][a-z-]*[a-z])$/;
this.div = (( ) => {
if ( typeof document !== 'object' ) { return null; }
if ( document instanceof Object === false ) { return null; }
return document.createElement('div');
})();
this.reProceduralOperator = new RegExp([
'^(?:',
Array.from(parser.proceduralOperatorTokens.keys()).join('|'),
@ -1366,11 +1368,11 @@ Parser.prototype.SelectorCompiler = class {
}
let extendedSyntax = false;
const selectorType = this.cssSelectorType(raw);
if ( selectorType !== 0 ) {
// Can be used in a declarative CSS rule?
if ( this.sheetSelectable(raw) ) {
extendedSyntax = this.reExtendedSyntax.test(raw);
if ( (extendedSyntax || isProcedural) === false ) {
out.pseudoclass = selectorType === 3;
out.compiled = raw;
return true;
}
@ -1402,9 +1404,6 @@ Parser.prototype.SelectorCompiler = class {
const compiled = this.compileProceduralSelector(raw);
if ( compiled === undefined ) { return false; }
if ( compiled.pseudo !== undefined ) {
out.pseudoclass = compiled.pseudo;
}
out.compiled = compiled.selector !== compiled.raw
? JSON.stringify(compiled)
: compiled.selector;
@ -1429,11 +1428,6 @@ Parser.prototype.SelectorCompiler = class {
: `${selector}:style(${style})`;
}
// Return value:
// 0b00 (0) = not a valid CSS selector
// 0b01 (1) = valid CSS selector, without pseudo-element
// 0b11 (3) = valid CSS selector, with pseudo element
//
// Quick regex-based validation -- most cosmetic filters are of the
// simple form and in such case a regex is much faster.
// Keep in mind:
@ -1445,27 +1439,28 @@ Parser.prototype.SelectorCompiler = class {
// https://github.com/uBlockOrigin/uBlock-issues/issues/1751
// Do not rely on matches() or querySelector() to test whether a
// selector is declarative or not.
cssSelectorType(s) {
if ( this.reSimpleSelector.test(s) ) { return 1; }
const pos = this.cssPseudoElement(s);
if ( pos !== -1 ) {
return this.cssSelectorType(s.slice(0, pos)) === 1 ? 3 : 0;
}
if ( this.stylesheet === null ) { return 1; }
sheetSelectable(s) {
if ( this.reSimpleSelector.test(s) ) { return true; }
if ( this.stylesheet === null ) { return true; }
try {
this.stylesheet.insertRule(`${s}{color:red}`);
if ( this.stylesheet.cssRules.length === 0 ) { return 0; }
if ( this.stylesheet.cssRules.length === 0 ) { return false; }
this.stylesheet.deleteRule(0);
} catch (ex) {
return 0;
return false;
}
return 1;
return true;
}
cssPseudoElement(s) {
if ( s.lastIndexOf(':') === -1 ) { return -1; }
const match = this.rePseudoElement.exec(s);
return match !== null ? match.index : -1;
querySelectable(s) {
if ( this.reSimpleSelector.test(s) ) { return true; }
if ( this.div === null ) { return true; }
try {
this.div.querySelector(`${s},${s}:not(#foo)`);
} catch (ex) {
return false;
}
return true;
}
compileProceduralSelector(raw) {
@ -1534,10 +1529,10 @@ Parser.prototype.SelectorCompiler = class {
// https://github.com/uBlockOrigin/uBlock-issues/issues/341#issuecomment-447603588
// Reject instances of :not() filters for which the argument is
// a valid CSS selector, otherwise we would be adversely
// changing the behavior of CSS4's :not().
// a valid CSS selector, otherwise we would be adversely changing the
// behavior of CSS4's :not().
compileNotSelector(s) {
if ( this.cssSelectorType(s) === 0 ) {
if ( this.querySelectable(s) === false ) {
return this.compileProcedural(s);
}
}
@ -1545,7 +1540,7 @@ Parser.prototype.SelectorCompiler = class {
compileUpwardArgument(s) {
const i = this.compileInteger(s, 1, 256);
if ( i !== undefined ) { return i; }
if ( this.cssSelectorType(s) === 1 ) { return s; }
if ( this.querySelectable(s) ) { return s; }
}
compileRemoveSelector(s) {
@ -1555,7 +1550,7 @@ Parser.prototype.SelectorCompiler = class {
// https://github.com/uBlockOrigin/uBlock-issues/issues/382#issuecomment-703725346
// Prepend `:scope` only when it can be deemed implicit.
compileSpathExpression(s) {
if ( this.cssSelectorType(/^\s*[+:>~]/.test(s) ? `:scope${s}` : s) === 1 ) {
if ( this.querySelectable(/^\s*[+:>~]/.test(s) ? `:scope${s}` : s) ) {
return s;
}
}
@ -1714,9 +1709,7 @@ Parser.prototype.SelectorCompiler = class {
// https://github.com/uBlockOrigin/uBlock-issues/issues/341#issuecomment-447603588
// Maybe that one operator is a valid CSS selector and if so,
// then consider it to be part of the prefix.
if ( this.cssSelectorType(raw.slice(opNameBeg, i)) === 1 ) {
continue;
}
if ( this.querySelectable(raw.slice(opNameBeg, i)) ) { continue; }
// Extract and remember operator details.
let operator = raw.slice(opNameBeg, opNameEnd);
operator = this.normalizedOperators.get(operator) || operator;
@ -1752,8 +1745,18 @@ Parser.prototype.SelectorCompiler = class {
// No task found: then we have a CSS selector.
// At least one task found: nothing should be left to parse.
if ( tasks.length === 0 && action === undefined ) {
if ( tasks.length === 0 ) {
if ( action === undefined ) {
prefix = raw;
}
if ( root && this.sheetSelectable(prefix) ) {
if ( action === undefined ) {
return { selector: prefix };
} else if ( action[0] === ':style' ) {
return { selector: prefix, action };
}
}
} else if ( opPrefixBeg < n ) {
if ( action !== undefined ) { return; }
const spath = this.compileSpathExpression(raw.slice(opPrefixBeg));
@ -1773,7 +1776,7 @@ Parser.prototype.SelectorCompiler = class {
prefix += ' *';
}
prefix = prefix.replace(this.reDropScope, '');
if ( this.cssSelectorType(prefix) === 0 ) {
if ( this.querySelectable(prefix) === false ) {
if (
root ||
this.reIsCombinator.test(prefix) === false ||
@ -1797,17 +1800,6 @@ Parser.prototype.SelectorCompiler = class {
out.action = action;
}
// Pseudo elements are valid only when used in a root task list AND
// only when there are no procedural operators: pseudo elements can't
// be querySelectorAll-ed.
if ( prefix !== '' ) {
const pos = this.cssPseudoElement(prefix);
if ( pos !== -1 ) {
if ( root === false || tasks.length !== 0 ) { return; }
out.pseudo = pos;
}
}
return out;
}