Skip to content

Commit

Permalink
order accepts regular expressions. Add alphabetize option.
Browse files Browse the repository at this point in the history
  • Loading branch information
duncanbeevers committed Apr 15, 2016
1 parent f97990e commit f94a807
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 20 deletions.
10 changes: 9 additions & 1 deletion docs/rules/import-order.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,18 @@ var path = require('path');

This rule supports the following options:

`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.
`order`: The order to respect. The default order is `"builtin", "external", "parent", "sibling", "index"`. It may be overriden with an array containing any or all of these values. Any value from the default list not appearing in an explicit ordering are appended to the provided list.

You can set the options like this:

```js
"import-order/import-order": [2, {"order": ["index", "sibling", "parent", "external", "builtin"]}]
```

Additionally, you may supply regular expressions within this list, against which import paths will be tested. Paths not matching any rule are subject to the default sorting rules, and are sorted after the regular expression-based rules.

`alphabetize`: Whether to enforce alphabetization of imports within a single sort group. For example, while all `"builtin"` modules must appear before `"external"` modules, this rule will enforce that all `"builtin"` rules are sorted alphabetically with respect to one another.

```js
"import-order/import-order": [2, {"alphabetize": true}]
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"dependencies": {
"builtin-modules": "^1.1.1",
"lodash.cond": "^4.2.0",
"lodash.find": "^4.2.0"
"lodash.find": "^4.3.0"
},
"xo": {
"space": 2,
Expand Down
101 changes: 83 additions & 18 deletions rules/import-order.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ function report(context, imported, outOfOrder, order) {
var 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 + '`');
var message = '`' + imp.name + '` import should occur ' + order + ' import of `' + found.name + '`';
context.report(imp.node, message);
});
}

Expand All @@ -55,6 +56,35 @@ function makeReport(context, imported) {
report(context, imported, outOfOrder, 'before');
}

// Flatten the array-of-array buckets structure.
function rankBuckets(buckets, options) {
// Don't actually need a reference to imports since the data node is shared
// between the buckets and the actual-order imports.
if (options.alphabetize) {
buckets.forEach(function (bucket) {
bucket.sort(function (a, b) {
return a.name < b.name ? -1 : 1;
});
});
}

// Flatten bucket and append to final order list.
var imported = buckets.reduce(function (acc, bucket) {
if (bucket && bucket.length) {
acc = acc.concat(bucket);
}

return acc;
}, []);

// Annotate ranks.
imported.forEach(function (entry, i) {
entry.rank = i;
});

return imported;
}

// DETECTING

function isStaticRequire(node) {
Expand All @@ -65,14 +95,25 @@ function isStaticRequire(node) {
node.arguments[0].type === 'Literal';
}

function computeRank(order, name) {
return order.indexOf(utils.importType(name));
function computeBucket(order, name) {
var regExp = find(order, function (o) {
return o.test && o.test(name);
});

// Buckets numbers are cardinal.
return order.indexOf(regExp || utils.importType(name));
}

function registerNode(node, name, order, imported) {
var rank = computeRank(order, name);
if (rank !== -1) {
imported.push({name: name, rank: rank, node: node});
function registerNode(node, name, order, imported, buckets) {
// Each node will fall into a single bucket, and may optionally be sorted
// within that bucket.
var bucket = computeBucket(order, name);

if (~bucket) {
var data = {name: name, node: node};
buckets[bucket] = buckets[bucket] || [];
buckets[bucket].push(data);
imported.push(data);
}
}

Expand All @@ -81,11 +122,36 @@ function isInVariableDeclarator(node) {
(node.type === 'VariableDeclarator' || isInVariableDeclarator(node.parent));
}

function getOrder(options) {
// Clone any explicitly-provided order tiers.
var order = options.order ? options.order.slice() : [];

// Loop through the default tiers, checking to see if they already exist
// within the explicitly-provided tiers.
for (var i = 0, len = defaultOrder.length; i < len; i++) {
var name = defaultOrder[i];
var index = order.indexOf(name);

// If the tier is not already present, append it to the list.
if (!~index) {
order.push(name);
}
}

return order;
}

/* eslint quote-props: [2, "as-needed"] */
module.exports = function importOrderRule(context) {
var imported = [];
var options = context.options[0] || {};
var order = options.order || defaultOrder;

// buckets are numbered, order is important.
var buckets = [];
// List of all recognized imports, in order of actual occurrence.
var imported = [];

var order = getOrder(options);

var level = 0;

function incrementLevel() {
Expand All @@ -99,19 +165,20 @@ module.exports = function importOrderRule(context) {
ImportDeclaration: function handleImports(node) {
if (node.specifiers.length) { // Ignoring unassigned imports
var name = node.source.value;
registerNode(node, name, order, imported);
registerNode(node, name, order, imported, buckets);
}
},
CallExpression: function handleRequires(node) {
if (level !== 0 || !isStaticRequire(node) || !isInVariableDeclarator(node.parent)) {
return;
}
var name = node.arguments[0].value;
registerNode(node, name, order, imported);
registerNode(node, name, order, imported, buckets);
},
'Program:exit': function reportAndReset() {
rankBuckets(buckets, options);
makeReport(context, imported);
imported = [];
buckets = [];
},
FunctionDeclaration: incrementLevel,
FunctionExpression: incrementLevel,
Expand All @@ -129,12 +196,10 @@ module.exports.schema = [
type: 'object',
properties: {
order: {
type: 'array',
uniqueItems: true,
length: 5,
items: {
enum: defaultOrder
}
type: 'array'
},
alphabetize: {
type: 'boolean'
}
},
additionalProperties: false
Expand Down
41 changes: 41 additions & 0 deletions test/import-order.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,24 @@ test(() => {
`,
options: [{order: ['index', 'sibling', 'parent', 'external', 'builtin']}]
},
// Overriding order to include a regular expression
{
code: `
var cond = require('lodash.cond');
var findIndex = require('lodash.find');
var path = require('path');
var $ = require('jquery');
`,
options: [{order: [/^lodash\./, 'builtin', /^jquery$/]}]
},
// Alphabetize by import path
{
code: `
var fs = require('fs');
var path = require('path');
`,
options: [{alphabetize: true}]
},
// Ignore dynamic requires
`
var path = require('path');
Expand Down Expand Up @@ -227,6 +245,29 @@ test(() => {
{...ruleError, message: '`./` import should occur before import of `fs`'}
]
},
// Overriding order to include a regular expression
{
code: `
var $ = require('jquery');
var cond = require('lodash.cond');
var findIndex = require('lodash.find');
`,
options: [{order: [/^lodash\./, /^jquery$/]}],
errors: [
{...ruleError, message: '`jquery` import should occur after import of `lodash.find`'}
]
},
// Alphabetize by import path
{
code: `
var path = require('path');
var fs = require('fs');
`,
options: [{alphabetize: true}],
errors: [
{...ruleError, message: '`fs` import should occur before import of `path`'}
]
},
// member expression of require
{
code: `
Expand Down

0 comments on commit f94a807

Please sign in to comment.