Skip to content

Commit

Permalink
Rewrite linter system (openstyles#487)
Browse files Browse the repository at this point in the history
* Add: implement new linter system

* Refactor: pull out editor worker

* Switch to new linter and worker

* Enable eslint cache

* Fix: undefined error

* Windows compatibility

* Fix: refresh linter if the editor.linter changes

* Add: stylelint

* Add: getStylelintRules, getCsslintRules

* Fix: logic to get correct linter

* WIP: linter-report

* Fix: toggle hidden state

* Add: matain the order of lint report for section editor

* Add: unhook event

* Add: gotoLintIssue

* Fix: shouldn't delete rule.init

* Add: linter-help-dialog

* Drop linterConfig

* Add: linter-config-dialog, cacheFn

* Add: use cacheFn

* Drop lint.js

* Add: refresh. Fix report order

* Fix: hide empty table

* Add: updateCount. Fix table caption

* Switch to new linter/worker

* Fix: remove unneeded comment

* Fix: cacheFn -> cacheFirstCall

* Fix: use cacheFirstCall

* Fix: cache metaIndex

* Fix: i < trs.length

* Fix: drop isEmpty

* Fix: expose some simple states to global

* Fix: return object code style

* Fix: use proxy to reflect API

* Fix: eslint-disable-line -> eslint-disable-next-line

* Fix: requestId -> id

* Fix: one-liner

* Fix: one-liner

* Fix: move dom event block to top

* Fix: pending -> pendingResponse

* Fix: onSuccess -> onUpdated

* Fix: optimize row removing when i === 0

* Fix: hook/unhook -> enableForEditor/disableForEditor

* Fix: linter.refresh -> linter.run

* Fix: some shadowing

* Fix: simplify getAnnotations

* Fix: cacheFirstCall -> memoize

* Fix: table.update -> table.updateCaption

* Fix: unneeded reassign

* Fix: callbacks -> listeners

* Fix: don't compose but extend

* Refactor: replace linter modules with linter-defaults and linter-engines

* Fix: implement linter fallbacks

* Fix: linter.onChange -> linter.onLintingUpdated

* Fix: cms -> tables

* Fix: parseMozFormat is not called correctly

* Move csslint-loader to background

* Fix: watch config changes

* Fix: switch to LINTER_DEFAULTS

* Fix: csslint-loader -> parserlib-loader
  • Loading branch information
eight04 authored and Mottie committed Oct 1, 2018
1 parent 5f60c51 commit 2fd531e
Show file tree
Hide file tree
Showing 27 changed files with 1,090 additions and 968 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -5,3 +5,4 @@ node_modules/
package-lock.json
yarn.lock
*.zip
.eslintcache
9 changes: 9 additions & 0 deletions background/parserlib-loader.js
@@ -0,0 +1,9 @@
/* global importScripts parserlib CSSLint parseMozFormat */
'use strict';

importScripts('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');
parserlib.css.Tokens[parserlib.css.Tokens.COMMENT].hide = false;

self.onmessage = ({data}) => {
self.postMessage(parseMozFormat(data));
};
15 changes: 13 additions & 2 deletions edit.html
Expand Up @@ -25,7 +25,6 @@
<script src="js/script-loader.js"></script>
<script src="js/storage-util.js"></script>
<script src="content/apply.js"></script>
<script src="edit/lint.js"></script>
<script src="edit/util.js"></script>
<script src="edit/regexp-tester.js"></script>
<script src="edit/applies-to-line-widget.js"></script>
Expand Down Expand Up @@ -65,6 +64,8 @@
<script src="vendor/codemirror/addon/fold/comment-fold.js"></script>

<link href="vendor/codemirror/addon/lint/lint.css" rel="stylesheet" />
<script src="vendor/codemirror/addon/lint/lint.js"></script>


<link href="vendor/codemirror/addon/hint/show-hint.css" rel="stylesheet" />
<script src="vendor/codemirror/addon/hint/show-hint.js"></script>
Expand All @@ -87,6 +88,16 @@
<link href="edit/codemirror-default.css" rel="stylesheet">
<script src="edit/codemirror-default.js"></script>

<script src="edit/linter.js"></script>
<script src="edit/linter-defaults.js"></script>
<script src="edit/linter-engines.js"></script>
<script src="edit/linter-meta.js"></script>
<script src="edit/linter-help-dialog.js"></script>
<script src="edit/linter-report.js"></script>
<script src="edit/linter-config-dialog.js"></script>

<script src="edit/editor-worker.js"></script>

<link id="cm-theme" rel="stylesheet">

<template data-id="appliesTo">
Expand Down Expand Up @@ -417,7 +428,7 @@ <h2 i18n-text="linterIssues">: <span id="issue-count"></span>
</a>
</h2>
</summary>
<div></div>
<div class="lint-report-container"></div>
</details>
<div id="footer" class="hidden">
<a href="https://github.com/openstyles/stylus/wiki/Usercss"
Expand Down
5 changes: 1 addition & 4 deletions edit/codemirror-editing-hooks.js
@@ -1,5 +1,5 @@
/*
global CodeMirror linterConfig loadScript
global CodeMirror loadScript
global editors editor styleId ownTabId
global save toggleStyle setupAutocomplete makeSectionVisible getSectionForChild
global getSectionsHashes
Expand All @@ -8,9 +8,6 @@ global messageBox
'use strict';

onDOMscriptReady('/codemirror.js').then(() => {

CodeMirror.defaults.lint = linterConfig.getForCodeMirror();

const COMMANDS = {
save,
toggleStyle,
Expand Down
41 changes: 0 additions & 41 deletions edit/csslint-loader.js

This file was deleted.

3 changes: 3 additions & 0 deletions edit/edit.css
Expand Up @@ -651,6 +651,9 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
#lint table:last-child {
margin-bottom: 0;
}
#lint table.empty {
display: none;
}
#lint caption {
text-align: left;
font-weight: bold;
Expand Down
31 changes: 4 additions & 27 deletions edit/edit.js
@@ -1,13 +1,12 @@
/*
global CodeMirror parserlib loadScript
global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter
global CodeMirror loadScript
global createSourceEditor
global closeCurrentTab regExpTester messageBox
global setupCodeMirror
global beautify
global initWithSectionStyle addSections removeSection getSectionsHashes
global sectionsToMozFormat
global moveFocus
global moveFocus editorWorker
*/
'use strict';

Expand Down Expand Up @@ -212,7 +211,6 @@ function beforeUnload() {
}
const isDirty = editor ? editor.isDirty() : !isCleanGlobal();
if (isDirty) {
updateLintReportIfEnabled(null, 0);
// neither confirm() nor custom messages work in modern browsers but just in case
return t('styleChangesNotSaved');
}
Expand Down Expand Up @@ -276,9 +274,6 @@ function initHooks() {
$('#save-button').addEventListener('click', save, false);
$('#sections-help').addEventListener('click', showSectionHelp, false);

// TODO: investigate why FF needs this delay
debounce(initLint, FIREFOX ? 100 : 0);

if (!FIREFOX) {
$$([
'input:not([type])',
Expand Down Expand Up @@ -353,7 +348,6 @@ function toggleStyle() {
}

function save() {
updateLintReportIfEnabled(null, 0);
if (!validate()) {
return;
}
Expand Down Expand Up @@ -414,12 +408,6 @@ function updateTitle() {
$('#save-button').disabled = clean;
}

function updateLintReportIfEnabled(...args) {
if (CodeMirror.defaults.lint) {
updateLintReport(...args);
}
}

function showMozillaFormat() {
const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
popup.codebox.setValue(toMozillaFormat());
Expand Down Expand Up @@ -461,16 +449,7 @@ function fromMozillaFormat() {

function doImport({replaceOldStyle = false}) {
lockPageUI(true);
new Promise(setTimeout)
.then(() => {
const worker = linterConfig.worker.csslint;
if (!worker.instance) worker.instance = new Worker(worker.path);
})
.then(() => linterConfig.invokeWorker({
linter: 'csslint',
action: 'parse',
code: popup.codebox.getValue().trim(),
}))
editorWorker.parseMozFormat({code: popup.codebox.getValue().trim()})
.then(({sections, errors}) => {
// shouldn't happen but just in case
if (!sections.length && errors.length) {
Expand All @@ -483,8 +462,7 @@ function fromMozillaFormat() {
removeOldSections(replaceOldStyle);
return addSections(sections, div => setCleanItem(div, false));
})
.then(sectionDivs => {
sectionDivs.forEach(div => updateLintReportIfEnabled(div.CodeMirror, 1));
.then(() => {
$('.dismiss').dispatchEvent(new Event('click'));
})
.catch(showError)
Expand Down Expand Up @@ -604,7 +582,6 @@ function showCodeMirrorPopup(title, html, options) {
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
matchBrackets: true,
lint: linterConfig.getForCodeMirror(),
styleActiveLine: true,
theme: prefs.get('editor.theme'),
keyMap: prefs.get('editor.keyMap')
Expand Down
118 changes: 118 additions & 0 deletions edit/editor-worker-body.js
@@ -0,0 +1,118 @@
/* global importScripts parseMozFormat parserlib CSSLint require */
'use strict';

createAPI({
csslint: (code, config) => {
loadParserLib();
loadScript(['/vendor-overwrites/csslint/csslint.js']);
return CSSLint.verify(code, config).messages
.map(m => Object.assign(m, {rule: {id: m.rule.id}}));
},
stylelint: (code, config) => {
loadScript(['/vendor/stylelint-bundle/stylelint-bundle.min.js']);
return require('stylelint').lint({code, config});
},
parseMozFormat: data => {
loadParserLib();
loadScript(['/js/moz-parser.js']);
return parseMozFormat(data);
},
getStylelintRules,
getCsslintRules
});

function getCsslintRules() {
loadScript(['/vendor-overwrites/csslint/csslint.js']);
return CSSLint.getRules().map(rule => {
const output = {};
for (const [key, value] of Object.entries(rule)) {
if (typeof value !== 'function') {
output[key] = value;
}
}
return output;
});
}

function getStylelintRules() {
loadScript(['/vendor/stylelint-bundle/stylelint-bundle.min.js']);
const stylelint = require('stylelint');
const options = {};
const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
const rxString = /"([-\w\s]{3,}?)"/g;
for (const id of Object.keys(stylelint.rules)) {
const ruleCode = String(stylelint.rules[id]);
const sets = [];
let m, mStr;
while ((m = rxPossible.exec(ruleCode))) {
const possible = m[1];
const set = [];
while ((mStr = rxString.exec(possible))) {
const s = mStr[1];
if (s.includes(' ')) {
set.push(...s.split(/\s+/));
} else {
set.push(s);
}
}
if (possible.includes('ignoreAtRules')) {
set.push('ignoreAtRules');
}
if (possible.includes('ignoreShorthands')) {
set.push('ignoreShorthands');
}
if (set.length) {
sets.push(set);
}
}
if (sets.length) {
options[id] = sets;
}
}
return options;
}

function loadParserLib() {
if (typeof parserlib !== 'undefined') {
return;
}
importScripts('/vendor-overwrites/csslint/parserlib.js');
parserlib.css.Tokens[parserlib.css.Tokens.COMMENT].hide = false;
}

const loadedUrls = new Set();
function loadScript(urls) {
urls = urls.filter(u => !loadedUrls.has(u));
importScripts(...urls);
urls.forEach(u => loadedUrls.add(u));
}

function createAPI(methods) {
self.onmessage = e => {
const message = e.data;
Promise.resolve()
.then(() => methods[message.action](...message.args))
.then(result => ({
id: message.id,
error: false,
data: result
}))
.catch(err => ({
id: message.id,
error: true,
data: cloneError(err)
}))
.then(data => self.postMessage(data));
};
}

function cloneError(err) {
return Object.assign({
name: err.name,
stack: err.stack,
message: err.message,
lineNumber: err.lineNumber,
columnNumber: err.columnNumber,
fileName: err.fileName
}, err);
}
39 changes: 39 additions & 0 deletions edit/editor-worker.js
@@ -0,0 +1,39 @@
'use strict';

// eslint-disable-next-line no-var
var editorWorker = (() => {
let worker;
return new Proxy({}, {
get: (target, prop) =>
(...args) => {
if (!worker) {
worker = createWorker();
}
return worker.invoke(prop, args);
}
});

function createWorker() {
let id = 0;
const pendingResponse = new Map();
const worker = new Worker('/edit/editor-worker-body.js');
worker.onmessage = e => {
const message = e.data;
pendingResponse.get(message.id)[message.error ? 'reject' : 'resolve'](message.data);
pendingResponse.delete(message.id);
};
return {invoke};

function invoke(action, args) {
return new Promise((resolve, reject) => {
pendingResponse.set(id, {resolve, reject});
worker.postMessage({
id,
action,
args
});
id++;
});
}
}
})();

0 comments on commit 2fd531e

Please sign in to comment.