diff --git a/README.md b/README.md index a3b74de..26deb3f 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ If we were to provide configuration by default, then if `bottom-level/.eslintrc. | [liferay/no-require-and-call](./plugins/eslint-plugin-liferay/docs/rules/no-require-and-call.md) | liferay | [\#94](https://github.com/liferay/eslint-config-liferay/issues/94) | | [liferay/padded-test-blocks](./plugins/eslint-plugin-liferay/docs/rules/padded-test-blocks.md) | liferay | [\#75](https://github.com/liferay/eslint-config-liferay/pull/75) | | [liferay/sort-imports](./plugins/eslint-plugin-liferay/docs/rules/sort-imports.md) | liferay | [\#60](https://github.com/liferay/liferay-frontend-guidelines/issues/60) | +| [liferay/sort-import-destructures](./plugins/eslint-plugin-liferay/docs/rules/sort-import-destructures.md) | liferay | [\#124](https://github.com/liferay/eslint-config-liferay/issues/124) | | [liferay/sort-class-names](./plugins/eslint-plugin-liferay/docs/rules/sort-class-names.md) | liferay | [\#108](https://github.com/liferay/eslint-config-liferay/issues/108) | | [liferay/trim-class-names](./plugins/eslint-plugin-liferay/docs/rules/trim-class-names.md) | liferay | [\#108](https://github.com/liferay/eslint-config-liferay/issues/108) | | [no-console](https://eslint.org/docs/rules/no-console) | liferay | [\#79](https://github.com/liferay/eslint-config-liferay/pull/79) | @@ -175,6 +176,7 @@ The bundled `eslint-plugin-liferay` plugin includes the following [rules](./plug - [liferay/padded-test-blocks](./plugins/eslint-plugin-liferay/docs/rules/padded-test-blocks.md): Enforces blank lines between test blocks (`it()` etc). - [liferay/sort-class-names](./plugins/eslint-plugin-liferay/docs/rules/sort-class-names.md): Enforces (and autofixes) ordering of class names inside JSX `className` attributes. - [liferay/sort-imports](./plugins/eslint-plugin-liferay/docs/rules/sort-imports.md): Enforces (and autofixes) `import` and `require` ordering. +- [liferay/sort-import-destructures](./plugins/eslint-plugin-liferay/docs/rules/sort-import-destructures.md): Enforces (and autofixes) ordering of destructured names in `import` statements. - [liferay/trim-class-names](./plugins/eslint-plugin-liferay/docs/rules/trim-class-names.md): Enforces (and autofixes) that class names inside JSX `className` attributes do not have leading or trailing whitespace. #### `eslint-plugin-liferay-portal` diff --git a/index.js b/index.js index e00df4b..2c75442 100644 --- a/index.js +++ b/index.js @@ -36,6 +36,7 @@ const config = { 'liferay/no-it-should': 'error', 'liferay/no-require-and-call': 'error', 'liferay/padded-test-blocks': 'error', + 'liferay/sort-import-destructures': 'error', 'liferay/sort-imports': 'error', 'no-console': ['error', {allow: ['warn', 'error']}], 'no-constant-condition': ['error', {checkLoops: false}], diff --git a/plugins/eslint-plugin-liferay/docs/rules/sort-import-destructures.md b/plugins/eslint-plugin-liferay/docs/rules/sort-import-destructures.md new file mode 100644 index 0000000..16e2cea --- /dev/null +++ b/plugins/eslint-plugin-liferay/docs/rules/sort-import-destructures.md @@ -0,0 +1,27 @@ +# Sort destructured names in `import` statements (sort-import-destructures) + +This rule enforces (and autofixes) that destructured names in `import` statements are sorted. + +## Rule Details + +Examples of **incorrect** code for this rule: + +```js +import {xyz, abc} from 'other'; + +import main, {second as alias, first as nickname} from 'something'; +``` + +Examples of **correct** code for this rule: + +```js +import {abc, xyz} from 'other'; + +import main, {first as nickname, second as alias} from 'something'; +``` + +Note how the sorting is based on the name of the imported export (eg. `first`, `second`) and not the local name (eg. `nickname`, `alias`). + +## Further Reading + +- https://github.com/liferay/eslint-config-liferay/issues/124 diff --git a/plugins/eslint-plugin-liferay/index.js b/plugins/eslint-plugin-liferay/index.js index ee7eca6..909f48b 100644 --- a/plugins/eslint-plugin-liferay/index.js +++ b/plugins/eslint-plugin-liferay/index.js @@ -18,6 +18,7 @@ module.exports = { 'no-require-and-call': require('./lib/rules/no-require-and-call'), 'padded-test-blocks': require('./lib/rules/padded-test-blocks'), 'sort-class-names': require('./lib/rules/sort-class-names'), + 'sort-import-destructures': require('./lib/rules/sort-import-destructures'), 'sort-imports': require('./lib/rules/sort-imports'), 'trim-class-names': require('./lib/rules/trim-class-names'), }, diff --git a/plugins/eslint-plugin-liferay/lib/rules/sort-import-destructures.js b/plugins/eslint-plugin-liferay/lib/rules/sort-import-destructures.js new file mode 100644 index 0000000..d07983e --- /dev/null +++ b/plugins/eslint-plugin-liferay/lib/rules/sort-import-destructures.js @@ -0,0 +1,101 @@ +/** + * © 2017 Liferay, Inc. + * + * SPDX-License-Identifier: MIT + */ + +const DESCRIPTION = 'destructured names in imports must be sorted'; + +module.exports = { + create(context) { + return { + ImportDeclaration(node) { + const specifiers = node.specifiers.filter(specifier => { + // Just `ImportSpecifier` (ignore `ImportDefaultSpecifier`). + return specifier.type === 'ImportSpecifier'; + }); + + if (specifiers.length > 1) { + const source = context.getSourceCode(); + + if ( + source.commentsExistBetween( + source.getTokenBefore(specifiers[0]), + node.source + ) + ) { + // Don't touch if any of the specifiers have + // comments. + return; + } + + let fix; + + // Given: + // + // import {a as b, c} from 'd'; + // + // We'll have two specifiers: + // + // - `imported.name === 'a'`, `local.name === 'b'). + // - `imported.name === 'c'`. + // + // We sort by `imported` always, ignoring `local`. + const sorted = specifiers.slice().sort((a, b) => { + const order = + a.imported.name > b.imported.name ? 1 : -1; + + if (order === 1) { + fix = true; + } + + return order; + }); + + if (fix) { + const text = + ' '.repeat(node.start) + source.getText(node); + + const start = specifiers[0].start; + const end = specifiers[specifiers.length - 1].end; + + let fixed = ''; + + for (let i = 0; i < specifiers.length; i++) { + fixed += source.getText(sorted[i]); + + if (i < specifiers.length - 1) { + // Grab all text between specifier and next. + const between = text.slice( + specifiers[i].end, + specifiers[i + 1].start + ); + + fixed += between; + } + } + + context.report({ + fix: fixer => + fixer.replaceTextRange([start, end], fixed), + message: DESCRIPTION, + node, + }); + } + } + }, + }; + }, + + meta: { + docs: { + category: 'Best Practices', + description: DESCRIPTION, + recommended: false, + url: 'https://github.com/liferay/eslint-config-liferay/issues/124', + }, + fixable: 'code', + schema: [], + type: 'problem', + }, +}; diff --git a/plugins/eslint-plugin-liferay/tests/lib/rules/sort-import-destructures.js b/plugins/eslint-plugin-liferay/tests/lib/rules/sort-import-destructures.js new file mode 100644 index 0000000..a5f8f52 --- /dev/null +++ b/plugins/eslint-plugin-liferay/tests/lib/rules/sort-import-destructures.js @@ -0,0 +1,133 @@ +/** + * © 2017 Liferay, Inc. + * + * SPDX-License-Identifier: MIT + */ + +const {RuleTester} = require('eslint'); + +const rule = require('../../../lib/rules/sort-import-destructures'); + +const parserOptions = { + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + }, +}; + +const ruleTester = new RuleTester(parserOptions); + +ruleTester.run('sort-import-destructures', rule, { + invalid: [ + { + code: `import {z, g} from 'a';`, + errors: [ + { + message: 'destructured names in imports must be sorted', + }, + ], + output: `import {g, z} from 'a';`, + }, + { + code: `import {b as bar, a as foo} from 'b';`, + errors: [ + { + message: 'destructured names in imports must be sorted', + }, + ], + output: `import {a as foo, b as bar} from 'b';`, + }, + { + code: `import thing, {k, h, j} from 'c';`, + errors: [ + { + message: 'destructured names in imports must be sorted', + }, + ], + output: `import thing, {h, j, k} from 'c';`, + }, + { + // Same as previous, but with line breaks. + code: ` + import thing, { + k, + h, + j + } from 'c'; + `, + errors: [ + { + message: 'destructured names in imports must be sorted', + }, + ], + output: ` + import thing, { + h, + j, + k + } from 'c'; + `, + }, + { + // Note that trailing commas are preserved. + code: ` + import { + z, + y, + x, + } from 'file'; + `, + errors: [ + { + message: 'destructured names in imports must be sorted', + }, + ], + output: ` + import { + x, + y, + z, + } from 'file'; + `, + }, + ], + + valid: [ + { + code: `import thing from 'thing';`, + }, + { + code: `import {gizmo} from 'gizmo';`, + }, + { + code: `import {g, z} from 'a';`, + }, + { + code: `import {a as foo, b as bar} from 'b';`, + }, + { + code: `import thing, {h, j, k} from 'c';`, + }, + { + // We don't touch the sort order if there are comments anywhere. + code: ` + import { + // Comment. + zoo, + school + } from 'places'; + + import { + zzz, // Comment. + xxx + } from 'letters'; + + import { + three, + two, + one // Comment. + } from 'numbers'; + `, + }, + ], +}); diff --git a/plugins/eslint-plugin-liferay/tests/lib/rules/sort-imports.js b/plugins/eslint-plugin-liferay/tests/lib/rules/sort-imports.js index 26e37de..0e96a49 100644 --- a/plugins/eslint-plugin-liferay/tests/lib/rules/sort-imports.js +++ b/plugins/eslint-plugin-liferay/tests/lib/rules/sort-imports.js @@ -17,11 +17,6 @@ const parserOptions = { const ruleTester = new RuleTester(parserOptions); -// TODO: make sure destructuring is ordered too -// eg. https://github.com/mthadley/eslint-plugin-sort-destructure-keys -// eslint's sort-imports plug-in sorts destructuring patterns too -// https://github.com/eslint/eslint/blob/master/lib/rules/sort-imports.js -// i think these need to be separate rules ruleTester.run('sort-imports', rule, { invalid: [ {