diff --git a/.eslintrc.js b/.eslintrc.js index 4ff74862..985b8a9b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,6 +12,7 @@ module.exports = { 'plugin:eslint-plugin/recommended', 'prettier' ], + env: { mocha: true }, root: true, rules: { 'prettier/prettier': ['error', { singleQuote: true }], diff --git a/eslint-plugin-prettier.js b/eslint-plugin-prettier.js index 1bb9d0c4..9b979fb5 100644 --- a/eslint-plugin-prettier.js +++ b/eslint-plugin-prettier.js @@ -27,6 +27,10 @@ const FB_PRETTIER_OPTIONS = { const LINE_ENDING_RE = /\r\n|[\r\n\u2028\u2029]/; +const OPERATION_INSERT = 'insert'; +const OPERATION_DELETE = 'delete'; +const OPERATION_REPLACE = 'replace'; + // ------------------------------------------------------------------------------ // Privates // ------------------------------------------------------------------------------ @@ -104,18 +108,13 @@ function showInvisibles(str) { return ret; } -// ------------------------------------------------------------------------------ -// Rule Definition -// ------------------------------------------------------------------------------ - /** - * Reports issues where the context's source code differs from the Prettier - formatted version. - * @param {RuleContext} context - The ESLint rule context. + * Generate results for differences between source code and formatted version. + * @param {string} source - The original source. * @param {string} prettierSource - The Prettier formatted source. - * @returns {void} + * @returns {Array} - An array contains { operation, offset, insertText, deleteText } */ -function reportDifferences(context, prettierSource) { +function generateDifferences(source, prettierSource) { // fast-diff returns the differences between two texts as a series of // INSERT, DELETE or EQUAL operations. The results occur only in these // sequences: @@ -130,8 +129,8 @@ function reportDifferences(context, prettierSource) { // and another's beginning does not have line endings (i.e. issues that occur // on contiguous lines). - const source = context.getSourceCode().text; const results = diff(source, prettierSource); + const differences = []; const batch = []; let offset = 0; // NOTE: INSERT never advances the offset. @@ -166,6 +165,8 @@ function reportDifferences(context, prettierSource) { } } + return differences; + function flush() { let aheadDeleteText = ''; let aheadInsertText = ''; @@ -187,16 +188,33 @@ function reportDifferences(context, prettierSource) { } } if (aheadDeleteText && aheadInsertText) { - reportReplace(context, offset, aheadDeleteText, aheadInsertText); + differences.push({ + offset, + operation: OPERATION_REPLACE, + insertText: aheadInsertText, + deleteText: aheadDeleteText + }); } else if (!aheadDeleteText && aheadInsertText) { - reportInsert(context, offset, aheadInsertText); + differences.push({ + offset, + operation: OPERATION_INSERT, + insertText: aheadInsertText + }); } else if (aheadDeleteText && !aheadInsertText) { - reportDelete(context, offset, aheadDeleteText); + differences.push({ + offset, + operation: OPERATION_DELETE, + deleteText: aheadDeleteText + }); } offset += aheadDeleteText.length; } } +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + /** * Reports an "Insert ..." issue where text must be inserted. * @param {RuleContext} context - The ESLint rule context. @@ -268,68 +286,99 @@ function reportReplace(context, offset, deleteText, insertText) { // Module Definition // ------------------------------------------------------------------------------ -module.exports.rules = { - prettier: { - meta: { - fixable: 'code', - schema: [ - // Prettier options: - { - anyOf: [ - { enum: [null, 'fb'] }, - { type: 'object', properties: {}, additionalProperties: true } - ] - }, - // Pragma: - { type: 'string', pattern: '^@\\w+$' } - ] - }, - create(context) { - const prettierOptions = context.options[0] === 'fb' - ? FB_PRETTIER_OPTIONS - : context.options[0]; - - const pragma = context.options[1] - ? context.options[1].slice(1) // Remove leading @ - : null; +module.exports = { + showInvisibles, + generateDifferences, + rules: { + prettier: { + meta: { + fixable: 'code', + schema: [ + // Prettier options: + { + anyOf: [ + { enum: [null, 'fb'] }, + { type: 'object', properties: {}, additionalProperties: true } + ] + }, + // Pragma: + { type: 'string', pattern: '^@\\w+$' } + ] + }, + create(context) { + const prettierOptions = context.options[0] === 'fb' + ? FB_PRETTIER_OPTIONS + : context.options[0]; - const sourceCode = context.getSourceCode(); - const source = sourceCode.text; + const pragma = context.options[1] + ? context.options[1].slice(1) // Remove leading @ + : null; - // The pragma is only valid if it is found in a block comment at the very - // start of the file. - if (pragma) { - // ESLint 3.x reports the shebang as a "Line" node, while ESLint 4.x - // reports it as a "Shebang" node. This works for both versions: - const hasShebang = source.startsWith('#!'); - const allComments = sourceCode.getAllComments(); - const firstComment = hasShebang ? allComments[1] : allComments[0]; - if ( - !(firstComment && - firstComment.type === 'Block' && - firstComment.loc.start.line === (hasShebang ? 2 : 1) && - firstComment.loc.start.column === 0) - ) { - return {}; - } - const parsed = docblock.parse(firstComment.value); - if (parsed[pragma] !== '') { - return {}; - } - } + const sourceCode = context.getSourceCode(); + const source = sourceCode.text; - return { - Program() { - if (!prettier) { - // Prettier is expensive to load, so only load it if needed. - prettier = require('prettier'); + // The pragma is only valid if it is found in a block comment at the very + // start of the file. + if (pragma) { + // ESLint 3.x reports the shebang as a "Line" node, while ESLint 4.x + // reports it as a "Shebang" node. This works for both versions: + const hasShebang = source.startsWith('#!'); + const allComments = sourceCode.getAllComments(); + const firstComment = hasShebang ? allComments[1] : allComments[0]; + if ( + !(firstComment && + firstComment.type === 'Block' && + firstComment.loc.start.line === (hasShebang ? 2 : 1) && + firstComment.loc.start.column === 0) + ) { + return {}; } - const prettierSource = prettier.format(source, prettierOptions); - if (source !== prettierSource) { - reportDifferences(context, prettierSource); + const parsed = docblock.parse(firstComment.value); + if (parsed[pragma] !== '') { + return {}; } } - }; + + return { + Program() { + if (!prettier) { + // Prettier is expensive to load, so only load it if needed. + prettier = require('prettier'); + } + const prettierSource = prettier.format(source, prettierOptions); + if (source !== prettierSource) { + const differences = generateDifferences(source, prettierSource); + + differences.forEach(difference => { + switch (difference.operation) { + case OPERATION_INSERT: + reportInsert( + context, + difference.offset, + difference.insertText + ); + break; + case OPERATION_DELETE: + reportDelete( + context, + difference.offset, + difference.deleteText + ); + break; + case OPERATION_REPLACE: + reportReplace( + context, + difference.offset, + difference.deleteText, + difference.insertText + ); + break; + } + }); + } + } + }; + } } } }; diff --git a/package.json b/package.json index 0abcf236..b09e902e 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "jest-docblock": "^20.0.1" }, "peerDependencies": { - "eslint": ">=3.14.1", "prettier": ">= 0.11.0" }, "devDependencies": { diff --git a/test/prettier.js b/test/prettier.js index 83edfe65..8eaa54bc 100644 --- a/test/prettier.js +++ b/test/prettier.js @@ -14,8 +14,11 @@ const fs = require('fs'); const path = require('path'); +const assert = require('assert'); -const rule = require('..').rules.prettier; +const eslintPluginPrettier = require('..'); + +const rule = eslintPluginPrettier.rules.prettier; const RuleTester = require('eslint').RuleTester; // ------------------------------------------------------------------------------ @@ -64,6 +67,33 @@ ruleTester.run('prettier', rule, { ].map(loadInvalidFixture) }); +describe('generateDifferences', () => { + it('operation: insert', () => { + const differences = eslintPluginPrettier.generateDifferences( + 'abc', + 'abcdef' + ); + assert.deepEqual(differences, [ + { operation: 'insert', offset: 3, insertText: 'def' } + ]); + }); + it('operation: delete', () => { + const differences = eslintPluginPrettier.generateDifferences( + 'abcdef', + 'abc' + ); + assert.deepEqual(differences, [ + { operation: 'delete', offset: 3, deleteText: 'def' } + ]); + }); + it('operation: replace', () => { + const differences = eslintPluginPrettier.generateDifferences('abc', 'def'); + assert.deepEqual(differences, [ + { operation: 'replace', offset: 0, deleteText: 'abc', insertText: 'def' } + ]); + }); +}); + // ------------------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------------------