1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-07-08 12:57:57 +02:00

Add infrastructure for static filter syntax linter

Sort of related issue:
- https://github.com/uBlockOrigin/uBlock-issues/issues/1134
This commit is contained in:
Raymond Hill 2023-04-01 16:42:41 -04:00
parent b10f15dd89
commit 50afd5ae38
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
10 changed files with 254 additions and 14 deletions

View File

@ -52,10 +52,10 @@
<script src="lib/diff/swatinem_diff.js"></script>
<script src="lib/hsluv/hsluv-0.1.0.min.js"></script>
<script src="js/codemirror/search.js"></script>
<script src="js/codemirror/search.js" type="module"></script>
<script src="js/codemirror/search-thread.js"></script>
<script src="js/fa-icons.js"></script>
<script src="js/fa-icons.js" type="module"></script>
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>

View File

@ -1265,6 +1265,10 @@
"message": "Click to load",
"description": "Message used in frame placeholders"
},
"linterMainReport": {
"message": "Errors: {{count}}",
"description": "Summary of number of errors as reported by the linter "
},
"dummy": {
"message": "This entry must be the last one",
"description": "so we dont need to deal with comma for last entry"

View File

@ -35,10 +35,10 @@
<script src="lib/codemirror/addon/selection/active-line.js"></script>
<script src="lib/hsluv/hsluv-0.1.0.min.js"></script>
<script src="js/codemirror/search.js"></script>
<script src="js/codemirror/search.js" type="module"></script>
<script src="js/codemirror/search-thread.js"></script>
<script src="js/fa-icons.js"></script>
<script src="js/fa-icons.js" type="module"></script>
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>

View File

@ -36,10 +36,10 @@
<script src="lib/codemirror/mode/xml/xml.js"></script>
<script src="lib/codemirror/mode/htmlmixed/htmlmixed.js"></script>
<script src="js/codemirror/search.js"></script>
<script src="js/codemirror/search.js" type="module"></script>
<script src="js/codemirror/search-thread.js"></script>
<script src="lib/js-beautify/beautifier.min.js"></script>
<script src="js/fa-icons.js"></script>
<script src="js/fa-icons.js" type="module"></script>
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>

View File

@ -164,6 +164,13 @@
-webkit-user-select: none;
z-index: 1000;
}
.cm-search-widget > * {
flex-grow: 1;
}
.cm-search-widget > :last-child {
justify-content: flex-end;
}
.cm-search-widget-input {
display: inline-flex;
flex-grow: 1;
@ -196,8 +203,21 @@
color: #000;
}
.cm-search-widget .sourceURL[href=""] {
display: none;
visibility: hidden;
}
.cm-linter-widget {
display: none;
flex-grow: 1;
}
.cm-linter-widget.hasErrors {
display: initial;
}
.cm-linter-widget .cm-linter-widget-count {
color: var(--accent-surface-1);
fill: var(--accent-surface-1);
}
.cm-searching.cm-overlay {
background-color: var(--cm-searching-surface);
border: 0;
@ -247,3 +267,27 @@
.CodeMirror-activeline-background {
background-color: var(--cm-active-line);
}
.CodeMirror-lintmarker {
background-color: var(--sf-error-ink);
height: calc(var(--font-size) - 2px);
margin-top: 1px;
position: relative;
}
.CodeMirror-lintmarker > span {
display: none;
}
.CodeMirror-lintmarker > span {
background-color: var(--surface-0);
border: 1px solid var(--sf-error-ink);
color: var(--ink-1);
left: 100%;
padding: var(--default-gap-xsmall);
position: absolute;
top: -2px;
white-space: pre;
}
.CodeMirror-lintmarker:hover > span,
.CodeMirror-lintmarker > span:hover {
display: initial;
}

View File

@ -37,7 +37,11 @@ const cmEditor = new CodeMirror(qs$('#userFilters'), {
'Tab': 'toggleComment',
},
foldGutter: true,
gutters: [ 'CodeMirror-linenumbers', 'CodeMirror-foldgutter' ],
gutters: [
'CodeMirror-foldgutter',
'CodeMirror-linenumbers',
{ className: 'CodeMirror-lintgutter', style: 'width: 10px' },
],
lineNumbers: true,
lineWrapping: true,
matchBrackets: true,

View File

@ -52,7 +52,11 @@ import './codemirror/ubo-static-filtering.js';
const cmEditor = new CodeMirror(qs$('#content'), {
autofocus: true,
foldGutter: true,
gutters: [ 'CodeMirror-linenumbers', 'CodeMirror-foldgutter' ],
gutters: [
'CodeMirror-foldgutter',
'CodeMirror-linenumbers',
{ className: 'CodeMirror-lintgutter', style: 'width: 10px' },
],
lineNumbers: true,
lineWrapping: true,
matchBrackets: true,

View File

@ -27,6 +27,9 @@
'use strict';
import { dom } from '../dom.js';
import { i18n$ } from '../i18n.js';
{
const CodeMirror = self.CodeMirror;
@ -101,6 +104,10 @@
findNext(cm, -1);
} else if ( tcl.contains('cm-search-widget-down') ) {
findNext(cm, 1);
} else if ( tcl.contains('cm-linter-widget-up') ) {
findNextError(cm, -1);
} else if ( tcl.contains('cm-linter-widget-down') ) {
findNextError(cm, 1);
}
if ( ev.target.localName !== 'input' ) {
ev.preventDefault();
@ -137,6 +144,7 @@
this.queryTimer = null;
this.dirty = true;
this.lines = [];
this.errorLines = [];
cm.on('changes', (cm, changes) => {
for ( const change of changes ) {
if ( change.text.length !== 0 || change.removed !== 0 ) {
@ -365,6 +373,26 @@
});
};
const findNextError = function(cm, dir) {
const state = getSearchState(cm);
const lines = state.errorLines;
if ( lines.length === 0 ) { return; }
const cursor = cm.getCursor('from');
const start = cursor.line;
const next = lines.reduce((best, v) => {
if ( dir < 0 ) {
if ( v < start && (best === -1 || v > best) ) { return v; }
return best;
}
if ( v > start && (best === -1 || v < best) ) { return v; }
return best;
}, -1);
if ( next === -1 || next === start ) { return; }
cm.getDoc().setCursor(next);
const { clientHeight } = cm.getScrollInfo();
cm.scrollIntoView({ line: next, ch: 0 }, clientHeight >>> 1);
};
const clearSearch = function(cm, hard) {
cm.operation(function() {
const state = getSearchState(cm);
@ -444,6 +472,11 @@
'<span class="cm-search-widget-down cm-search-widget-button fa-icon fa-icon-vflipped">angle-up</span>&emsp;' +
'<span class="cm-search-widget-count"></span>' +
'</span>' +
'<span class="cm-linter-widget">' +
'<span class="cm-linter-widget-count"></span>&emsp;' +
'<span class="cm-linter-widget-up cm-search-widget-button fa-icon">angle-up</span>&nbsp;' +
'<span class="cm-linter-widget-down cm-search-widget-button fa-icon fa-icon-vflipped">angle-up</span>&emsp;' +
'</span>' +
'<a class="fa-icon sourceURL" href>external-link</a>' +
'</div>' +
'</div>';
@ -459,5 +492,14 @@
CodeMirror.defineInitHook(function(cm) {
getSearchState(cm);
cm.on('linterDone', details => {
const count = details.lines.length;
getSearchState(cm).errorLines = details.lines;
dom.cl.toggle('.cm-linter-widget', 'hasErrors', count !== 0);
dom.text(
'.cm-linter-widget .cm-linter-widget-count',
i18n$('linterMainReport').replace('{{count}}', count.toLocaleString())
);
});
});
}

View File

@ -39,7 +39,6 @@ let hintHelperRegistered = false;
/******************************************************************************/
CodeMirror.defineMode('ubo-static-filtering', function() {
if ( sfp.AstFilterParser instanceof Object === false ) { return; }
const astParser = new sfp.AstFilterParser({
interactive: true,
nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'),
@ -300,8 +299,6 @@ CodeMirror.defineMode('ubo-static-filtering', function() {
// https://codemirror.net/demo/complete.html
const initHints = function() {
if ( sfp.AstFilterParser instanceof Object === false ) { return; }
const astParser = new sfp.AstFilterParser({
interactive: true,
nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'),
@ -666,6 +663,146 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => {
/******************************************************************************/
// Linter
{
const astParser = new sfp.AstFilterParser({
interactive: true,
nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'),
});
const markedset = [];
let markedsetStart = 0;
let markedsetTimer;
const processMarkedsetAsync = doc => {
if ( markedsetTimer !== undefined ) { return; }
markedsetTimer = self.requestIdleCallback(deadline => {
markedsetTimer = undefined;
processMarkedset(doc, deadline);
});
};
const processMarkedset = (doc, deadline) => {
const lineCount = doc.lineCount();
doc.eachLine(markedsetStart, lineCount, lineHandle => {
const line = markedsetStart++;
const markers = lineHandle.gutterMarkers || null;
if ( markers && markers['CodeMirror-lintgutter'] ) {
markedset.push(lineHandle.lineNo());
}
if ( (line & 0x0F) === 0 && deadline.timeRemaining() === 0 ) {
processMarkedsetAsync(doc);
return true;
}
if ( markedsetStart === lineCount ) {
CodeMirror.signal(
doc.getEditor(),
'linterDone',
{ lines: markedset }
);
}
});
};
const changeset = [];
let changesetTimer;
const addChanges = (doc, change) => {
changeset.push(change);
processChangesetAsync(doc);
};
const processChangesetAsync = doc => {
if ( changesetTimer !== undefined ) { return; }
if ( markedsetTimer ) {
self.cancelIdleCallback(markedsetTimer);
markedsetTimer = undefined;
}
changesetTimer = self.requestIdleCallback(deadline => {
changesetTimer = undefined;
processChangeset(doc, deadline);
});
};
const extractError = ( ) => {
if ( astParser.isComment() ) { return; }
if ( astParser.isFilter() === false ) { return; }
if ( astParser.hasError() === false ) { return; }
let error = 'Invalid filter';
if ( astParser.isCosmeticFilter() && astParser.result.error ) {
error = `${error}: ${astParser.result.error}`;
}
return error;
};
const extractMarker = lineInfo => {
if ( lineInfo.gutterMarkers instanceof Object === false ) { return; }
return lineInfo.gutterMarkers['CodeMirror-lintgutter'];
};
const markerTemplate = (( ) => {
const marker = document.createElement('div');
marker.classList.add('CodeMirror-lintmarker');
marker.textContent = '\xA0';
const info = document.createElement('span');
marker.append(info);
return marker;
})();
const makeMarker = (doc, line, marker, error) => {
if ( marker === undefined ) {
marker = markerTemplate.cloneNode(true);
doc.setGutterMarker(line, 'CodeMirror-lintgutter', marker);
}
marker.children[0].textContent = error;
};
const processChangeset = (doc, deadline) => {
const cm = doc.getEditor();
cm.startOperation();
while ( changeset.length !== 0 ) {
const { from, to } = changeset.shift();
for ( let line = from; line < to; line++ ) {
const lineInfo = doc.lineInfo(line);
if ( lineInfo === null ) { continue; }
astParser.parse(lineInfo.text);
const error = extractError();
let marker = extractMarker(lineInfo);
if ( error === undefined && marker ) {
doc.setGutterMarker(line, 'CodeMirror-lintgutter', null);
} else if ( error !== undefined ) {
makeMarker(doc, line, marker, error);
}
if ( (line & 0x0F) === 0 && deadline.timeRemaining() === 0 ) {
changeset.unshift({ doc, from: line+1, to });
break;
}
}
}
cm.endOperation();
if ( changeset.length !== 0 ) {
return processChangesetAsync(doc);
}
markedset.length = 0;
markedsetStart = 0;
processMarkedsetAsync(doc);
};
CodeMirror.defineInitHook(cm => {
cm.on('changes', function(cm, changes) {
const doc = cm.getDoc();
for ( const change of changes ) {
const from = change.from.line;
const to = from + change.text.length;
addChanges(doc, { from, to });
}
});
});
}
/******************************************************************************/
// Enhanced word selection
{

View File

@ -705,7 +705,7 @@ export class AstFilterParser {
this.badTypes = new Set(options.badTypes || []);
this.maxTokenLength = options.maxTokenLength || 7;
// TODO: rethink this
this.result = { exception: false, raw: '', compiled: '' };
this.result = { exception: false, raw: '', compiled: '', error: undefined };
this.selectorCompiler = new ExtSelectorCompiler(options);
// Regexes
this.reWhitespaceStart = /^\s+/;
@ -2967,6 +2967,7 @@ class ExtSelectorCompiler {
this.nativeCssHas = instanceOptions.nativeCssHas === true;
// https://www.w3.org/TR/css-syntax-3/#typedef-ident-token
this.reInvalidIdentifier = /^\d/;
this.error = undefined;
}
compile(raw, out, compileOptions = {}) {
@ -3016,7 +3017,10 @@ class ExtSelectorCompiler {
}
out.compiled = this.compileSelector(raw);
if ( out.compiled === undefined ) { return false; }
if ( out.compiled === undefined ) {
out.error = this.error || undefined;
return false;
}
if ( out.compiled instanceof Object ) {
out.compiled.raw = raw;
@ -3060,6 +3064,7 @@ class ExtSelectorCompiler {
parseValue: false,
});
} catch(reason) {
this.error = reason && reason.message || undefined;
return;
}
const parts = [];