Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add preprocess feature #62

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ This setting can be given the result of `require('.../path/to/svelte/compiler')`

The default is `require('svelte/compiler')` from wherever the plugin is installed to.

### `svelte3/preprocess`

You can use a preprocessor function to return custom AST info according to the original code.

For now this only supports `module` and `instance` scripts.

NOTE: The preprocess function MUST be a synchronous function because ESLint works doesn't work with async functions. See issue [#10 (comment)](https://github.com/sveltejs/eslint-plugin-svelte3/issues/10#issuecomment-490634346)

## Using the CLI

It's probably a good idea to make sure you can lint from the command line before proceeding with configuring your editor.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
"test": "npm run build && node test"
},
"devDependencies": {
"eslint": ">=6.0.0",
"eslint": "^7.7.0",
"rollup": "^2",
"svelte": "^3.2.0"
"svelte": "^3.24.1"
}
}
9 changes: 9 additions & 0 deletions src/block.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { get_offsets, dedent_code } from './utils.js';

// find the index of the last element of an array matching a condition
export const find_last_index = (array, cond) => {
const idx = array.findIndex(item => !cond(item));
return idx === -1 ? array.length - 1 : idx - 1;
};

// find the last element of an array matching a condition
export const find_last = (array, cond) => array[find_last_index(array, cond)];

// return a new block
export const new_block = () => ({ transformed_code: '', line_offsets: null, translations: new Map() });

Expand Down
29 changes: 29 additions & 0 deletions src/postprocess.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
import { state, reset } from './state.js';
import { get_line_offsets } from './utils.js';
import { find_last_index, find_last } from './block.js'

export const unmap = message => {
for (let j = 0; j < 2; j++) {
if (message[j ? 'endLine' : 'line']) {
const mapping = find_last(state.mappings[message[j ? 'endLine' : 'line'] - 1], ([column]) => column < message[j ? 'endColumn' : 'column']);
if (!mapping || mapping[1] !== 0) {
return false;
}
message[j ? 'endLine' : 'line'] = mapping[2] + 1;
message[j ? 'endColumn' : 'column'] += mapping[3] - mapping[0];
}
}
if (message.fix) {
for (let j = 0; j < 2; j++) {
const line = find_last_index(state.post_line_offsets, offset => offset < message.fix.range[j]);
const line_offset = state.post_line_offsets[line];
const mapping = find_last(state.mappings[line], ([column]) => column < message.fix.range[j] - line_offset);
if (!mapping || mapping[1] !== 0) {
return false;
}
message.fix.range[j] += mapping[3] - mapping[0] + state.pre_line_offsets[mapping[2]] - line_offset;
}
}
return true;
};

// transform a linting message according to the module/instance script info we've gathered
const transform_message = ({ transformed_code }, { unoffsets, dedent, offsets, range }, message) => {
Expand Down Expand Up @@ -118,6 +144,9 @@ export const postprocess = blocks_messages => {
}
}
}
if (state.mappings) {
state.messages = state.messages.filter(unmap);
}

// sort messages and return
const sorted_messages = state.messages.sort((a, b) => a.line - b.line || a.column - b.column);
Expand Down
233 changes: 216 additions & 17 deletions src/preprocess.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,89 @@
import { new_block, get_translation } from './block.js';
import { processor_options } from './processor_options.js';
import { get_line_offsets } from './utils.js';
import { state } from './state.js';

var charToInteger = {};
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
for (var i = 0; i < chars.length; i++) {
charToInteger[chars.charCodeAt(i)] = i;
}
function decode(mappings) {
var generatedCodeColumn = 0; // first field
var sourceFileIndex = 0; // second field
var sourceCodeLine = 0; // third field
var sourceCodeColumn = 0; // fourth field
var nameIndex = 0; // fifth field
var decoded = [];
var line = [];
var segment = [];
for (var i = 0, j = 0, shift = 0, value = 0, len = mappings.length; i < len; i++) {
var c = mappings.charCodeAt(i);
if (c === 44) { // ","
if (segment.length)
line.push(segment);
segment = [];
j = 0;
}
else if (c === 59) { // ";"
if (segment.length)
line.push(segment);
segment = [];
j = 0;
decoded.push(line);
line = [];
generatedCodeColumn = 0;
}
else {
var integer = charToInteger[c];
if (integer === undefined) {
throw new Error('Invalid character (' + String.fromCharCode(c) + ')');
}
var hasContinuationBit = integer & 32;
integer &= 31;
value += integer << shift;
if (hasContinuationBit) {
shift += 5;
}
else {
var shouldNegate = value & 1;
value >>>= 1;
if (shouldNegate) {
value = -value;
if (value === 0)
value = -0x80000000;
}
if (j == 0) {
generatedCodeColumn += value;
segment.push(generatedCodeColumn);
}
else if (j === 1) {
sourceFileIndex += value;
segment.push(sourceFileIndex);
}
else if (j === 2) {
sourceCodeLine += value;
segment.push(sourceCodeLine);
}
else if (j === 3) {
sourceCodeColumn += value;
segment.push(sourceCodeColumn);
}
else if (j === 4) {
nameIndex += value;
segment.push(nameIndex);
}
j++;
value = shift = 0; // reset
}
}
}
if (segment.length)
line.push(segment);
decoded.push(line);
return decoded;
}

let default_compiler;

// find the contextual name or names described by a particular node in the AST
Expand All @@ -23,7 +105,7 @@ const find_contextual_names = (compiler, node) => {
};

// extract scripts to lint from component definition
export const preprocess = text => {
export const preprocess = (text, filename) => {
const compiler = processor_options.custom_compiler || default_compiler || (default_compiler = require('svelte/compiler'));
if (processor_options.ignore_styles) {
// wipe the appropriate <style> tags in the file
Expand All @@ -40,10 +122,112 @@ export const preprocess = text => {
return processor_options.ignore_styles(attrs) ? match.replace(/\S/g, ' ') : match;
});
}
// get information about the component
let result;
let processedResult;
let processedModule;
let processedInstance;
let processedStyle;
let processedMarkup;
let moduleExt = 'js';
let instanceExt = 'js';
let moduleEndLine;
let processedModuleLineOffset;
let instanceEndLine;
let processedInstanceLineOffset;
try {
result = compiler.compile(text, { generate: false, ...processor_options.compiler_options });
// run preprocessor if present
if (processor_options.svelte_preprocess) {
const result = processor_options.svelte_preprocess(text, filename);
if (result) {
state.pre_line_offsets = get_line_offsets(text);
processedResult = result.code;
state.post_line_offsets = get_line_offsets(processedResult);
if (result.mappings) {
state.mappings = decode(result.mappings);
}

if (result.module) {
processedModule = result.module;
moduleExt = result.module.ext;
}
if (result.instance) {
processedInstance = result.instance;
instanceExt = result.instance.ext;
}

processedStyle = result.style;

processedMarkup = result.markup;

processor_options.named_blocks = true;
}
}
// get information about the component
result = compiler.compile(processedResult || text, { generate: false, ...processor_options.compiler_options });
if (processedResult) {
const { html, css, instance, module } = result.ast;

let moduleDiff = processedModule ? processedModule.diff : 0;
let instanceDiff = processedInstance ? processedInstance.diff : 0;
let styleDiff = processedStyle ? processedStyle.diff : 0;
let markupDiff = processedMarkup ? processedMarkup.diff : 0;

let modulePreOffset = 0;
let modulePostOffset = 0;
if (module) {
if (module.start > html.start) {
modulePreOffset += markupDiff;
}
if (css && module.start > css.start) {
modulePreOffset += styleDiff;
}
if (instance && module.start > instance.start) {
modulePreOffset += instanceDiff;
}

modulePostOffset = modulePreOffset + moduleDiff;
}

let instancePreOffset = 0;
let instancePostOffset = 0;
if (instance) {
if (instance.start > html.start) {
instancePreOffset += markupDiff;
}
if (css && instance.start > css.start) {
instancePreOffset += styleDiff;
}
if (module && instance.start > module.start) {
instancePreOffset += moduleDiff;
}

instancePostOffset = instancePreOffset + instanceDiff;
}

if (module && processedModule) {
moduleEndLine = module.content.loc.end.line;
processedModuleLineOffset = processedModule.ast.loc.end.line - moduleEndLine;
module.content.body = processedModule.ast.body;

module.start += modulePreOffset;
module.end += modulePostOffset;

module.content.start += modulePreOffset;
module.content.end += modulePostOffset;
}

if (instance && processedInstance) {
instanceEndLine = instance.content.loc.end.line;
processedInstanceLineOffset = processedInstance.ast.loc.end.line - instanceEndLine;
instance.content.body = processedInstance.ast.body;

instance.start += instancePreOffset;
instance.end += instancePostOffset;

instance.content.start += instancePreOffset;
instance.content.end += instancePostOffset;
}
}
} catch ({ name, message, start, end }) {
// convert the error to a linting message, store it, and return
state.messages = [
Expand All @@ -64,27 +248,40 @@ export const preprocess = text => {
state.var_names = new Set(vars.map(v => v.name));

// convert warnings to linting messages
state.messages = (processor_options.ignore_warnings ? warnings.filter(warning => !processor_options.ignore_warnings(warning)) : warnings).map(({ code, message, start, end }) => ({
ruleId: code,
severity: 1,
message,
line: start && start.line,
column: start && start.column + 1,
endLine: end && end.line,
endColumn: end && end.column + 1,
}));
state.messages = (processor_options.ignore_warnings ? warnings.filter(warning => !processor_options.ignore_warnings(warning)) : warnings).map(({ code, message, start, end }) => {
let fixLine = 0;

if (processedInstanceLineOffset && start && start.line > instanceEndLine ) {
fixLine += processedInstanceLineOffset;
}

if (processedModuleLineOffset && start && start.line > moduleEndLine ) {
fixLine += processedModuleLineOffset;
}
return {
ruleId: code,
severity: 1,
message,
line: start && start.line + fixLine,
column: start && start.column + 1,
endLine: end && end.line + fixLine,
endColumn: end && end.column + 1,
}
});

// build strings that we can send along to ESLint to get the remaining messages

if (ast.module) {
// block for <script context='module'>
const block = new_block();
state.blocks.set('module.js', block);
state.blocks.set(`module.${moduleExt}`, block);

get_translation(text, block, ast.module.content);

if (ast.instance) {
block.transformed_code += text.slice(ast.instance.content.start, ast.instance.content.end);
block.transformed_code += processedResult
? processedInstance.original
: text.slice(ast.instance.content.start, ast.instance.content.end);
}

block.transformed_code += references_and_reassignments;
Expand All @@ -93,7 +290,7 @@ export const preprocess = text => {
if (ast.instance) {
// block for <script context='instance'>
const block = new_block();
state.blocks.set('instance.js', block);
state.blocks.set(`instance.${instanceExt}`, block);

block.transformed_code = vars.filter(v => v.injected || v.module).map(v => `let ${v.name};`).join('');

Expand All @@ -111,11 +308,13 @@ export const preprocess = text => {

const nodes_with_contextual_scope = new WeakSet();
let in_quoted_attribute = false;
const htmlText = processedResult || text;

compiler.walk(ast.html, {
enter(node, parent, prop) {
if (prop === 'expression') {
return this.skip();
} else if (prop === 'attributes' && '\'"'.includes(text[node.end - 1])) {
} else if (prop === 'attributes' && '\'"'.includes(htmlText[node.end - 1])) {
in_quoted_attribute = true;
}
contextual_names.length = 0;
Expand All @@ -136,7 +335,7 @@ export const preprocess = text => {
if (node.expression && typeof node.expression === 'object') {
// add the expression in question to the constructed string
block.transformed_code += '(';
get_translation(text, block, node.expression, { template: true, in_quoted_attribute });
get_translation(htmlText, block, node.expression, { template: true, in_quoted_attribute });
block.transformed_code += ');';
}
},
Expand Down
1 change: 1 addition & 0 deletions src/processor_options.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Linter.prototype.verify = function(code, config, options) {
processor_options.ignore_styles = settings['svelte3/ignore-styles'];
processor_options.compiler_options = settings['svelte3/compiler-options'];
processor_options.named_blocks = settings['svelte3/named-blocks'];
processor_options.svelte_preprocess = settings['svelte3/preprocess'];
// call original Linter#verify
return verify.call(this, code, config, options);
};
3 changes: 3 additions & 0 deletions src/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export const reset = () => {
messages: null,
var_names: null,
blocks: new Map(),
pre_line_offsets: null,
post_line_offsets: null,
mappings: null,
};
};
reset();