Skip to content

Commit

Permalink
Add infrastructure for static filter syntax linter
Browse files Browse the repository at this point in the history
Sort of related issue:
- uBlockOrigin/uBlock-issues#1134
  • Loading branch information
gorhill committed Apr 1, 2023
1 parent b10f15d commit 50afd5a
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 13 deletions.
4 changes: 2 additions & 2 deletions src/1p-filters.html
Expand Up @@ -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>
Expand Down
4 changes: 4 additions & 0 deletions src/_locales/en/messages.json
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions src/asset-viewer.html
Expand Up @@ -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>
Expand Down
4 changes: 2 additions & 2 deletions src/code-viewer.html
Expand Up @@ -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>
Expand Down
44 changes: 44 additions & 0 deletions src/css/codemirror.css
Expand Up @@ -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;
Expand Down Expand Up @@ -196,8 +203,21 @@
color: #000;
}
.cm-search-widget .sourceURL[href=""] {
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;
Expand Down Expand Up @@ -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;
}
6 changes: 5 additions & 1 deletion src/js/1p-filters.js
Expand Up @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion src/js/asset-viewer.js
Expand Up @@ -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,
Expand Down
42 changes: 42 additions & 0 deletions src/js/codemirror/search.js
Expand Up @@ -27,6 +27,9 @@

'use strict';

import { dom } from '../dom.js';
import { i18n$ } from '../i18n.js';

{
const CodeMirror = self.CodeMirror;

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 ) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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>';
Expand All @@ -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())
);
});
});
}
143 changes: 140 additions & 3 deletions src/js/codemirror/ubo-static-filtering.js
Expand Up @@ -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'),
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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

{
Expand Down

1 comment on commit 50afd5a

@gwarser
Copy link
Contributor

@gwarser gwarser commented on 50afd5a Apr 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cloud widget does not appear anymore on "My filters" tab, and this is the commit bisection shows as being responsible.


Fixed in 3b14fd9

Please sign in to comment.