Skip to content

Commit 055201e

Browse files
committed
Merge pull request #247 from jfmengels/import-order
Add `order` rule
2 parents 9e5ea9b + 38f3935 commit 055201e

File tree

10 files changed

+715
-25
lines changed

10 files changed

+715
-25
lines changed

CHANGELOG.md

+1-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
99
- add [`no-extraneous-dependencies`] rule ([#241], thanks [@jfmengels])
1010
- add [`extensions`] rule ([#250], thanks [@lo1tuma])
1111
- add [`no-nodejs-modules`] rule ([#261], thanks [@jfmengels])
12+
- add [`order`] rule ([#247], thanks [@jfmengels])
1213
- consider `resolve.fallback` config option in the webpack resolver ([#254])
1314

1415
### Changed
@@ -155,9 +156,6 @@ for info on changes for earlier releases.
155156
[`imports-first`]: ./docs/rules/imports-first.md
156157
[`no-nodejs-modules`]: ./docs/rules/no-nodejs-modules.md
157158

158-
[#256]: https://github.com/benmosher/eslint-plugin-import/pull/256
159-
[#254]: https://github.com/benmosher/eslint-plugin-import/pull/254
160-
[#250]: https://github.com/benmosher/eslint-plugin-import/pull/250
161159
[#243]: https://github.com/benmosher/eslint-plugin-import/pull/243
162160
[#241]: https://github.com/benmosher/eslint-plugin-import/pull/241
163161
[#239]: https://github.com/benmosher/eslint-plugin-import/pull/239
@@ -198,5 +196,3 @@ for info on changes for earlier releases.
198196
[@singles]: https://github.com/singles
199197
[@jfmengels]: https://github.com/jfmengels
200198
[@dmnd]: https://github.com/dmnd
201-
[@lo1tuma]: https://github.com/lo1tuma
202-
[@lemonmade]: https://github.com/lemonmade

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,13 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
5353
* Report repeated import of the same module in multiple places ([`no-duplicates`])
5454
* Report namespace imports ([`no-namespace`])
5555
* Ensure consistent use of file extension within the import path ([`extensions`])
56+
* Enforce a convention in module import order ([`order`])
5657

5758
[`imports-first`]: ./docs/rules/imports-first.md
5859
[`no-duplicates`]: ./docs/rules/no-duplicates.md
5960
[`no-namespace`]: ./docs/rules/no-namespace.md
6061
[`extensions`]: ./docs/rules/extensions.md
62+
[`order`]: ./docs/rules/order.md
6163

6264

6365
## Installation

docs/rules/order.md

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Enforce a convention in module import order
2+
3+
Enforce a convention in the order of `require()` / `import` statements. The order is as shown in the following example:
4+
5+
```js
6+
// 1. node "builtins"
7+
import fs from 'fs';
8+
import path from 'path';
9+
// 2. "external" modules
10+
import _ from 'lodash';
11+
import chalk from 'chalk';
12+
// 3. "internal" modules
13+
// (if you have configured your path or webpack to handle your internal paths differently)
14+
import foo from 'src/foo';
15+
// 4. modules from a "parent" directory
16+
import foo from '../foo';
17+
import qux from '../../foo/qux';
18+
// 5. "sibling" modules from the same or a sibling's directory
19+
import bar from './bar';
20+
import baz from './bar/baz';
21+
// 6. "index" of the current directory
22+
import main from './';
23+
```
24+
25+
Unassigned imports are ignored, as the order they are imported in may be important.
26+
27+
Statements using the ES6 `import` syntax must appear before any `require()` statements.
28+
29+
30+
## Fail
31+
32+
```js
33+
import _ from 'lodash';
34+
import path from 'path'; // `path` import should occur before import of `lodash`
35+
36+
// -----
37+
38+
var _ = require('lodash');
39+
var path = require('path'); // `path` import should occur before import of `lodash`
40+
41+
// -----
42+
43+
var path = require('path');
44+
import foo from './foo'; // `import` statements must be before `require` statement
45+
```
46+
47+
48+
## Pass
49+
50+
```js
51+
import path from 'path';
52+
import _ from 'lodash';
53+
54+
// -----
55+
56+
var path = require('path');
57+
var _ = require('lodash');
58+
59+
// -----
60+
61+
// Allowed as ̀`babel-register` is not assigned.
62+
require('babel-register');
63+
var path = require('path');
64+
65+
// -----
66+
67+
// Allowed as `import` must be before `require`
68+
import foo from './foo';
69+
var path = require('path');
70+
```
71+
72+
## Options
73+
74+
This rule supports the following options:
75+
76+
`groups`: How groups are defined, and the order to respect. `groups` must be an array of `string` or [`string`]. The only allowed `string`s are: `"builtin"`, `"external"`, `"internal"`, `"parent"`, `"sibling"`, `"index"`. The enforced order is the same as the order of each element in a group. Omitted types are implicitly grouped together as the last element. Example:
77+
```js
78+
[
79+
'builtin', // Built-in types are first
80+
['sibling', 'parent'], // Then sibling and parent types. They can be mingled together
81+
'index', // Then the index file
82+
// Then the rest: internal and external type
83+
]
84+
```
85+
The default value is `["builtin", "external", "internal", "parent", "sibling", "index"]`.
86+
87+
You can set the options like this:
88+
89+
```js
90+
"import/order": ["error", {"groups": ["index", "sibling", "parent", "internal", "external", "builtin"]}]
91+
```

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"eslint-import-resolver-node": "^0.2.0",
7474
"lodash.cond": "^4.3.0",
7575
"lodash.endswith": "^4.0.1",
76+
"lodash.find": "^4.3.0",
7677
"object-assign": "^4.0.1",
7778
"pkg-up": "^1.0.0"
7879
}

src/core/importType.js

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import cond from 'lodash.cond'
22
import builtinModules from 'builtin-modules'
3-
import { basename, join } from 'path'
3+
import { join } from 'path'
44

55
import resolve from './resolve'
66

@@ -18,7 +18,12 @@ function isExternalModule(name, path) {
1818
return (!path || -1 < path.indexOf(join('node_modules', name)))
1919
}
2020

21-
function isProjectModule(name, path) {
21+
const scopedRegExp = /^@\w+\/\w+/
22+
function isScoped(name) {
23+
return scopedRegExp.test(name)
24+
}
25+
26+
function isInternalModule(name, path) {
2227
if (!externalModuleRegExp.test(name)) return false
2328
return (path && -1 === path.indexOf(join('node_modules', name)))
2429
}
@@ -28,8 +33,7 @@ function isRelativeToParent(name) {
2833
}
2934

3035
const indexFiles = ['.', './', './index', './index.js']
31-
function isIndex(name, path) {
32-
if (path) return basename(path).split('.')[0] === 'index'
36+
function isIndex(name) {
3337
return indexFiles.indexOf(name) !== -1
3438
}
3539

@@ -40,7 +44,8 @@ function isRelativeToSibling(name) {
4044
const typeTest = cond([
4145
[isBuiltIn, constant('builtin')],
4246
[isExternalModule, constant('external')],
43-
[isProjectModule, constant('project')],
47+
[isScoped, constant('external')],
48+
[isInternalModule, constant('internal')],
4449
[isRelativeToParent, constant('parent')],
4550
[isIndex, constant('index')],
4651
[isRelativeToSibling, constant('sibling')],

src/core/staticRequire.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// todo: merge with module visitor
22
export default function isStaticRequire(node) {
33
return node &&
4+
node.callee &&
45
node.callee.type === 'Identifier' &&
56
node.callee.name === 'require' &&
67
node.arguments.length === 1 &&

src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const rules = {
1616
'imports-first': require('./rules/imports-first'),
1717
'no-extraneous-dependencies': require('./rules/no-extraneous-dependencies'),
1818
'no-nodejs-modules': require('./rules/no-nodejs-modules'),
19+
'order': require('./rules/order'),
1920

2021
// metadata-based
2122
'no-deprecated': require('./rules/no-deprecated'),

src/rules/order.js

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
'use strict'
2+
3+
import find from 'lodash.find'
4+
import importType from '../core/importType'
5+
import isStaticRequire from '../core/staticRequire'
6+
7+
const defaultGroups = ['builtin', 'external', 'parent', 'sibling', 'index']
8+
9+
// REPORTING
10+
11+
function reverse(array) {
12+
return array.map(function (v) {
13+
return {
14+
name: v.name,
15+
rank: -v.rank,
16+
node: v.node,
17+
}
18+
}).reverse()
19+
}
20+
21+
function findOutOfOrder(imported) {
22+
if (imported.length === 0) {
23+
return []
24+
}
25+
let maxSeenRankNode = imported[0]
26+
return imported.filter(function (importedModule) {
27+
const res = importedModule.rank < maxSeenRankNode.rank
28+
if (maxSeenRankNode.rank < importedModule.rank) {
29+
maxSeenRankNode = importedModule
30+
}
31+
return res
32+
})
33+
}
34+
35+
function report(context, imported, outOfOrder, order) {
36+
outOfOrder.forEach(function (imp) {
37+
const found = find(imported, function hasHigherRank(importedItem) {
38+
return importedItem.rank > imp.rank
39+
})
40+
context.report(imp.node, '`' + imp.name + '` import should occur ' + order +
41+
' import of `' + found.name + '`')
42+
})
43+
}
44+
45+
function makeReport(context, imported) {
46+
const outOfOrder = findOutOfOrder(imported)
47+
if (!outOfOrder.length) {
48+
return
49+
}
50+
// There are things to report. Try to minimize the number of reported errors.
51+
const reversedImported = reverse(imported)
52+
const reversedOrder = findOutOfOrder(reversedImported)
53+
if (reversedOrder.length < outOfOrder.length) {
54+
report(context, reversedImported, reversedOrder, 'after')
55+
return
56+
}
57+
report(context, imported, outOfOrder, 'before')
58+
}
59+
60+
// DETECTING
61+
62+
function computeRank(context, ranks, name, type) {
63+
return ranks[importType(name, context)] +
64+
(type === 'import' ? 0 : 100)
65+
}
66+
67+
function registerNode(context, node, name, type, ranks, imported) {
68+
const rank = computeRank(context, ranks, name, type)
69+
if (rank !== -1) {
70+
imported.push({name, rank, node})
71+
}
72+
}
73+
74+
function isInVariableDeclarator(node) {
75+
return node &&
76+
(node.type === 'VariableDeclarator' || isInVariableDeclarator(node.parent))
77+
}
78+
79+
const types = ['builtin', 'external', 'internal', 'parent', 'sibling', 'index']
80+
81+
// Creates an object with type-rank pairs.
82+
// Example: { index: 0, sibling: 1, parent: 1, external: 1, builtin: 2, internal: 2 }
83+
// Will throw an error if it contains a type that does not exist, or has a duplicate
84+
function convertGroupsToRanks(groups) {
85+
const rankObject = groups.reduce(function(res, group, index) {
86+
if (typeof group === 'string') {
87+
group = [group]
88+
}
89+
group.forEach(function(groupItem) {
90+
if (types.indexOf(groupItem) === -1) {
91+
throw new Error('Incorrect configuration of the rule: Unknown type `' +
92+
JSON.stringify(groupItem) + '`')
93+
}
94+
if (res[groupItem] !== undefined) {
95+
throw new Error('Incorrect configuration of the rule: `' + groupItem + '` is duplicated')
96+
}
97+
res[groupItem] = index
98+
})
99+
return res
100+
}, {})
101+
102+
const omittedTypes = types.filter(function(type) {
103+
return rankObject[type] === undefined
104+
})
105+
106+
return omittedTypes.reduce(function(res, type) {
107+
res[type] = groups.length
108+
return res
109+
}, rankObject)
110+
}
111+
112+
module.exports = function importOrderRule (context) {
113+
const options = context.options[0] || {}
114+
let ranks
115+
116+
try {
117+
ranks = convertGroupsToRanks(options.groups || defaultGroups)
118+
} catch (error) {
119+
// Malformed configuration
120+
return {
121+
Program: function(node) {
122+
context.report(node, error.message)
123+
},
124+
}
125+
}
126+
let imported = []
127+
let level = 0
128+
129+
function incrementLevel() {
130+
level++
131+
}
132+
function decrementLevel() {
133+
level--
134+
}
135+
136+
return {
137+
ImportDeclaration: function handleImports(node) {
138+
if (node.specifiers.length) { // Ignoring unassigned imports
139+
const name = node.source.value
140+
registerNode(context, node, name, 'import', ranks, imported)
141+
}
142+
},
143+
CallExpression: function handleRequires(node) {
144+
if (level !== 0 || !isStaticRequire(node) || !isInVariableDeclarator(node.parent)) {
145+
return
146+
}
147+
const name = node.arguments[0].value
148+
registerNode(context, node, name, 'require', ranks, imported)
149+
},
150+
'Program:exit': function reportAndReset() {
151+
makeReport(context, imported)
152+
imported = []
153+
},
154+
FunctionDeclaration: incrementLevel,
155+
FunctionExpression: incrementLevel,
156+
ArrowFunctionExpression: incrementLevel,
157+
BlockStatement: incrementLevel,
158+
'FunctionDeclaration:exit': decrementLevel,
159+
'FunctionExpression:exit': decrementLevel,
160+
'ArrowFunctionExpression:exit': decrementLevel,
161+
'BlockStatement:exit': decrementLevel,
162+
}
163+
}
164+
165+
module.exports.schema = [
166+
{
167+
type: 'object',
168+
properties: {
169+
groups: {
170+
type: 'array',
171+
},
172+
},
173+
additionalProperties: false,
174+
},
175+
]

0 commit comments

Comments
 (0)