2020-03-07 11:45:40 +08:00
|
|
|
/**
|
|
|
|
* @fileoverview Runs `prettier` as an ESLint rule.
|
|
|
|
* @author Andres Suarez
|
|
|
|
*/
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------------
|
|
|
|
// Requirements
|
|
|
|
// ------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
const {
|
|
|
|
showInvisibles,
|
|
|
|
generateDifferences
|
|
|
|
} = require('prettier-linter-helpers');
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------------
|
|
|
|
// Constants
|
|
|
|
// ------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
const { INSERT, DELETE, REPLACE } = generateDifferences;
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------------
|
|
|
|
// Privates
|
|
|
|
// ------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
// Lazily-loaded Prettier.
|
|
|
|
let prettier;
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------------
|
|
|
|
// Rule Definition
|
|
|
|
// ------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reports an "Insert ..." issue where text must be inserted.
|
|
|
|
* @param {RuleContext} context - The ESLint rule context.
|
|
|
|
* @param {number} offset - The source offset where to insert text.
|
|
|
|
* @param {string} text - The text to be inserted.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
function reportInsert(context, offset, text) {
|
|
|
|
const pos = context.getSourceCode().getLocFromIndex(offset);
|
|
|
|
const range = [offset, offset];
|
|
|
|
context.report({
|
|
|
|
message: 'Insert `{{ code }}`',
|
|
|
|
data: { code: showInvisibles(text) },
|
|
|
|
loc: { start: pos, end: pos },
|
|
|
|
fix(fixer) {
|
|
|
|
return fixer.insertTextAfterRange(range, text);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reports a "Delete ..." issue where text must be deleted.
|
|
|
|
* @param {RuleContext} context - The ESLint rule context.
|
|
|
|
* @param {number} offset - The source offset where to delete text.
|
|
|
|
* @param {string} text - The text to be deleted.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
function reportDelete(context, offset, text) {
|
|
|
|
const start = context.getSourceCode().getLocFromIndex(offset);
|
|
|
|
const end = context.getSourceCode().getLocFromIndex(offset + text.length);
|
|
|
|
const range = [offset, offset + text.length];
|
|
|
|
context.report({
|
|
|
|
message: 'Delete `{{ code }}`',
|
|
|
|
data: { code: showInvisibles(text) },
|
|
|
|
loc: { start, end },
|
|
|
|
fix(fixer) {
|
|
|
|
return fixer.removeRange(range);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reports a "Replace ... with ..." issue where text must be replaced.
|
|
|
|
* @param {RuleContext} context - The ESLint rule context.
|
|
|
|
* @param {number} offset - The source offset where to replace deleted text
|
|
|
|
with inserted text.
|
|
|
|
* @param {string} deleteText - The text to be deleted.
|
|
|
|
* @param {string} insertText - The text to be inserted.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
function reportReplace(context, offset, deleteText, insertText) {
|
|
|
|
const start = context.getSourceCode().getLocFromIndex(offset);
|
|
|
|
const end = context
|
|
|
|
.getSourceCode()
|
|
|
|
.getLocFromIndex(offset + deleteText.length);
|
|
|
|
const range = [offset, offset + deleteText.length];
|
|
|
|
context.report({
|
|
|
|
message: 'Replace `{{ deleteCode }}` with `{{ insertCode }}`',
|
|
|
|
data: {
|
|
|
|
deleteCode: showInvisibles(deleteText),
|
|
|
|
insertCode: showInvisibles(insertText)
|
|
|
|
},
|
|
|
|
loc: { start, end },
|
|
|
|
fix(fixer) {
|
|
|
|
return fixer.replaceTextRange(range, insertText);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------------
|
|
|
|
// Module Definition
|
|
|
|
// ------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
configs: {
|
|
|
|
recommended: {
|
|
|
|
extends: ['prettier'],
|
|
|
|
plugins: ['prettier'],
|
|
|
|
rules: {
|
|
|
|
'prettier/prettier': 'error'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
rules: {
|
|
|
|
prettier: {
|
|
|
|
meta: {
|
|
|
|
docs: {
|
|
|
|
url: 'https://github.com/prettier/eslint-plugin-prettier#options'
|
|
|
|
},
|
2020-04-30 20:40:07 +08:00
|
|
|
type: 'layout',
|
2020-03-07 11:45:40 +08:00
|
|
|
fixable: 'code',
|
|
|
|
schema: [
|
|
|
|
// Prettier options:
|
|
|
|
{
|
|
|
|
type: 'object',
|
|
|
|
properties: {},
|
|
|
|
additionalProperties: true
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: 'object',
|
|
|
|
properties: {
|
|
|
|
usePrettierrc: { type: 'boolean' },
|
|
|
|
fileInfoOptions: {
|
|
|
|
type: 'object',
|
|
|
|
properties: {},
|
|
|
|
additionalProperties: true
|
|
|
|
}
|
|
|
|
},
|
|
|
|
additionalProperties: true
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
create(context) {
|
|
|
|
const usePrettierrc =
|
|
|
|
!context.options[1] || context.options[1].usePrettierrc !== false;
|
|
|
|
const eslintFileInfoOptions =
|
|
|
|
(context.options[1] && context.options[1].fileInfoOptions) || {};
|
|
|
|
const sourceCode = context.getSourceCode();
|
|
|
|
const filepath = context.getFilename();
|
|
|
|
const source = sourceCode.text;
|
|
|
|
|
2020-06-27 02:01:06 +08:00
|
|
|
// This allows long-running ESLint processes (e.g. vscode-eslint) to
|
|
|
|
// pick up changes to .prettierrc without restarting the editor. This
|
|
|
|
// will invalidate the prettier plugin cache on every file as well which
|
|
|
|
// will make ESLint very slow, so it would probably be a good idea to
|
|
|
|
// find a better way to do this.
|
|
|
|
if (usePrettierrc && prettier && prettier.clearConfigCache) {
|
2020-03-07 11:45:40 +08:00
|
|
|
prettier.clearConfigCache();
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
Program() {
|
|
|
|
if (!prettier) {
|
|
|
|
// Prettier is expensive to load, so only load it if needed.
|
|
|
|
prettier = require('prettier');
|
|
|
|
}
|
|
|
|
|
|
|
|
const eslintPrettierOptions = context.options[0] || {};
|
|
|
|
|
|
|
|
const prettierRcOptions = usePrettierrc
|
|
|
|
? prettier.resolveConfig.sync(filepath, {
|
|
|
|
editorconfig: true
|
|
|
|
})
|
|
|
|
: null;
|
|
|
|
|
|
|
|
const prettierFileInfo = prettier.getFileInfo.sync(
|
|
|
|
filepath,
|
|
|
|
Object.assign(
|
|
|
|
{},
|
|
|
|
{ resolveConfig: true, ignorePath: '.prettierignore' },
|
|
|
|
eslintFileInfoOptions
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
// Skip if file is ignored using a .prettierignore file
|
|
|
|
if (prettierFileInfo.ignored) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const initialOptions = {};
|
|
|
|
|
|
|
|
// ESLint suppports processors that let you extract and lint JS
|
|
|
|
// fragments within a non-JS language. In the cases where prettier
|
|
|
|
// supports the same language as a processor, we want to process
|
|
|
|
// the provided source code as javascript (as ESLint provides the
|
|
|
|
// rules with fragments of JS) instead of guessing the parser
|
|
|
|
// based off the filename. Otherwise, for instance, on a .md file we
|
|
|
|
// end up trying to run prettier over a fragment of JS using the
|
|
|
|
// markdown parser, which throws an error.
|
|
|
|
// If we can't infer the parser from from the filename, either
|
|
|
|
// because no filename was provided or because there is no parser
|
|
|
|
// found for the filename, use javascript.
|
|
|
|
// This is added to the options first, so that
|
|
|
|
// prettierRcOptions and eslintPrettierOptions can still override
|
|
|
|
// the parser.
|
|
|
|
//
|
|
|
|
// `parserBlocklist` should contain the list of prettier parser
|
|
|
|
// names for file types where:
|
|
|
|
// * Prettier supports parsing the file type
|
|
|
|
// * There is an ESLint processor that extracts JavaScript snippets
|
|
|
|
// from the file type.
|
|
|
|
const parserBlocklist = [null, 'graphql', 'markdown', 'html'];
|
|
|
|
if (
|
|
|
|
parserBlocklist.indexOf(prettierFileInfo.inferredParser) !== -1
|
|
|
|
) {
|
|
|
|
// Prettier v1.16.0 renamed the `babylon` parser to `babel`
|
|
|
|
// Use the modern name if available
|
|
|
|
const supportBabelParser = prettier
|
|
|
|
.getSupportInfo()
|
|
|
|
.languages.some(language => language.parsers.includes('babel'));
|
|
|
|
|
|
|
|
initialOptions.parser = supportBabelParser ? 'babel' : 'babylon';
|
|
|
|
}
|
|
|
|
|
|
|
|
const prettierOptions = Object.assign(
|
|
|
|
{},
|
|
|
|
initialOptions,
|
|
|
|
prettierRcOptions,
|
|
|
|
eslintPrettierOptions,
|
|
|
|
{ filepath }
|
|
|
|
);
|
|
|
|
|
|
|
|
// prettier.format() may throw a SyntaxError if it cannot parse the
|
|
|
|
// source code it is given. Ususally for JS files this isn't a
|
|
|
|
// problem as ESLint will report invalid syntax before trying to
|
|
|
|
// pass it to the prettier plugin. However this might be a problem
|
|
|
|
// for non-JS languages that are handled by a plugin. Notably Vue
|
|
|
|
// files throw an error if they contain unclosed elements, such as
|
|
|
|
// `<template><div></template>. In this case report an error at the
|
|
|
|
// point at which parsing failed.
|
|
|
|
let prettierSource;
|
|
|
|
try {
|
|
|
|
prettierSource = prettier.format(source, prettierOptions);
|
|
|
|
} catch (err) {
|
|
|
|
if (!(err instanceof SyntaxError)) {
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
|
|
|
|
let message = 'Parsing error: ' + err.message;
|
|
|
|
|
|
|
|
// Prettier's message contains a codeframe style preview of the
|
|
|
|
// invalid code and the line/column at which the error occured.
|
|
|
|
// ESLint shows those pieces of information elsewhere already so
|
|
|
|
// remove them from the message
|
|
|
|
if (err.codeFrame) {
|
|
|
|
message = message.replace(`\n${err.codeFrame}`, '');
|
|
|
|
}
|
|
|
|
if (err.loc) {
|
|
|
|
message = message.replace(/ \(\d+:\d+\)$/, '');
|
|
|
|
}
|
|
|
|
|
|
|
|
context.report({ message, loc: err.loc });
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (source !== prettierSource) {
|
|
|
|
const differences = generateDifferences(source, prettierSource);
|
|
|
|
|
|
|
|
differences.forEach(difference => {
|
|
|
|
switch (difference.operation) {
|
|
|
|
case INSERT:
|
|
|
|
reportInsert(
|
|
|
|
context,
|
|
|
|
difference.offset,
|
|
|
|
difference.insertText
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
case DELETE:
|
|
|
|
reportDelete(
|
|
|
|
context,
|
|
|
|
difference.offset,
|
|
|
|
difference.deleteText
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
case REPLACE:
|
|
|
|
reportReplace(
|
|
|
|
context,
|
|
|
|
difference.offset,
|
|
|
|
difference.deleteText,
|
|
|
|
difference.insertText
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|