-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #247 from jfmengels/import-order
Add `order` rule
- Loading branch information
Showing
10 changed files
with
715 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
# Enforce a convention in module import order | ||
|
||
Enforce a convention in the order of `require()` / `import` statements. The order is as shown in the following example: | ||
|
||
```js | ||
// 1. node "builtins" | ||
import fs from 'fs'; | ||
import path from 'path'; | ||
// 2. "external" modules | ||
import _ from 'lodash'; | ||
import chalk from 'chalk'; | ||
// 3. "internal" modules | ||
// (if you have configured your path or webpack to handle your internal paths differently) | ||
import foo from 'src/foo'; | ||
// 4. modules from a "parent" directory | ||
import foo from '../foo'; | ||
import qux from '../../foo/qux'; | ||
// 5. "sibling" modules from the same or a sibling's directory | ||
import bar from './bar'; | ||
import baz from './bar/baz'; | ||
// 6. "index" of the current directory | ||
import main from './'; | ||
``` | ||
|
||
Unassigned imports are ignored, as the order they are imported in may be important. | ||
|
||
Statements using the ES6 `import` syntax must appear before any `require()` statements. | ||
|
||
|
||
## Fail | ||
|
||
```js | ||
import _ from 'lodash'; | ||
import path from 'path'; // `path` import should occur before import of `lodash` | ||
|
||
// ----- | ||
|
||
var _ = require('lodash'); | ||
var path = require('path'); // `path` import should occur before import of `lodash` | ||
|
||
// ----- | ||
|
||
var path = require('path'); | ||
import foo from './foo'; // `import` statements must be before `require` statement | ||
``` | ||
|
||
|
||
## Pass | ||
|
||
```js | ||
import path from 'path'; | ||
import _ from 'lodash'; | ||
|
||
// ----- | ||
|
||
var path = require('path'); | ||
var _ = require('lodash'); | ||
|
||
// ----- | ||
|
||
// Allowed as ̀`babel-register` is not assigned. | ||
require('babel-register'); | ||
var path = require('path'); | ||
|
||
// ----- | ||
|
||
// Allowed as `import` must be before `require` | ||
import foo from './foo'; | ||
var path = require('path'); | ||
``` | ||
|
||
## Options | ||
|
||
This rule supports the following options: | ||
|
||
`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: | ||
```js | ||
[ | ||
'builtin', // Built-in types are first | ||
['sibling', 'parent'], // Then sibling and parent types. They can be mingled together | ||
'index', // Then the index file | ||
// Then the rest: internal and external type | ||
] | ||
``` | ||
The default value is `["builtin", "external", "internal", "parent", "sibling", "index"]`. | ||
|
||
You can set the options like this: | ||
|
||
```js | ||
"import/order": ["error", {"groups": ["index", "sibling", "parent", "internal", "external", "builtin"]}] | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
'use strict' | ||
|
||
import find from 'lodash.find' | ||
import importType from '../core/importType' | ||
import isStaticRequire from '../core/staticRequire' | ||
|
||
const defaultGroups = ['builtin', 'external', 'parent', 'sibling', 'index'] | ||
|
||
// REPORTING | ||
|
||
function reverse(array) { | ||
return array.map(function (v) { | ||
return { | ||
name: v.name, | ||
rank: -v.rank, | ||
node: v.node, | ||
} | ||
}).reverse() | ||
} | ||
|
||
function findOutOfOrder(imported) { | ||
if (imported.length === 0) { | ||
return [] | ||
} | ||
let maxSeenRankNode = imported[0] | ||
return imported.filter(function (importedModule) { | ||
const res = importedModule.rank < maxSeenRankNode.rank | ||
if (maxSeenRankNode.rank < importedModule.rank) { | ||
maxSeenRankNode = importedModule | ||
} | ||
return res | ||
}) | ||
} | ||
|
||
function report(context, imported, outOfOrder, order) { | ||
outOfOrder.forEach(function (imp) { | ||
const found = find(imported, function hasHigherRank(importedItem) { | ||
return importedItem.rank > imp.rank | ||
}) | ||
context.report(imp.node, '`' + imp.name + '` import should occur ' + order + | ||
' import of `' + found.name + '`') | ||
}) | ||
} | ||
|
||
function makeReport(context, imported) { | ||
const outOfOrder = findOutOfOrder(imported) | ||
if (!outOfOrder.length) { | ||
return | ||
} | ||
// There are things to report. Try to minimize the number of reported errors. | ||
const reversedImported = reverse(imported) | ||
const reversedOrder = findOutOfOrder(reversedImported) | ||
if (reversedOrder.length < outOfOrder.length) { | ||
report(context, reversedImported, reversedOrder, 'after') | ||
return | ||
} | ||
report(context, imported, outOfOrder, 'before') | ||
} | ||
|
||
// DETECTING | ||
|
||
function computeRank(context, ranks, name, type) { | ||
return ranks[importType(name, context)] + | ||
(type === 'import' ? 0 : 100) | ||
} | ||
|
||
function registerNode(context, node, name, type, ranks, imported) { | ||
const rank = computeRank(context, ranks, name, type) | ||
if (rank !== -1) { | ||
imported.push({name, rank, node}) | ||
} | ||
} | ||
|
||
function isInVariableDeclarator(node) { | ||
return node && | ||
(node.type === 'VariableDeclarator' || isInVariableDeclarator(node.parent)) | ||
} | ||
|
||
const types = ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'] | ||
|
||
// Creates an object with type-rank pairs. | ||
// Example: { index: 0, sibling: 1, parent: 1, external: 1, builtin: 2, internal: 2 } | ||
// Will throw an error if it contains a type that does not exist, or has a duplicate | ||
function convertGroupsToRanks(groups) { | ||
const rankObject = groups.reduce(function(res, group, index) { | ||
if (typeof group === 'string') { | ||
group = [group] | ||
} | ||
group.forEach(function(groupItem) { | ||
if (types.indexOf(groupItem) === -1) { | ||
throw new Error('Incorrect configuration of the rule: Unknown type `' + | ||
JSON.stringify(groupItem) + '`') | ||
} | ||
if (res[groupItem] !== undefined) { | ||
throw new Error('Incorrect configuration of the rule: `' + groupItem + '` is duplicated') | ||
} | ||
res[groupItem] = index | ||
}) | ||
return res | ||
}, {}) | ||
|
||
const omittedTypes = types.filter(function(type) { | ||
return rankObject[type] === undefined | ||
}) | ||
|
||
return omittedTypes.reduce(function(res, type) { | ||
res[type] = groups.length | ||
return res | ||
}, rankObject) | ||
} | ||
|
||
module.exports = function importOrderRule (context) { | ||
const options = context.options[0] || {} | ||
let ranks | ||
|
||
try { | ||
ranks = convertGroupsToRanks(options.groups || defaultGroups) | ||
} catch (error) { | ||
// Malformed configuration | ||
return { | ||
Program: function(node) { | ||
context.report(node, error.message) | ||
}, | ||
} | ||
} | ||
let imported = [] | ||
let level = 0 | ||
|
||
function incrementLevel() { | ||
level++ | ||
} | ||
function decrementLevel() { | ||
level-- | ||
} | ||
|
||
return { | ||
ImportDeclaration: function handleImports(node) { | ||
if (node.specifiers.length) { // Ignoring unassigned imports | ||
const name = node.source.value | ||
registerNode(context, node, name, 'import', ranks, imported) | ||
} | ||
}, | ||
CallExpression: function handleRequires(node) { | ||
if (level !== 0 || !isStaticRequire(node) || !isInVariableDeclarator(node.parent)) { | ||
return | ||
} | ||
const name = node.arguments[0].value | ||
registerNode(context, node, name, 'require', ranks, imported) | ||
}, | ||
'Program:exit': function reportAndReset() { | ||
makeReport(context, imported) | ||
imported = [] | ||
}, | ||
FunctionDeclaration: incrementLevel, | ||
FunctionExpression: incrementLevel, | ||
ArrowFunctionExpression: incrementLevel, | ||
BlockStatement: incrementLevel, | ||
'FunctionDeclaration:exit': decrementLevel, | ||
'FunctionExpression:exit': decrementLevel, | ||
'ArrowFunctionExpression:exit': decrementLevel, | ||
'BlockStatement:exit': decrementLevel, | ||
} | ||
} | ||
|
||
module.exports.schema = [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
groups: { | ||
type: 'array', | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
] |
Oops, something went wrong.