├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── README.md ├── __test__ ├── .eslintrc ├── rules │ ├── sort-imports.spec.js │ └── sort-named-imports.spec.js └── utils │ ├── TranspositionManager.spec.js │ ├── getImportKind.spec.js │ └── nodes.spec.js ├── package-lock.json ├── package.json └── src ├── index.js ├── rules ├── sort-imports.js └── sort-named-imports.js └── utils ├── TranspositionManager.js ├── getImportGroup.js ├── getImportKind.js ├── isStaticRequire.js └── nodes.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": "commonjs", 7 | "targets": { 8 | "node": [ "6.0.0" ] 9 | }, 10 | "useBuiltIns": false // should we? 11 | } 12 | ] 13 | ], 14 | "plugins": [ 15 | "@babel/plugin-proposal-object-rest-spread" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | charset = utf-8 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/* 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends" : [ 4 | "airbnb", 5 | "prettier" 6 | ], 7 | "plugins": [ 8 | "prettier", 9 | "import" 10 | ], 11 | "parserOptions": { 12 | "ecmaVersion": 8, 13 | "sourceType": "module" 14 | }, 15 | "env": { 16 | "es6": true, 17 | "browser": true, 18 | "shared-node-browser": true 19 | }, 20 | "rules": { 21 | "semi": ["error", "never"], 22 | "prettier/prettier": ["error", { "singleQuote": true, "trailingComma": "es5", "jsxBracketSameLine": true, "semi": false, "printWidth": 100 }], 23 | "curly": ["error", "multi"], 24 | "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], 25 | "no-underscore-dangle": "off" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (http://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # Typescript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | 65 | # End of https://www.gitignore.io/api/node 66 | 67 | # Created by https://www.gitignore.io/api/webstorm 68 | 69 | ### WebStorm ### 70 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 71 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 72 | 73 | # User-specific stuff: 74 | .idea/**/workspace.xml 75 | .idea/**/tasks.xml 76 | .idea/dictionaries 77 | 78 | # Sensitive or high-churn files: 79 | .idea/**/dataSources/ 80 | .idea/**/dataSources.ids 81 | .idea/**/dataSources.xml 82 | .idea/**/dataSources.local.xml 83 | .idea/**/sqlDataSources.xml 84 | .idea/**/dynamic.xml 85 | .idea/**/uiDesigner.xml 86 | 87 | # Gradle: 88 | .idea/**/gradle.xml 89 | .idea/**/libraries 90 | 91 | # CMake 92 | cmake-build-debug/ 93 | 94 | # Mongo Explorer plugin: 95 | .idea/**/mongoSettings.xml 96 | 97 | ## File-based project format: 98 | *.iws 99 | 100 | ## Plugin-specific files: 101 | 102 | # IntelliJ 103 | /out/ 104 | 105 | # mpeltonen/sbt-idea plugin 106 | .idea_modules/ 107 | 108 | # JIRA plugin 109 | atlassian-ide-plugin.xml 110 | 111 | # Cursive Clojure plugin 112 | .idea/replstate.xml 113 | 114 | # Crashlytics plugin (for Android Studio and IntelliJ) 115 | com_crashlytics_export_strings.xml 116 | crashlytics.properties 117 | crashlytics-build.properties 118 | fabric.properties 119 | 120 | ### WebStorm Patch ### 121 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 122 | 123 | # *.iml 124 | # modules.xml 125 | # .idea/misc.xml 126 | # *.ipr 127 | 128 | # Sonarlint plugin 129 | .idea/sonarlint 130 | 131 | # End of https://www.gitignore.io/api/webstorm 132 | 133 | # Created by https://www.gitignore.io/api/visualstudiocode 134 | 135 | ### VisualStudioCode ### 136 | .vscode/* 137 | !.vscode/settings.json 138 | !.vscode/tasks.json 139 | !.vscode/launch.json 140 | !.vscode/extensions.json 141 | 142 | # End of https://www.gitignore.io/api/visualstudiocode 143 | 144 | # Created by https://www.gitignore.io/api/linux 145 | 146 | ### Linux ### 147 | *~ 148 | 149 | # temporary files which can be created if a process still has a handle open of a deleted file 150 | .fuse_hidden* 151 | 152 | # KDE directory preferences 153 | .directory 154 | 155 | # Linux trash folder which might appear on any partition or disk 156 | .Trash-* 157 | 158 | # .nfs files are created when an open file is removed but is still being accessed 159 | .nfs* 160 | 161 | # End of https://www.gitignore.io/api/linux 162 | 163 | # Created by https://www.gitignore.io/api/windows 164 | 165 | ### Windows ### 166 | # Windows thumbnail cache files 167 | Thumbs.db 168 | ehthumbs.db 169 | ehthumbs_vista.db 170 | 171 | # Folder config file 172 | Desktop.ini 173 | 174 | # Recycle Bin used on file shares 175 | $RECYCLE.BIN/ 176 | 177 | # Windows Installer files 178 | *.cab 179 | *.msi 180 | *.msm 181 | *.msp 182 | 183 | # Windows shortcuts 184 | *.lnk 185 | 186 | # End of https://www.gitignore.io/api/windows 187 | 188 | # Created by https://www.gitignore.io/api/macos 189 | 190 | ### macOS ### 191 | *.DS_Store 192 | .AppleDouble 193 | .LSOverride 194 | 195 | # Icon must end with two \r 196 | Icon 197 | 198 | # Thumbnails 199 | ._* 200 | 201 | # Files that might appear in the root of a volume 202 | .DocumentRevisions-V100 203 | .fseventsd 204 | .Spotlight-V100 205 | .TemporaryItems 206 | .Trashes 207 | .VolumeIcon.icns 208 | .com.apple.timemachine.donotpresent 209 | 210 | # Directories potentially created on remote AFP share 211 | .AppleDB 212 | .AppleDesktop 213 | Network Trash Folder 214 | Temporary Items 215 | .apdisk 216 | 217 | # End of https://www.gitignore.io/api/macos 218 | 219 | .vscode/* 220 | lib 221 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "7" 5 | - "6" 6 | 7 | script: npm test 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | eslint-plugin-codebox 2 | === 3 | 4 | This is a plugin we use at CodeBox for some analysis rules and fixers that are missing in other plugins, namely 5 | 6 | __codebox/sort-imports__ (fixable) - allows to sort imports in a variety of ways: 7 | 8 | 1. Group imports by them being: 9 | * Node.js stdlib imports 10 | * External imports (imports from node_modules) 11 | * Imports from parent directory (i.e. ../../) 12 | * Sibling imports (./) 13 | * Imports of index file (./index', ./index.js) 14 | * Absolute imports (/root/whatever/somefile) 15 | * Undetected type imports (happens pretty rarely, but still) 16 | 2. Group imports inside first group by them being: 17 | * Named imports (e.g. `import { foo } from 'bar'`) 18 | * Default imports (e.g. `import React from 'react'`) 19 | * Imports of the whole namespace (e.g. `imports * as foo from 'bar'`) 20 | * Imports, where none of the elements is added to namespace (e.g. `import './foo'`) 21 | 3. And finally, sort the imports alphabetically 22 | 23 | Moreover, this rule is fixable, which means, that ESLint will reorder your imports when you run `eslint --fix` automagically. Use this rule with caution, as it may break your code if it depends on the order of imports (tip: it should not) 24 | 25 | __codebox/sort-named-imports__ (fixable) - allows to sort components inside named imports alphabetically, i.e. 26 | 27 | Incorrect code for this rule: 28 | ```js 29 | import { b, a } from 'foo' 30 | ``` 31 | 32 | Correct code: 33 | ```js 34 | import { a, b } from 'foo' 35 | ``` 36 | 37 | WARNING: 38 | === 39 | This plugin is still in a very early stage of development. More features will be added in the upcoming time, if you still wish to use it: 40 | 41 | ``` 42 | npm install --saveDev eslint-plugin-codebox 43 | ``` 44 | 45 | Add to plugins sections of your .eslintrc: 46 | 47 | ```json 48 | "plugins": [ 49 | "codebox" 50 | ] 51 | ``` 52 | 53 | Configure the rules you want: 54 | ```js 55 | module.exports = { 56 | "rules": { 57 | "codebox/sort-imports": ["error", { 58 | "groups": [ 59 | "builtin", // builtin dependencies go first 60 | "external", // then external dependencies 61 | "parent", // then parent 62 | "sibling", // ...ok, you got it 63 | "index", 64 | ["unknown", "absolute"] // An array inside array of groups means that these two groups share same priority for sorting 65 | ], 66 | "importTypes": [ 67 | "default", // Default imports are at top of each group 68 | "named", // After that - named imports 69 | "all", // Imports of the whole namespace 70 | "none" // Plain import 71 | ], 72 | "ignoreCase": true // Indicates whether we want to ignore case during alphabetical sorting 73 | }], 74 | "codebox/sort-named-imports": ["error", { 75 | "ignoreCase": true // Indicates whether we want to ignore case during alphabetical sorting 76 | }] 77 | } 78 | } 79 | ``` 80 | -------------------------------------------------------------------------------- /__test__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.eslintrc", 3 | "env": { 4 | "jest": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /__test__/rules/sort-imports.spec.js: -------------------------------------------------------------------------------- 1 | import ESLint from 'eslint' 2 | 3 | const sortImportsRule = require('../../src/rules/sort-imports') 4 | 5 | const ruleTester = new ESLint.RuleTester({ 6 | parser: 'babel-eslint', 7 | parserOptions: { 8 | ecmaVersion: 8, 9 | sourceType: 'module', 10 | }, 11 | }) 12 | 13 | ruleTester.run('codebox/sort-imports', sortImportsRule, { 14 | valid: [ 15 | { 16 | code: ` 17 | import fs from 'fs' 18 | import os from 'os' 19 | import redux from 'redux' 20 | a = 'c' 21 | `, 22 | options: [ 23 | { 24 | importTypes: ['default', 'named', 'all', 'none'], 25 | groups: ['builtin', 'external', 'parent', 'sibling', ['index', 'unknown', 'absolute']], 26 | ignoreCase: true, 27 | }, 28 | ], 29 | }, 30 | { 31 | code: ` 32 | import fs from 'fs' 33 | import os from 'os' 34 | import redux from 'redux' 35 | import zxc from 'zxc' 36 | `, 37 | options: [ 38 | { 39 | importTypes: ['default', 'named', 'all', 'none'], 40 | groups: ['builtin', 'external', 'parent', 'sibling', ['index', 'unknown', 'absolute']], 41 | ignoreCase: true, 42 | }, 43 | ], 44 | }, 45 | ], 46 | invalid: [ 47 | { 48 | code: ` 49 | import DevTools from './modules/common/components/DevTools' 50 | import MainPage from './modules/main/MainPage' 51 | import React from 'react' 52 | import { BrowserRouter, Route } from 'react-router-dom' 53 | import { Provider } from 'react-redux' 54 | `, 55 | errors: [ 56 | { 57 | message: `Imports './modules/common/components/DevTools' and 'react' should be swapped`, 58 | }, 59 | { message: `Imports './modules/main/MainPage' and 'react-router-dom' should be swapped` }, 60 | { message: `Imports 'react' and 'react-redux' should be swapped` }, 61 | ], 62 | output: ` 63 | import React from 'react' 64 | import { BrowserRouter, Route } from 'react-router-dom' 65 | import { Provider } from 'react-redux' 66 | import DevTools from './modules/common/components/DevTools' 67 | import MainPage from './modules/main/MainPage' 68 | `, 69 | options: [ 70 | { 71 | importTypes: ['default', 'named', 'all', 'none'], 72 | groups: ['builtin', 'external', 'parent', 'sibling', ['index', 'unknown', 'absolute']], 73 | ignoreCase: true, 74 | }, 75 | ], 76 | }, 77 | { 78 | code: ` 79 | import redux from 'redux' 80 | const a = 'b' 81 | fs.readFileSync('etcetera') 82 | import os from 'os' 83 | a() 84 | import fs from 'fs' 85 | b = 'c' 86 | `, 87 | errors: [{ message: `Imports 'redux' and 'fs' should be swapped` }], 88 | options: [ 89 | { 90 | importTypes: ['default', 'named', 'all', 'none'], 91 | groups: ['builtin', 'external', 'parent', 'sibling', ['index', 'unknown', 'absolute']], 92 | ignoreCase: true, 93 | }, 94 | ], 95 | output: ` 96 | import fs from 'fs' 97 | const a = 'b' 98 | fs.readFileSync('etcetera') 99 | import os from 'os' 100 | a() 101 | import redux from 'redux' 102 | b = 'c' 103 | `, 104 | }, 105 | { 106 | code: ` 107 | import os from 'os' 108 | import redux from 'redux' 109 | import fs from 'fs' 110 | `, 111 | errors: [ 112 | { message: `Imports 'os' and 'fs' should be swapped` }, 113 | { message: `Imports 'fs' and 'redux' should be swapped` }, 114 | ], 115 | options: [ 116 | { 117 | importTypes: ['default', 'named', 'all', 'none'], 118 | groups: ['builtin', 'external', 'parent', 'sibling', ['index', 'unknown', 'absolute']], 119 | ignoreCase: true, 120 | }, 121 | ], 122 | output: ` 123 | import fs from 'fs' 124 | import os from 'os' 125 | import redux from 'redux' 126 | `, 127 | }, 128 | { 129 | code: ` 130 | import os from 'os' 131 | import zxc from 'zxc' 132 | import redux from 'redux' 133 | import fs from 'fs' 134 | `, 135 | errors: [ 136 | { message: `Imports 'os' and 'fs' should be swapped` }, 137 | { message: `Imports 'fs' and 'zxc' should be swapped` }, 138 | ], 139 | options: [ 140 | { 141 | importTypes: ['default', 'named', 'all', 'none'], 142 | groups: ['builtin', 'external', 'parent', 'sibling', ['index', 'unknown', 'absolute']], 143 | ignoreCase: true, 144 | }, 145 | ], 146 | }, 147 | ], 148 | }) 149 | -------------------------------------------------------------------------------- /__test__/rules/sort-named-imports.spec.js: -------------------------------------------------------------------------------- 1 | import ESLint from 'eslint' 2 | 3 | const sortNamedImportsRule = require('../../src/rules/sort-named-imports') 4 | 5 | const ruleTester = new ESLint.RuleTester({ 6 | parser: 'babel-eslint', 7 | parserOptions: { 8 | ecmaVersion: 8, 9 | sourceType: 'module', 10 | }, 11 | }) 12 | 13 | ruleTester.run('codebox/sort-named-imports', sortNamedImportsRule, { 14 | valid: [ 15 | { 16 | code: ` 17 | import { a, b, c } from 'fs' 18 | `, 19 | options: [{ ignoreCase: true }], 20 | }, 21 | ], 22 | invalid: [ 23 | { 24 | code: ` 25 | import { b, a, c } from 'fs' 26 | `, 27 | errors: [{ message: `Member 'a' of the import declaration should be sorted alphabetically` }], 28 | options: [{ ignoreCase: true }], 29 | output: ` 30 | import { a, b, c } from 'fs' 31 | `, 32 | }, 33 | ], 34 | }) 35 | -------------------------------------------------------------------------------- /__test__/utils/TranspositionManager.spec.js: -------------------------------------------------------------------------------- 1 | import TranspositionManager from '../../src/utils/TranspositionManager' 2 | 3 | describe('TranspositionManager', () => { 4 | const transpositionTest = [ 5 | { shouldBe: 4, currentIdx: 0 }, 6 | { shouldBe: 0, currentIdx: 1 }, 7 | { shouldBe: 2, currentIdx: 2 }, 8 | { shouldBe: 1, currentIdx: 3 }, 9 | { shouldBe: 3, currentIdx: 4 }, 10 | ] 11 | describe('constructor', () => { 12 | it('creates TranspositionManager instance', () => { 13 | expect(new TranspositionManager(transpositionTest).constructor.name).toBe( 14 | 'TranspositionManager' 15 | ) 16 | }) 17 | }) 18 | 19 | describe('canTranspose()', () => { 20 | it('returns whether the next transposition is possible', () => { 21 | const tr = new TranspositionManager(transpositionTest) 22 | for (let i = 0; i < 4; i += 1) { 23 | expect(tr.canTranspose()).toBe(true) 24 | tr.transpose() 25 | } 26 | expect(tr.canTranspose()).toBe(false) 27 | }) 28 | }) 29 | 30 | describe('transpose()', () => { 31 | it('returns next transposition in the chain of transposition leading to correct result', () => { 32 | const tr = new TranspositionManager(transpositionTest) 33 | expect(tr.transpose()).toEqual([ 34 | { currentIdx: 4, shouldBe: 3 }, 35 | { currentIdx: 3, shouldBe: 1 }, 36 | ]) 37 | expect(tr.transpose()).toEqual([ 38 | { currentIdx: 3, shouldBe: 1 }, 39 | { currentIdx: 1, shouldBe: 0 }, 40 | ]) 41 | expect(tr.transpose()).toEqual([ 42 | { currentIdx: 1, shouldBe: 0 }, 43 | { currentIdx: 0, shouldBe: 4 }, 44 | ]) 45 | expect(tr.transpose()).toEqual([ 46 | { currentIdx: 1, shouldBe: 0 }, 47 | { currentIdx: 1, shouldBe: 0 }, 48 | ]) 49 | }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /__test__/utils/getImportKind.spec.js: -------------------------------------------------------------------------------- 1 | import getImportKind from '../../src/utils/getImportKind' 2 | 3 | describe('getImportKind()', () => { 4 | it(`returns 'none' for empty imports`, () => { 5 | expect(getImportKind({ specifiers: [] })).toBe('none') 6 | }) 7 | 8 | it(`returns 'default' for default imports`, () => { 9 | expect( 10 | getImportKind({ 11 | specifiers: [{ type: 'ImportDefaultSpecifier' }], 12 | }) 13 | ).toBe('default') 14 | }) 15 | 16 | it(`returns 'all' for import * as namespace syntax`, () => { 17 | expect( 18 | getImportKind({ 19 | specifiers: [{ type: 'ImportNamespaceSpecifier' }], 20 | }) 21 | ).toBe('all') 22 | }) 23 | 24 | it(`returns 'named' for namespace imports`, () => { 25 | expect( 26 | getImportKind({ 27 | specifiers: [{ type: 'Whatever' }], 28 | }) 29 | ).toBe('named') 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /__test__/utils/nodes.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | NodeClassifier, 3 | getFirstMemberName, 4 | alphabeticCompareNodes, 5 | compareNodes, 6 | } from '../../src/utils/nodes' 7 | 8 | describe('nodes', () => { 9 | describe('getFirstMemberName()', () => { 10 | it('returns null if structure given is not a correct import node', () => { 11 | expect(getFirstMemberName({ node: null })).toBe(null) 12 | expect(getFirstMemberName({ node: { specifiers: [] } })).toBe(null) 13 | }) 14 | 15 | it('returns name of first member of import', () => { 16 | expect(getFirstMemberName({ node: { specifiers: [{ local: { name: 'redux' } }] } })).toBe( 17 | 'redux' 18 | ) 19 | }) 20 | 21 | it('lowercases returned name if forceLowercase is true', () => { 22 | expect( 23 | getFirstMemberName({ node: { specifiers: [{ local: { name: 'Redux' } }] } }, true) 24 | ).toBe('redux') 25 | }) 26 | }) 27 | 28 | describe('alphabeticCompareNodes(node1, node2, ignoreCase)', () => { 29 | const fsNode = { 30 | node: { 31 | specifiers: [{ local: { name: 'fs' } }], 32 | }, 33 | } 34 | const osNode = { 35 | node: { 36 | specifiers: [{ local: { name: 'os' } }], 37 | }, 38 | } 39 | 40 | expect(alphabeticCompareNodes(fsNode, osNode, true)).toBe(-1) 41 | expect(alphabeticCompareNodes(fsNode, fsNode, true)).toBe(0) 42 | expect(alphabeticCompareNodes(osNode, osNode, true)).toBe(0) 43 | expect(alphabeticCompareNodes(osNode, fsNode, true)).toBe(1) 44 | }) 45 | 46 | describe('compareNodes()', () => {}) 47 | }) 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-codebox", 3 | "version": "2.0.2", 4 | "description": "A set of ESLint rules we use at Codebox", 5 | "main": "./lib/index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "babel src -d lib", 9 | "prepublish": "npm run build" 10 | }, 11 | "repository": "asn007/eslint-plugin-codebox", 12 | "keywords": [ 13 | "codebox", 14 | "eslint", 15 | "eslint-plugin", 16 | "rules" 17 | ], 18 | "directories": { 19 | "test": "__test__" 20 | }, 21 | "files": [ 22 | "lib", 23 | "src" 24 | ], 25 | "author": { 26 | "name": "Andrey Sinitsyn", 27 | "email": "andrey.sin98@gmail.com", 28 | "url": "https://andrey.space" 29 | }, 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/asn007/eslint-plugin-codebox/issues" 33 | }, 34 | "homepage": "https://github.com/asn007/eslint-plugin-codebox#readme", 35 | "devDependencies": { 36 | "@babel/cli": "^7.0.0-beta.36", 37 | "@babel/core": "^7.0.0-beta.36", 38 | "@babel/preset-env": "^7.0.0-beta.36", 39 | "babel-core": "^7.0.0-bridge.0", 40 | "babel-eslint": "^8.1.2", 41 | "babel-jest": "^22.0.4", 42 | "eslint": "^4.14.0", 43 | "eslint-config-airbnb": "^16.1.0", 44 | "eslint-config-prettier": "^2.9.0", 45 | "eslint-plugin-import": "^2.8.0", 46 | "eslint-plugin-jsx-a11y": "^6.0.3", 47 | "eslint-plugin-prettier": "^2.4.0", 48 | "eslint-plugin-react": "^7.5.1", 49 | "jest": "^22.0.4", 50 | "prettier": "^1.9.2" 51 | }, 52 | "peerDependencies": { 53 | "eslint": "^3.0.0 || ^4.0.0" 54 | }, 55 | "dependencies": { 56 | "builtin-modules": "^2.0.0", 57 | "eslint-module-utils": "^2.1.1", 58 | "jest-cli": "^22.0.4", 59 | "lodash": "^4.17.4" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export const rules = { 2 | 'sort-imports': require('./rules/sort-imports'), // eslint-disable-line 3 | 'sort-named-imports': require('./rules/sort-named-imports'), // eslint-disable-line 4 | } 5 | 6 | export const configs = { 7 | recommended: { 8 | rules: { 9 | 'codebox/sort-imports': 'error', 10 | 'codebox/sort-named-imports': [ 11 | 'error', 12 | { 13 | importTypes: ['default', 'named', 'all', 'none'], 14 | groups: ['builtin', 'external', 'parent', 'sibling', ['index', 'unknown', 'absolute']], 15 | ignoreCase: true, 16 | }, 17 | ], 18 | }, 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /src/rules/sort-imports.js: -------------------------------------------------------------------------------- 1 | import { NodeClassifier, compareNodes } from '../utils/nodes' 2 | import isStaticRequire from '../utils/isStaticRequire' 3 | import TranspositionManager from '../utils/TranspositionManager' 4 | 5 | function rankImports(imports, ignoreCase) { 6 | return imports 7 | .map((imp, idx) => ({ ...imp, currentIdx: idx })) 8 | .sort((a, b) => compareNodes(a, b, ignoreCase)) 9 | .map((imp, idx) => ({ ...imp, shouldBe: idx })) 10 | .sort((a, b) => a.currentIdx - b.currentIdx) 11 | } 12 | 13 | function createImportFix(context, imported) { 14 | return fixer => { 15 | const transpositionManager = new TranspositionManager(imported) 16 | const transpositions = [] 17 | while (transpositionManager.canTranspose()) 18 | transpositions.push(transpositionManager.transpose()) 19 | const importRange = [imported[0].node.range[0], imported[imported.length - 1].node.range[1]] 20 | const sourceCode = context.getSourceCode() 21 | const importsIncludedSourceCut = sourceCode.text.slice(importRange[0], importRange[1]) 22 | return fixer.replaceTextRange( 23 | importRange, 24 | transpositions.reduce( 25 | (previous, [{ node: firstNode }, { node: secondNode }]) => 26 | previous 27 | .replace(sourceCode.getText(firstNode), sourceCode.getText(secondNode)) 28 | .replace(sourceCode.getText(secondNode), sourceCode.getText(firstNode)), 29 | importsIncludedSourceCut 30 | ) 31 | ) 32 | } 33 | } 34 | 35 | function reportOutOfOrder(context, imported, outOfOrder, ignoreCase) { 36 | const detectedOutOfOrderImports = outOfOrder.slice() 37 | const reports = [] 38 | const fix = createImportFix(context, rankImports(imported, ignoreCase)) 39 | outOfOrder.forEach(imp => { 40 | const found = detectedOutOfOrderImports.find(item => item.shouldBe === imp.currentIdx) 41 | if (!found) return 42 | detectedOutOfOrderImports.splice(detectedOutOfOrderImports.indexOf(found), 1) 43 | detectedOutOfOrderImports.splice(detectedOutOfOrderImports.indexOf(imp), 1) 44 | reports.push({ 45 | node: imp.node, 46 | message: `Imports '{{firstImport}}' and '{{secondImport}}' should be swapped`, 47 | data: { 48 | firstImport: imp.name, 49 | secondImport: found.name, 50 | }, 51 | }) 52 | }) 53 | return reports.forEach(report => context.report({ ...report, fix })) 54 | } 55 | 56 | function findAllOutOfOrder(imported, ignoreCase) { 57 | return rankImports(imported, ignoreCase).filter((item, idx) => item.shouldBe !== idx) 58 | } 59 | 60 | function reportIfNeeded(context, imported, ignoreCase) { 61 | const outOfOrder = findAllOutOfOrder(imported, ignoreCase) 62 | if (outOfOrder.length === 0) return 63 | reportOutOfOrder(context, imported, outOfOrder, ignoreCase) 64 | } 65 | 66 | module.exports = { 67 | meta: { 68 | docs: { 69 | description: 'Group imports and sort them alphabetically inside groups', 70 | category: 'ECMAScript 6', 71 | recommended: true, 72 | }, 73 | 74 | schema: [ 75 | { 76 | type: 'object', 77 | properties: { 78 | importTypes: { 79 | type: 'array', 80 | }, 81 | groups: { 82 | type: 'array', 83 | }, 84 | ignoreCase: { 85 | type: 'boolean', 86 | default: true, 87 | }, 88 | }, 89 | additionalProperties: false, 90 | }, 91 | ], 92 | 93 | fixable: 'code', 94 | }, 95 | 96 | create(context) { 97 | const options = context.options[0] || {} 98 | let classifier 99 | try { 100 | classifier = new NodeClassifier(context, options.groups, options.importTypes) 101 | } catch (error) { 102 | return { 103 | Program: node => context.report(node, error.message), 104 | } 105 | } 106 | 107 | let imported = [] 108 | let level = 0 109 | 110 | function incrementLevel() { 111 | level += 1 112 | } 113 | 114 | function decrementLevel() { 115 | level -= 1 116 | } 117 | 118 | function noop() {} 119 | 120 | return { 121 | ImportDeclaration(node) { 122 | imported.push(classifier.rankNode(node, node.source.value, 'import')) 123 | }, 124 | CallExpression(node) { 125 | if (level !== 0 || !isStaticRequire(node)) return 126 | noop() 127 | // TODO: handle require calls 128 | }, 129 | 'Program:exit': () => { 130 | reportIfNeeded(context, imported, options.ignoreCase || true) 131 | 132 | imported = [] 133 | }, 134 | FunctionDeclaration: incrementLevel, 135 | FunctionExpression: incrementLevel, 136 | ArrowFunctionExpression: incrementLevel, 137 | BlockStatement: incrementLevel, 138 | ObjectExpression: incrementLevel, 139 | 'FunctionDeclaration:exit': decrementLevel, 140 | 'FunctionExpression:exit': decrementLevel, 141 | 'ArrowFunctionExpression:exit': decrementLevel, 142 | 'BlockStatement:exit': decrementLevel, 143 | 'ObjectExpression:exit': decrementLevel, 144 | } 145 | }, 146 | } 147 | -------------------------------------------------------------------------------- /src/rules/sort-named-imports.js: -------------------------------------------------------------------------------- 1 | import getImportKind from '../utils/getImportKind'; 2 | module.exports = { 3 | meta: { 4 | docs: { 5 | description: 'enforce sorted members of named imports', 6 | category: 'ECMAScript 6', 7 | recommended: true, 8 | }, 9 | 10 | schema: [ 11 | { 12 | type: 'object', 13 | properties: { 14 | ignoreCase: { 15 | type: 'boolean', 16 | default: true, 17 | }, 18 | }, 19 | additionalProperties: false, 20 | }, 21 | ], 22 | 23 | fixable: 'code', 24 | }, 25 | 26 | create(context) { 27 | const sourceCode = context.getSourceCode() 28 | const config = context.options[0] || {} 29 | const ignoreCase = config.ignoreCase || true 30 | return { 31 | ImportDeclaration(node) { 32 | const importSpecifiers = node.specifiers.filter( 33 | specifier => specifier.type === 'ImportSpecifier' 34 | ) 35 | const getSortableName = ignoreCase 36 | ? specifier => specifier.local.name.toLowerCase() 37 | : specifier => specifier.local.name 38 | const firstUnsortedIndex = importSpecifiers 39 | .map(getSortableName) 40 | .findIndex((name, index, array) => array[index - 1] > name) 41 | 42 | if (firstUnsortedIndex !== -1) 43 | context.report({ 44 | node: importSpecifiers[firstUnsortedIndex], 45 | message: `Member '{{member}}' of the import declaration should be sorted alphabetically`, 46 | data: { 47 | member: importSpecifiers[firstUnsortedIndex].local.name, 48 | }, 49 | fix(fixer) { 50 | // Skip rearranging specifiers if there are comments 51 | if ( 52 | importSpecifiers.some( 53 | specifier => 54 | sourceCode.getCommentsBefore(specifier).length || 55 | sourceCode.getCommentsAfter(specifier).length 56 | ) 57 | ) 58 | return null 59 | 60 | return fixer.replaceTextRange( 61 | [ 62 | importSpecifiers[0].range[0], 63 | importSpecifiers[importSpecifiers.length - 1].range[1], 64 | ], 65 | importSpecifiers 66 | .slice() 67 | .sort( 68 | (first, second) => (getSortableName(first) > getSortableName(second) ? 1 : -1) 69 | ) 70 | .reduce((sourceText, specifier, index) => { 71 | const textAfterSpecifier = 72 | index === importSpecifiers.length - 1 73 | ? '' 74 | : sourceCode 75 | .getText() 76 | .slice( 77 | importSpecifiers[index].range[1], 78 | importSpecifiers[index + 1].range[0] 79 | ) 80 | 81 | return sourceText + sourceCode.getText(specifier) + textAfterSpecifier 82 | }, '') 83 | ) 84 | }, 85 | }) 86 | }, 87 | } 88 | }, 89 | } 90 | -------------------------------------------------------------------------------- /src/utils/TranspositionManager.js: -------------------------------------------------------------------------------- 1 | function swap(array, indexOne, indexTwo) { 2 | const a = array[indexOne] 3 | const b = array[indexTwo] 4 | array[indexTwo] = a // eslint-disable-line 5 | array[indexOne] = b // eslint-disable-line 6 | } 7 | 8 | export default class TranspositionManager { 9 | constructor(nodes) { 10 | this.nodes = nodes 11 | this.workOn = nodes.length - 1 12 | this.virtualTransposedState = nodes.slice() 13 | } 14 | 15 | canTranspose() { 16 | return this.workOn > 0 17 | } 18 | 19 | transpose() { 20 | let workNode = this.virtualTransposedState[this.workOn].shouldBe 21 | while (workNode === this.workOn && this.workOn > 0) 22 | workNode = this.virtualTransposedState[--this.workOn].shouldBe // eslint-disable-line 23 | swap(this.virtualTransposedState, this.workOn, workNode) 24 | const result = [this.virtualTransposedState[workNode], this.virtualTransposedState[this.workOn]] 25 | if (this.workOn < workNode) result.reverse() 26 | return result 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/getImportGroup.js: -------------------------------------------------------------------------------- 1 | import cond from 'lodash/cond' 2 | import builtinModules from 'builtin-modules' 3 | import { join } from 'path' 4 | 5 | import resolve from 'eslint-module-utils/resolve' 6 | 7 | function constant(value) { 8 | return () => value 9 | } 10 | 11 | function baseModule(name) { 12 | if (isScoped(name)) { 13 | const [scope, pkg] = name.split('/') 14 | return `${scope}/${pkg}` 15 | } 16 | const [pkg] = name.split('/') 17 | return pkg 18 | } 19 | 20 | export function isAbsolute(name) { 21 | return name.indexOf('/') === 0 22 | } 23 | 24 | export function isBuiltIn(name, settings) { 25 | const base = baseModule(name) 26 | const extras = (settings && settings['codebox/core-modules']) || [] 27 | return builtinModules.indexOf(base) !== -1 || extras.indexOf(base) > -1 28 | } 29 | 30 | function isExternalPath(path, name, settings) { 31 | const folders = (settings && settings['codebox/external-module-folders']) || ['node_modules'] 32 | return !path || folders.some(folder => path.indexOf(join(folder, name)) > -1) 33 | } 34 | 35 | const externalModuleRegExp = /^\w/ 36 | function isExternalModule(name, settings, path) { 37 | return externalModuleRegExp.test(name) && isExternalPath(path, name, settings) 38 | } 39 | 40 | const externalModuleMainRegExp = /^[\w]((?!\/).)*$/ 41 | export function isExternalModuleMain(name, settings, path) { 42 | return externalModuleMainRegExp.test(name) && isExternalPath(path, name, settings) 43 | } 44 | 45 | const scopedRegExp = /^@[^/]+\/[^/]+/ 46 | function isScoped(name) { 47 | return scopedRegExp.test(name) 48 | } 49 | 50 | const scopedMainRegExp = /^@[^/]+\/?[^/]+$/ 51 | export function isScopedMain(name) { 52 | return scopedMainRegExp.test(name) 53 | } 54 | 55 | function isInternalModule(name, settings, path) { 56 | return externalModuleRegExp.test(name) && !isExternalPath(path, name, settings) 57 | } 58 | 59 | function isRelativeToParent(name) { 60 | return name.indexOf('../') === 0 61 | } 62 | 63 | const indexFiles = ['.', './', './index', './index.js'] 64 | function isIndex(name) { 65 | return indexFiles.indexOf(name) !== -1 66 | } 67 | 68 | function isRelativeToSibling(name) { 69 | return name.indexOf('./') === 0 70 | } 71 | 72 | const typeTest = cond([ 73 | [isAbsolute, constant('absolute')], 74 | [isBuiltIn, constant('builtin')], 75 | [isExternalModule, constant('external')], 76 | [isScoped, constant('external')], 77 | [isInternalModule, constant('internal')], 78 | [isRelativeToParent, constant('parent')], 79 | [isIndex, constant('index')], 80 | [isRelativeToSibling, constant('sibling')], 81 | [constant(true), constant('unknown')], 82 | ]) 83 | 84 | export default function getImportGroup(name, context) { 85 | return typeTest(name, context.settings, resolve(name, context)) 86 | } 87 | -------------------------------------------------------------------------------- /src/utils/getImportKind.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gets the used member syntax style. 3 | * 4 | * import 'my-module.js' --> none 5 | * import * as myModule from 'my-module.js' --> all 6 | * import {myMember} from 'my-module.js' --> named 7 | * import {foo, bar} from 'my-module.js' --> named 8 | * import FooBar from 'my-module.js' 9 | * @param {ASTNode} node - the ImportDeclaration node. 10 | * @returns {string} used member parameter style, ["all", "named", "default", "none"] 11 | */ 12 | export default function getImportKind(node) { 13 | const { specifiers } = node 14 | if (!specifiers.length) return 'none' 15 | else if (specifiers[0].type === 'ImportNamespaceSpecifier') return 'all' 16 | else if (specifiers[0].type === 'ImportDefaultSpecifier') return 'default' 17 | return 'named' 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/isStaticRequire.js: -------------------------------------------------------------------------------- 1 | export default function isStaticRequire(node) { 2 | return ( 3 | node && 4 | node.callee && 5 | node.callee.type === 'Identifier' && 6 | node.callee.name === 'require' && 7 | node.arguments.length === 1 && 8 | node.arguments[0].type === 'Literal' && 9 | typeof node.arguments[0].value === 'string' 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/nodes.js: -------------------------------------------------------------------------------- 1 | import getImportGroup from './getImportGroup' 2 | import getImportKind from './getImportKind' 3 | 4 | const importGroups = ['builtin', 'external', 'parent', 'sibling', 'index', 'unknown', 'absolute'] 5 | const importKinds = ['named', 'default', 'all', 'none'] 6 | 7 | const lower = s => s && s.toLowerCase() 8 | 9 | export function getFirstMemberName({ node }, forceLowercase) { 10 | const value = node && node.specifiers && node.specifiers[0] ? node.specifiers[0].local.name : null 11 | return forceLowercase ? lower(value) : value 12 | } 13 | 14 | export function alphabeticCompareNodes(first, second, ignoreCase) { 15 | const firstName = getFirstMemberName(first, ignoreCase) 16 | const secondName = getFirstMemberName(second, ignoreCase) 17 | if (firstName === secondName) return 0 18 | return firstName > secondName ? 1 : -1 19 | } 20 | 21 | export function compareNodes(first, second, ignoreCase) { 22 | if (first.groupRank > second.groupRank) return 1 23 | else if (first.groupRank < second.groupRank) return -1 24 | else if (first.kindRank > second.kindRank) return 1 25 | else if (first.kindRank < second.kindRank) return -1 26 | return alphabeticCompareNodes(first, second, ignoreCase) 27 | } 28 | 29 | function buildRank(groups, groupEnum) { 30 | const rankObject = groups.reduce((res, group, idx) => { 31 | if (typeof group === 'string') group = [ group ] // eslint-disable-line 32 | group.forEach(groupItem => { 33 | if (groupEnum.indexOf(groupItem) === -1) 34 | throw new Error( 35 | `Incorrect configuration of the rule: unknown type ${JSON.stringify(groupItem)}` 36 | ) 37 | 38 | if (typeof res[groupItem] !== 'undefined') 39 | throw new Error( 40 | `Incorrect configuration of the rule: duplicate type ${JSON.stringify(groupItem)}` 41 | ) 42 | 43 | res[groupItem] = idx 44 | }) 45 | return res 46 | }, {}) 47 | 48 | const omittedGroups = groupEnum.filter(group => rankObject[group] === undefined) 49 | 50 | return omittedGroups.reduce((res, group) => { 51 | res[group] = groupEnum.length 52 | return res 53 | }, rankObject) 54 | } 55 | 56 | export class NodeClassifier { 57 | constructor(context, groups = importGroups, importKindGroups = importKinds) { 58 | this.context = context 59 | this.groups = groups 60 | this.importKindGroups = importKindGroups 61 | this._buildRanks() 62 | } 63 | 64 | _buildRanks() { 65 | this.groupRank = buildRank(this.groups, importGroups) 66 | this.kindRank = buildRank(this.importKindGroups, importKinds) 67 | } 68 | 69 | rankNode(node, name, type) { 70 | return { 71 | node, 72 | name, 73 | groupRank: this.groupRank[getImportGroup(name, this.context)] + (type === 'import' ? 0 : 100), 74 | kindRank: this.kindRank[getImportKind(node)] + (type === 'import' ? 0 : 100), 75 | } 76 | } 77 | } 78 | --------------------------------------------------------------------------------