├── .npmignore ├── index.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── package.json ├── README.md ├── LICENSE ├── rules └── sort-imports-es6.js └── tests └── sort-imports-es6.js /.npmignore: -------------------------------------------------------------------------------- 1 | # Everything 2 | * 3 | 4 | !index.js 5 | !rules/* 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A sort-imports rule that properly distinguishes between ES6 import types. 3 | * @author Nicholas C. Zakas 4 | * @author Erik Desjardins 5 | */ 6 | 'use strict'; 7 | 8 | module.exports = { 9 | rules: { 10 | 'sort-imports-es6': require('./rules/sort-imports-es6') 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - v*.*.* 9 | pull_request: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: '12.x' 19 | registry-url: 'https://registry.npmjs.org' 20 | - run: npm install 21 | - run: npm test 22 | - run: npm publish 23 | if: startsWith(github.ref, 'refs/tags/') 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # IntelliJ 36 | *.iml 37 | .idea 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-sort-imports-es6", 3 | "version": "0.0.3", 4 | "description": "A sort-imports rule that properly distinguishes between ES6 import types.", 5 | "keywords": [ 6 | "eslint", 7 | "eslintplugin", 8 | "eslint-plugin" 9 | ], 10 | "author": "Erik Desjardins", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/erikdesjardins/eslint-plugin-sort-imports-es6.git" 14 | }, 15 | "main": "index.js", 16 | "scripts": { 17 | "test": "mocha ./tests/*.js" 18 | }, 19 | "peerDependencies": { 20 | "eslint": ">=2.0.0" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^6.6.0", 24 | "mocha": "^6.2.2" 25 | }, 26 | "license": "MIT" 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-sort-imports-es6 2 | 3 | A sort-imports rule that properly distinguishes between ES6 import types. 4 | 5 | ESLint's built-in `sort-imports` rule considers the following to be the same type of import: 6 | 7 | ```js 8 | import foo from 'foo'; 9 | import { bar } from 'bar'; 10 | ``` 11 | 12 | This version of the rule fixes that. 13 | 14 | It accepts the same options as the [original rule](http://eslint.org/docs/rules/sort-imports), but the `multiple` type corresponds to all named imports (regardless of how many are imported), while the `single` type corresponds only to default imports. 15 | 16 | ## Usage 17 | 18 | `npm i --save-dev eslint-plugin-sort-imports-es6` 19 | 20 | ```json 21 | { 22 | "plugins": [ 23 | "sort-imports-es6" 24 | ], 25 | "rules": { 26 | "sort-imports-es6/sort-imports-es6": [2, { 27 | "ignoreCase": false, 28 | "ignoreMemberSort": false, 29 | "memberSyntaxSortOrder": ["none", "all", "multiple", "single"] 30 | }] 31 | } 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ESLint 2 | Copyright (c) 2013 Nicholas C. Zakas. All rights reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /rules/sort-imports-es6.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Rule to require sorting of import declarations 3 | * @author Christian Schuller 4 | * @copyright 2015 Christian Schuller. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | "use strict"; 9 | 10 | //------------------------------------------------------------------------------ 11 | // Rule Definition 12 | //------------------------------------------------------------------------------ 13 | 14 | module.exports = function(context) { 15 | 16 | var configuration = context.options[0] || {}, 17 | ignoreCase = configuration.ignoreCase || false, 18 | ignoreMemberSort = configuration.ignoreMemberSort || false, 19 | memberSyntaxSortOrder = configuration.memberSyntaxSortOrder || ["none", "all", "multiple", "single"], 20 | previousDeclaration = null; 21 | 22 | /** 23 | * Gets the used member syntax style. 24 | * 25 | * import "my-module.js" --> none 26 | * import * as myModule from "my-module.js" --> all 27 | * import {myMember} from "my-module.js" --> single 28 | * import {foo, bar} from "my-module.js" --> multiple 29 | * 30 | * @param {ASTNode} node - the ImportDeclaration node. 31 | * @returns {string} used member parameter style, ["all", "multiple", "single"] 32 | */ 33 | function usedMemberSyntax(node) { 34 | if (node.specifiers.length === 0) { 35 | return "none"; 36 | } else if (node.specifiers[0].type === "ImportNamespaceSpecifier") { 37 | return "all"; 38 | } else if (node.specifiers[0].type === "ImportDefaultSpecifier") { 39 | return "single"; 40 | } else { 41 | return "multiple"; 42 | } 43 | } 44 | 45 | /** 46 | * Gets the group by member parameter index for given declaration. 47 | * @param {ASTNode} node - the ImportDeclaration node. 48 | * @returns {number} the declaration group by member index. 49 | */ 50 | function getMemberParameterGroupIndex(node) { 51 | return memberSyntaxSortOrder.indexOf(usedMemberSyntax(node)); 52 | } 53 | 54 | /** 55 | * Gets the local name of the first imported module. 56 | * @param {ASTNode} node - the ImportDeclaration node. 57 | * @returns {?string} the local name of the first imported module. 58 | */ 59 | function getFirstLocalMemberName(node) { 60 | if (node.specifiers[0]) { 61 | return node.specifiers[0].local.name; 62 | } else { 63 | return null; 64 | } 65 | } 66 | 67 | return { 68 | "ImportDeclaration": function(node) { 69 | if (previousDeclaration) { 70 | var currentLocalMemberName = getFirstLocalMemberName(node), 71 | currentMemberSyntaxGroupIndex = getMemberParameterGroupIndex(node), 72 | previousLocalMemberName = getFirstLocalMemberName(previousDeclaration), 73 | previousMemberSyntaxGroupIndex = getMemberParameterGroupIndex(previousDeclaration); 74 | 75 | if (ignoreCase) { 76 | previousLocalMemberName = previousLocalMemberName && previousLocalMemberName.toLowerCase(); 77 | currentLocalMemberName = currentLocalMemberName && currentLocalMemberName.toLowerCase(); 78 | } 79 | 80 | // When the current declaration uses a different member syntax, 81 | // then check if the ordering is correct. 82 | // Otherwise, make a default string compare (like rule sort-vars to be consistent) of the first used local member name. 83 | if (currentMemberSyntaxGroupIndex !== previousMemberSyntaxGroupIndex) { 84 | if (currentMemberSyntaxGroupIndex < previousMemberSyntaxGroupIndex) { 85 | context.report({ 86 | node: node, 87 | message: "Expected '{{syntaxA}}' syntax before '{{syntaxB}}' syntax.", 88 | data: { 89 | syntaxA: memberSyntaxSortOrder[currentMemberSyntaxGroupIndex], 90 | syntaxB: memberSyntaxSortOrder[previousMemberSyntaxGroupIndex] 91 | } 92 | }); 93 | } 94 | } else { 95 | if (previousLocalMemberName && 96 | currentLocalMemberName && 97 | currentLocalMemberName < previousLocalMemberName 98 | ) { 99 | context.report({ 100 | node: node, 101 | message: "Imports should be sorted alphabetically." 102 | }); 103 | } 104 | } 105 | } 106 | 107 | // Multiple members of an import declaration should also be sorted alphabetically. 108 | if (!ignoreMemberSort && node.specifiers.length > 1) { 109 | var previousSpecifier = null; 110 | var previousSpecifierName = null; 111 | 112 | for (var i = 0; i < node.specifiers.length; ++i) { 113 | var currentSpecifier = node.specifiers[i]; 114 | if (currentSpecifier.type !== "ImportSpecifier") { 115 | continue; 116 | } 117 | 118 | var currentSpecifierName = currentSpecifier.local.name; 119 | if (ignoreCase) { 120 | currentSpecifierName = currentSpecifierName.toLowerCase(); 121 | } 122 | 123 | if (previousSpecifier && currentSpecifierName < previousSpecifierName) { 124 | context.report({ 125 | node: currentSpecifier, 126 | message: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.", 127 | data: { 128 | memberName: currentSpecifier.local.name 129 | } 130 | }); 131 | } 132 | 133 | previousSpecifier = currentSpecifier; 134 | previousSpecifierName = currentSpecifierName; 135 | } 136 | } 137 | 138 | previousDeclaration = node; 139 | } 140 | }; 141 | }; 142 | 143 | module.exports.schema = [ 144 | { 145 | "type": "object", 146 | "properties": { 147 | "ignoreCase": { 148 | "type": "boolean" 149 | }, 150 | "memberSyntaxSortOrder": { 151 | "type": "array", 152 | "items": { 153 | "enum": ["none", "all", "multiple", "single"] 154 | }, 155 | "uniqueItems": true, 156 | "minItems": 4, 157 | "maxItems": 4 158 | }, 159 | "ignoreMemberSort": { 160 | "type": "boolean" 161 | } 162 | }, 163 | "additionalProperties": false 164 | } 165 | ]; 166 | -------------------------------------------------------------------------------- /tests/sort-imports-es6.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for sort-imports rule. 3 | * @author Christian Schuller 4 | * @copyright 2015 Christian Schuller. All rights reserved. 5 | * See LICENSE file in root directory for full license. 6 | */ 7 | 8 | "use strict"; 9 | 10 | //------------------------------------------------------------------------------ 11 | // Requirements 12 | //------------------------------------------------------------------------------ 13 | 14 | var rule = require("../rules/sort-imports-es6"); 15 | var RuleTester = require('eslint').RuleTester; 16 | 17 | //------------------------------------------------------------------------------ 18 | // Tests 19 | //------------------------------------------------------------------------------ 20 | 21 | var ruleTester = new RuleTester(), 22 | parserOptions = { 23 | ecmaVersion: 6, 24 | sourceType: "module" 25 | }, 26 | expectedError = { 27 | message: "Imports should be sorted alphabetically.", 28 | type: "ImportDeclaration" 29 | }, 30 | ignoreCaseArgs = [{ignoreCase: true}]; 31 | 32 | ruleTester.run("sort-imports", rule, { 33 | valid: [ 34 | { 35 | code: 36 | "import a from 'foo.js';\n" + 37 | "import b from 'bar.js';\n" + 38 | "import c from 'baz.js';\n", 39 | parserOptions: parserOptions 40 | }, 41 | { 42 | code: 43 | "import * as B from 'foo.js';\n" + 44 | "import A from 'bar.js';", 45 | parserOptions: parserOptions 46 | }, 47 | { 48 | code: 49 | "import * as B from 'foo.js';\n" + 50 | "import {a, b} from 'bar.js';", 51 | parserOptions: parserOptions 52 | }, 53 | { 54 | code: 55 | "import {b, c} from 'bar.js';\n" + 56 | "import A from 'foo.js';", 57 | parserOptions: parserOptions 58 | }, 59 | { 60 | code: 61 | "import A from 'bar.js';\n" + 62 | "import {b, c} from 'foo.js';", 63 | parserOptions: parserOptions, 64 | options: [{ 65 | memberSyntaxSortOrder: [ "single", "multiple", "none", "all" ] 66 | }] 67 | }, 68 | { 69 | code: 70 | "import {a, b} from 'bar.js';\n" + 71 | "import {b_, c} from 'foo.js';", 72 | parserOptions: parserOptions 73 | }, 74 | { 75 | code: 76 | "import A from 'foo.js';\n" + 77 | "import B from 'bar.js';", 78 | parserOptions: parserOptions 79 | }, 80 | { 81 | code: 82 | "import A from 'foo.js';\n" + 83 | "import a from 'bar.js';", 84 | parserOptions: parserOptions 85 | }, 86 | { 87 | code: 88 | "import a, * as b from 'foo.js';\n" + 89 | "import b_ from 'bar.js';", 90 | parserOptions: parserOptions 91 | }, 92 | { 93 | code: 94 | "import 'foo.js';\n" + 95 | " import a from 'bar.js';", 96 | parserOptions: parserOptions 97 | }, 98 | { 99 | code: 100 | "import B from 'foo.js';\n" + 101 | "import a from 'bar.js';", 102 | parserOptions: parserOptions 103 | }, 104 | { 105 | code: 106 | "import a from 'foo.js';\n" + 107 | "import B from 'bar.js';", 108 | parserOptions: parserOptions, 109 | options: ignoreCaseArgs 110 | }, 111 | { 112 | code: "import {a, b, c, d} from 'foo.js';", 113 | parserOptions: parserOptions 114 | }, 115 | { 116 | code: "import {b, A, C, d} from 'foo.js';", 117 | parserOptions: parserOptions, 118 | options: [{ 119 | ignoreMemberSort: true 120 | }] 121 | }, 122 | { 123 | code: "import {B, a, C, d} from 'foo.js';", 124 | parserOptions: parserOptions, 125 | options: [{ 126 | ignoreMemberSort: true 127 | }] 128 | }, 129 | { 130 | code: "import {a, B, c, D} from 'foo.js';", 131 | parserOptions: parserOptions, 132 | options: ignoreCaseArgs 133 | }, 134 | { 135 | code: "import a, * as b from 'foo.js';", 136 | parserOptions: parserOptions 137 | }, 138 | { 139 | code: 140 | "import * as a from 'foo.js';\n" + 141 | "\n" + 142 | "import b from 'bar.js';", 143 | parserOptions: parserOptions 144 | }, 145 | { 146 | code: 147 | "import * as bar from 'bar.js';\n" + 148 | "import * as foo from 'foo.js';", 149 | parserOptions: parserOptions 150 | }, 151 | 152 | // https://github.com/eslint/eslint/issues/5130 153 | { 154 | code: 155 | "import 'foo';\n" + 156 | "import bar from 'bar';", 157 | parserOptions: parserOptions, 158 | options: ignoreCaseArgs 159 | }, 160 | 161 | // https://github.com/eslint/eslint/issues/5305 162 | { 163 | code: "import React, {Component} from 'react';", 164 | parserOptions: parserOptions 165 | }, 166 | 167 | // ensure that a single named import is treated differently from a default import 168 | { 169 | code: 170 | "import {foo} from 'foo';\n" + 171 | "import bar from 'bar';", 172 | parserOptions: parserOptions 173 | } 174 | ], 175 | invalid: [ 176 | { 177 | code: 178 | "import a from 'foo.js';\n" + 179 | "import A from 'bar.js';", 180 | parserOptions: parserOptions, 181 | errors: [expectedError] 182 | }, 183 | { 184 | code: 185 | "import b from 'foo.js';\n" + 186 | "import a from 'bar.js';", 187 | parserOptions: parserOptions, 188 | errors: [expectedError] 189 | }, 190 | { 191 | code: 192 | "import {b, c} from 'foo.js';\n" + 193 | "import {a, b_} from 'bar.js';", 194 | parserOptions: parserOptions, 195 | errors: [expectedError] 196 | }, 197 | { 198 | code: 199 | "import * as foo from 'foo.js';\n" + 200 | "import * as bar from 'bar.js';", 201 | parserOptions: parserOptions, 202 | errors: [expectedError] 203 | }, 204 | { 205 | code: 206 | "import a from 'foo.js';\n" + 207 | "import {b, c} from 'bar.js';", 208 | parserOptions: parserOptions, 209 | errors: [{ 210 | message: "Expected 'multiple' syntax before 'single' syntax.", 211 | type: "ImportDeclaration" 212 | }] 213 | }, 214 | { 215 | code: 216 | "import a from 'foo.js';\n" + 217 | "import * as b from 'bar.js';", 218 | parserOptions: parserOptions, 219 | errors: [{ 220 | message: "Expected 'all' syntax before 'single' syntax.", 221 | type: "ImportDeclaration" 222 | }] 223 | }, 224 | { 225 | code: 226 | "import a from 'foo.js';\n" + 227 | "import 'bar.js';", 228 | parserOptions: parserOptions, 229 | errors: [{ 230 | message: "Expected 'none' syntax before 'single' syntax.", 231 | type: "ImportDeclaration" 232 | }] 233 | }, 234 | { 235 | code: 236 | "import b from 'bar.js';\n" + 237 | "import * as a from 'foo.js';", 238 | parserOptions: parserOptions, 239 | options: [{ 240 | memberSyntaxSortOrder: [ "all", "single", "multiple", "none" ] 241 | }], 242 | errors: [{ 243 | message: "Expected 'all' syntax before 'single' syntax.", 244 | type: "ImportDeclaration" 245 | }] 246 | }, 247 | { 248 | code: "import {b, a, d, c} from 'foo.js';", 249 | parserOptions: parserOptions, 250 | errors: [{ 251 | message: "Member 'a' of the import declaration should be sorted alphabetically.", 252 | type: "ImportSpecifier" 253 | }, { 254 | message: "Member 'c' of the import declaration should be sorted alphabetically.", 255 | type: "ImportSpecifier" 256 | }] 257 | }, 258 | { 259 | code: "import {a, B, c, D} from 'foo.js';", 260 | parserOptions: parserOptions, 261 | errors: [{ 262 | message: "Member 'B' of the import declaration should be sorted alphabetically.", 263 | type: "ImportSpecifier" 264 | }, { 265 | message: "Member 'D' of the import declaration should be sorted alphabetically.", 266 | type: "ImportSpecifier" 267 | }] 268 | }, 269 | // ensure that a single named import is treated differently from a default import 270 | { 271 | code: 272 | "import foo from 'foo';\n" + 273 | "import { bar } from 'bar'", 274 | parserOptions: parserOptions, 275 | errors: [{ 276 | message: "Expected 'multiple' syntax before 'single' syntax.", 277 | type: "ImportDeclaration" 278 | }] 279 | }, 280 | // ensure that multiple named imports are sorted even when there's a default import 281 | { 282 | code: "import foo, {a, B, c, D} from 'foo.js';", 283 | parserOptions: parserOptions, 284 | errors: [{ 285 | message: "Member 'B' of the import declaration should be sorted alphabetically.", 286 | type: "ImportSpecifier" 287 | }, { 288 | message: "Member 'D' of the import declaration should be sorted alphabetically.", 289 | type: "ImportSpecifier" 290 | }] 291 | } 292 | ] 293 | }); 294 | --------------------------------------------------------------------------------