Skip to content

Commit a8bf7a0

Browse files
committed
Add import-order rule
1 parent e5480b7 commit a8bf7a0

File tree

10 files changed

+484
-16
lines changed

10 files changed

+484
-16
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
77
### Added
88
- [`no-named-as-default-member`] to `warnings` canned config
99
- add [`no-extraneous-dependencies`] rule ([#241], thanks [@jfmengels])
10+
- add [`import-order`] rule ([#247], thanks [@jfmengels])
1011

1112
## [1.5.0] - 2016-04-18
1213
### Added

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,12 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
5050
* Ensure all imports appear before other statements ([`imports-first`])
5151
* Report repeated import of the same module in multiple places ([`no-duplicates`])
5252
* Report namespace imports ([`no-namespace`])
53+
* Enforce a convention in module import order ([`import-order`])
5354

5455
[`imports-first`]: ./docs/rules/imports-first.md
5556
[`no-duplicates`]: ./docs/rules/no-duplicates.md
5657
[`no-namespace`]: ./docs/rules/no-namespace.md
58+
[`import-order`]: ./docs/rules/import-order.md
5759

5860

5961
## Installation

docs/rules/import-order.md

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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. modules from a "parent" directory
13+
import foo from '../foo';
14+
import qux from '../../foo/qux';
15+
// 4. "sibling" modules from the same or a sibling's directory
16+
import bar from './bar';
17+
import baz from './bar/baz';
18+
// 5. "index" of the current directory
19+
import main from './';
20+
```
21+
22+
Unassigned imports are not accounted for, as the order they are imported in may be important.
23+
24+
25+
## Fail
26+
27+
```js
28+
import _ from 'lodash';
29+
import path from 'path'; // `path` import should occur before import of `lodash`
30+
31+
// -----
32+
33+
var _ = require('lodash');
34+
var path = require('path'); // `path` import should occur before import of `lodash`
35+
```
36+
37+
38+
## Pass
39+
40+
```js
41+
import path from 'path';
42+
import _ from 'lodash';
43+
44+
// -----
45+
46+
var path = require('path');
47+
var _ = require('lodash');
48+
49+
// -----
50+
51+
// Allowed as ̀`babel-register` is not assigned.
52+
require('babel-register');
53+
var path = require('path');
54+
```
55+
56+
## Options
57+
58+
This rule supports the following options:
59+
60+
`order`: The order to respect. It needs to contain only and all of the following elements: `"builtin", "external", "parent", "sibling", "index"`, which is the default value.
61+
62+
You can set the options like this:
63+
64+
```js
65+
"import-order/import-order": ["error", {"order": ["index", "sibling", "parent", "external", "builtin"]}]
66+
```

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"es6-symbol": "*",
7373
"eslint-import-resolver-node": "^0.2.0",
7474
"lodash.cond": "^4.3.0",
75+
"lodash.find": "^4.3.0",
7576
"object-assign": "^4.0.1",
7677
"pkg-up": "^1.0.0"
7778
}

src/core/importType.js

+2-3
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

@@ -28,8 +28,7 @@ function isRelativeToParent(name) {
2828
}
2929

3030
const indexFiles = ['.', './', './index', './index.js']
31-
function isIndex(name, path) {
32-
if (path) return basename(path).split('.')[0] === 'index'
31+
function isIndex(name) {
3332
return indexFiles.indexOf(name) !== -1
3433
}
3534

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
@@ -14,6 +14,7 @@ export const rules = {
1414
'no-duplicates': require('./rules/no-duplicates'),
1515
'imports-first': require('./rules/imports-first'),
1616
'no-extraneous-dependencies': require('./rules/no-extraneous-dependencies'),
17+
'import-order': require('./rules/import-order'),
1718

1819
// metadata-based
1920
'no-deprecated': require('./rules/no-deprecated'),

src/rules/import-order.js

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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 defaultOrder = ['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, order, name) {
63+
return order.indexOf(importType(name, context))
64+
}
65+
66+
function registerNode(context, node, name, order, imported) {
67+
const rank = computeRank(context, order, name)
68+
if (rank !== -1) {
69+
imported.push({name: name, rank: rank, node: node})
70+
}
71+
}
72+
73+
function isInVariableDeclarator(node) {
74+
return node &&
75+
(node.type === 'VariableDeclarator' || isInVariableDeclarator(node.parent))
76+
}
77+
78+
module.exports = function importOrderRule (context) {
79+
const options = context.options[0] || {}
80+
const order = options.order || defaultOrder
81+
let imported = []
82+
let level = 0
83+
84+
function incrementLevel() {
85+
level++
86+
}
87+
function decrementLevel() {
88+
level--
89+
}
90+
91+
return {
92+
ImportDeclaration: function handleImports(node) {
93+
if (node.specifiers.length) { // Ignoring unassigned imports
94+
const name = node.source.value
95+
registerNode(context, node, name, order, imported)
96+
}
97+
},
98+
CallExpression: function handleRequires(node) {
99+
if (level !== 0 || !isStaticRequire(node) || !isInVariableDeclarator(node.parent)) {
100+
return
101+
}
102+
const name = node.arguments[0].value
103+
registerNode(context, node, name, order, imported)
104+
},
105+
'Program:exit': function reportAndReset() {
106+
makeReport(context, imported)
107+
imported = []
108+
},
109+
FunctionDeclaration: incrementLevel,
110+
FunctionExpression: incrementLevel,
111+
ArrowFunctionExpression: incrementLevel,
112+
BlockStatement: incrementLevel,
113+
'FunctionDeclaration:exit': decrementLevel,
114+
'FunctionExpression:exit': decrementLevel,
115+
'ArrowFunctionExpression:exit': decrementLevel,
116+
'BlockStatement:exit': decrementLevel,
117+
}
118+
}
119+
120+
module.exports.schema = [
121+
{
122+
type: 'object',
123+
properties: {
124+
order: {
125+
type: 'array',
126+
uniqueItems: true,
127+
length: 5,
128+
items: {
129+
enum: defaultOrder,
130+
},
131+
},
132+
},
133+
additionalProperties: false,
134+
},
135+
]

tests/src/core/importType.js

+9-13
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,17 @@ describe('importType(name)', function () {
3636
it("should return 'sibling' for internal modules that are connected to one of the siblings", function() {
3737
expect(importType('./foo', context)).to.equal('sibling')
3838
expect(importType('./foo/bar', context)).to.equal('sibling')
39+
expect(importType('./importType', context)).to.equal('sibling')
40+
expect(importType('./importType/', context)).to.equal('sibling')
41+
expect(importType('./importType/index', context)).to.equal('sibling')
42+
expect(importType('./importType/index.js', context)).to.equal('sibling')
3943
})
4044

41-
describe("should return 'index' for sibling index file when", function() {
42-
it("resolves", function() {
43-
expect(importType('./importType', context)).to.equal('index')
44-
expect(importType('./importType/', context)).to.equal('index')
45-
expect(importType('./importType/index', context)).to.equal('index')
46-
expect(importType('./importType/index.js', context)).to.equal('index')
47-
})
48-
it("doesn't resolve", function() {
49-
expect(importType('.', context)).to.equal('index')
50-
expect(importType('./', context)).to.equal('index')
51-
expect(importType('./index', context)).to.equal('index')
52-
expect(importType('./index.js', context)).to.equal('index')
53-
})
45+
describe("should return 'index' for sibling index file", function() {
46+
expect(importType('.', context)).to.equal('index')
47+
expect(importType('./', context)).to.equal('index')
48+
expect(importType('./index', context)).to.equal('index')
49+
expect(importType('./index.js', context)).to.equal('index')
5450
})
5551

5652
it("should return 'unknown' for any unhandled cases", function() {

0 commit comments

Comments
 (0)