255 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			255 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * @file Runs `prettier` as an ESLint rule.
 | |
|  * @author Andres Suarez
 | |
|  */
 | |
| 
 | |
| // @ts-check
 | |
| 
 | |
| /**
 | |
|  * @typedef {import('eslint').AST.Range} Range
 | |
|  * @typedef {import('eslint').AST.SourceLocation} SourceLocation
 | |
|  * @typedef {import('eslint').ESLint.Plugin} Plugin
 | |
|  * @typedef {import('eslint').ESLint.ObjectMetaProperties} ObjectMetaProperties
 | |
|  * @typedef {import('prettier').FileInfoOptions} FileInfoOptions
 | |
|  * @typedef {import('prettier').Options} PrettierOptions
 | |
|  * @typedef {PrettierOptions & { onDiskFilepath: string, parserMeta?: ObjectMetaProperties['meta'], parserPath?: string, usePrettierrc?: boolean }} Options
 | |
|  */
 | |
| 
 | |
| 'use strict';
 | |
| 
 | |
| // ------------------------------------------------------------------------------
 | |
| //  Requirements
 | |
| // ------------------------------------------------------------------------------
 | |
| 
 | |
| const {
 | |
|   showInvisibles,
 | |
|   generateDifferences,
 | |
| } = require('prettier-linter-helpers');
 | |
| const { name, version } = require('./package.json');
 | |
| 
 | |
| // ------------------------------------------------------------------------------
 | |
| //  Constants
 | |
| // ------------------------------------------------------------------------------
 | |
| 
 | |
| const { INSERT, DELETE, REPLACE } = generateDifferences;
 | |
| 
 | |
| // ------------------------------------------------------------------------------
 | |
| //  Privates
 | |
| // ------------------------------------------------------------------------------
 | |
| 
 | |
| // Lazily-loaded Prettier.
 | |
| /**
 | |
|  * @type {(source: string, options: Options, fileInfoOptions: FileInfoOptions) => string}
 | |
|  */
 | |
| let prettierFormat;
 | |
| 
 | |
| // ------------------------------------------------------------------------------
 | |
| //  Rule Definition
 | |
| // ------------------------------------------------------------------------------
 | |
| 
 | |
| /**
 | |
|  * Reports a difference.
 | |
|  *
 | |
|  * @param {import('eslint').Rule.RuleContext} context - The ESLint rule context.
 | |
|  * @param {import('prettier-linter-helpers').Difference} difference - The difference object.
 | |
|  * @returns {void}
 | |
|  */
 | |
| function reportDifference(context, difference) {
 | |
|   const { operation, offset, deleteText = '', insertText = '' } = difference;
 | |
|   const range = /** @type {Range} */ ([offset, offset + deleteText.length]);
 | |
|   // `context.getSourceCode()` was deprecated in ESLint v8.40.0 and replaced
 | |
|   // with the `sourceCode` property.
 | |
|   // TODO: Only use property when our eslint peerDependency is >=8.40.0.
 | |
|   const [start, end] = range.map(index =>
 | |
|     (context.sourceCode ?? context.getSourceCode()).getLocFromIndex(index),
 | |
|   );
 | |
| 
 | |
|   context.report({
 | |
|     messageId: operation,
 | |
|     data: {
 | |
|       deleteText: showInvisibles(deleteText),
 | |
|       insertText: showInvisibles(insertText),
 | |
|     },
 | |
|     loc: { start, end },
 | |
|     fix: fixer => fixer.replaceTextRange(range, insertText),
 | |
|   });
 | |
| }
 | |
| 
 | |
| // ------------------------------------------------------------------------------
 | |
| //  Module Definition
 | |
| // ------------------------------------------------------------------------------
 | |
| 
 | |
| /**
 | |
|  * @type {Plugin}
 | |
|  */
 | |
| const eslintPluginPrettier = {
 | |
|   meta: { name, version },
 | |
|   configs: {
 | |
|     recommended: {
 | |
|       extends: ['prettier'],
 | |
|       plugins: ['prettier'],
 | |
|       rules: {
 | |
|         'prettier/prettier': 'error',
 | |
|         'arrow-body-style': 'off',
 | |
|         'prefer-arrow-callback': 'off',
 | |
|       },
 | |
|     },
 | |
|   },
 | |
|   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,
 | |
|           },
 | |
|         ],
 | |
|         messages: {
 | |
|           [INSERT]: 'Insert `{{ insertText }}`',
 | |
|           [DELETE]: 'Delete `{{ deleteText }}`',
 | |
|           [REPLACE]: 'Replace `{{ deleteText }}` with `{{ insertText }}`',
 | |
|         },
 | |
|       },
 | |
|       create(context) {
 | |
|         const usePrettierrc =
 | |
|           !context.options[1] || context.options[1].usePrettierrc !== false;
 | |
|         /**
 | |
|          * @type {FileInfoOptions}
 | |
|          */
 | |
|         const fileInfoOptions =
 | |
|           (context.options[1] && context.options[1].fileInfoOptions) || {};
 | |
| 
 | |
|         // `context.getSourceCode()` was deprecated in ESLint v8.40.0 and replaced
 | |
|         // with the `sourceCode` property.
 | |
|         // TODO: Only use property when our eslint peerDependency is >=8.40.0.
 | |
|         const sourceCode = context.sourceCode ?? context.getSourceCode();
 | |
|         // `context.getFilename()` was deprecated in ESLint v8.40.0 and replaced
 | |
|         // with the `filename` property.
 | |
|         // TODO: Only use property when our eslint peerDependency is >=8.40.0.
 | |
|         const filepath = context.filename ?? context.getFilename();
 | |
| 
 | |
|         // Processors that extract content from a file, such as the markdown
 | |
|         // plugin extracting fenced code blocks may choose to specify virtual
 | |
|         // file paths. If this is the case then we need to resolve prettier
 | |
|         // config and file info using the on-disk path instead of the virtual
 | |
|         // path.
 | |
|         // `context.getPhysicalFilename()` was deprecated in ESLint v8.40.0 and replaced
 | |
|         // with the `physicalFilename` property.
 | |
|         // TODO: Only use property when our eslint peerDependency is >=8.40.0.
 | |
|         const onDiskFilepath =
 | |
|           context.physicalFilename ?? context.getPhysicalFilename();
 | |
|         const source = sourceCode.text;
 | |
| 
 | |
|         return {
 | |
|           Program() {
 | |
|             if (!prettierFormat) {
 | |
|               // Prettier is expensive to load, so only load it if needed.
 | |
|               prettierFormat = require('synckit').createSyncFn(
 | |
|                 require.resolve('./worker'),
 | |
|               );
 | |
|             }
 | |
| 
 | |
|             /**
 | |
|              * @type {PrettierOptions}
 | |
|              */
 | |
|             const eslintPrettierOptions = context.options[0] || {};
 | |
| 
 | |
|             const parser = context.languageOptions?.parser;
 | |
| 
 | |
|             // prettier.format() may throw a SyntaxError if it cannot parse the
 | |
|             // source code it is given. Usually 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.
 | |
|             /**
 | |
|              * @type {string}
 | |
|              */
 | |
|             let prettierSource;
 | |
|             try {
 | |
|               prettierSource = prettierFormat(
 | |
|                 source,
 | |
|                 {
 | |
|                   ...eslintPrettierOptions,
 | |
|                   filepath,
 | |
|                   onDiskFilepath,
 | |
|                   parserMeta:
 | |
|                     parser &&
 | |
|                     (parser.meta ?? {
 | |
|                       name: parser.name,
 | |
|                       version: parser.version,
 | |
|                     }),
 | |
|                   parserPath: context.parserPath,
 | |
|                   usePrettierrc,
 | |
|                 },
 | |
|                 fileInfoOptions,
 | |
|               );
 | |
|             } catch (err) {
 | |
|               if (!(err instanceof SyntaxError)) {
 | |
|                 throw err;
 | |
|               }
 | |
| 
 | |
|               let message = 'Parsing error: ' + err.message;
 | |
| 
 | |
|               const error =
 | |
|                 /** @type {SyntaxError & {codeFrame: string; loc: SourceLocation}} */ (
 | |
|                   err
 | |
|                 );
 | |
| 
 | |
|               // Prettier's message contains a codeframe style preview of the
 | |
|               // invalid code and the line/column at which the error occurred.
 | |
|               // ESLint shows those pieces of information elsewhere already so
 | |
|               // remove them from the message
 | |
|               if (error.codeFrame) {
 | |
|                 message = message.replace(`\n${error.codeFrame}`, '');
 | |
|               }
 | |
|               if (error.loc) {
 | |
|                 message = message.replace(/ \(\d+:\d+\)$/, '');
 | |
|               }
 | |
| 
 | |
|               context.report({ message, loc: error.loc });
 | |
| 
 | |
|               return;
 | |
|             }
 | |
| 
 | |
|             if (prettierSource == null) {
 | |
|               return;
 | |
|             }
 | |
| 
 | |
|             if (source !== prettierSource) {
 | |
|               const differences = generateDifferences(source, prettierSource);
 | |
| 
 | |
|               for (const difference of differences) {
 | |
|                 reportDifference(context, difference);
 | |
|               }
 | |
|             }
 | |
|           },
 | |
|         };
 | |
|       },
 | |
|     },
 | |
|   },
 | |
| };
 | |
| 
 | |
| module.exports = eslintPluginPrettier;
 |