123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- /**
- * @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;
- }
- });
- }
- }
- };
- }
- }
- }
- };
|