mirror of
https://github.com/gorhill/uBlock.git
synced 2024-10-06 09:37:12 +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:
parent
b10f15dd89
commit
50afd5ae38
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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> ' +
|
||||
'<span class="cm-search-widget-count"></span>' +
|
||||
'</span>' +
|
||||
'<span class="cm-linter-widget">' +
|
||||
'<span class="cm-linter-widget-count"></span> ' +
|
||||
'<span class="cm-linter-widget-up cm-search-widget-button fa-icon">angle-up</span> ' +
|
||||
'<span class="cm-linter-widget-down cm-search-widget-button fa-icon fa-icon-vflipped">angle-up</span> ' +
|
||||
'</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())
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
{
|
||||
|
@ -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 = [];
|
||||
|
Loading…
Reference in New Issue
Block a user