blob: 4735e8fc7dc0f2a2f0be7b1db67786010ddb0d11 [file] [log] [blame]
/**
* @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'
},
type: 'layout',
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;
// 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) {
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;
}
});
}
}
};
}
}
}
};