Skip to content

Commit

Permalink
Add ESLint sort dependencies rule (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
albertogasparin authored Jul 13, 2020
1 parent 142d5eb commit dec262b
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 1 deletion.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,9 @@ In order to enforce better practices, this package exports some eslint rules:
| `exhaustive-inject` | enforces all external components/hooks being used to be marked as injectable. | `ignore`: array of names |
| `no-duplicate` | prohibits marking the same dependency as injectable more than once in the same block | - |
| `no-extraneous` | enforces dependencies to be consumed in the scope, to prevent unused variables | - |
| `sort-dependencies` | require injectable dependencies to be sorted | - |

The rules are exported from `react-magnetic-di/eslint-plugin`. Unfortunately Eslint does not allow plugins that are not npm packages, so rules needs to be imported via other means.
The rules are exported from `react-magnetic-di/eslint-plugin`. Unfortunately Eslint does not allow plugins that are not npm packages, so rules needs to be imported via other means for now.

## Current limitations

Expand Down
2 changes: 2 additions & 0 deletions src/eslint/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ const order = require('./rules/order');
const exhaustiveInject = require('./rules/exhaustive-inject');
const noDuplicate = require('./rules/no-duplicate');
const noExtraneous = require('./rules/no-extraneous');
const sortDependencies = require('./rules/sort-dependencies');

module.exports = {
rules: {
order: order,
'exhaustive-inject': exhaustiveInject,
'no-duplicate': noDuplicate,
'no-extraneous': noExtraneous,
'sort-dependencies': sortDependencies,
},
};
116 changes: 116 additions & 0 deletions src/eslint/rules/__tests__/sort-dependencies.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { RuleTester } from 'eslint';
import rule from '../sort-dependencies';
import { genericCases } from './utils';

RuleTester.setDefaultConfig({
parserOptions: {
ecmaVersion: 6,
sourceType: 'module',
ecmaFeatures: { jsx: true },
},
});

var ruleTester = new RuleTester();
ruleTester.run('sort-dependencies', rule, {
valid: [
// it should pass generic cases
...genericCases,

// is should pass if only one argument
`
import { useState } from 'react';
import { di } from 'react-magnetic-di';
function MyComponent() {
di(useState);
return null;
}
`,
// is should pass if multiple arguments
`
import { useContext, useState } from 'react';
import { di } from 'react-magnetic-di';
import { useContext as useMyContext, MyStuff, MyStuff2 } from './my-stuff';
function MyComponent() {
di(MyStuff, MyStuff2, useContext, useMyContext, useState);
return null;
}
`,
],

invalid: [
{
// it should re-order inline
code: `
import { useContext, useState } from 'react';
import { di } from 'react-magnetic-di';
import { Query, useQuery, OtherQuery } from 'react-apollo';
function MyComponent() {
di(OtherQuery, useState, Query, useQuery, useContext);
return null;
}
`,
errors: [
{
messageId: 'unsortedInjectable',
type: 'Identifier',
},
{
messageId: 'unsortedInjectable',
type: 'Identifier',
},
],
output: `
import { useContext, useState } from 'react';
import { di } from 'react-magnetic-di';
import { Query, useQuery, OtherQuery } from 'react-apollo';
function MyComponent() {
di(OtherQuery, Query, useContext, useQuery, useState);
return null;
}
`,
},
{
// it should re-order new lines
code: `
import { useContext, useState } from 'react';
import { di } from 'react-magnetic-di';
import { Query, OtherQuery } from 'react-apollo';
function MyComponent() {
di(
Query,
useContext,
useState,
OtherQuery
);
return null;
}
`,
errors: [
{
messageId: 'unsortedInjectable',
type: 'Identifier',
},
],
output: `
import { useContext, useState } from 'react';
import { di } from 'react-magnetic-di';
import { Query, OtherQuery } from 'react-apollo';
function MyComponent() {
di(
OtherQuery,
Query,
useContext,
useState
);
return null;
}
`,
},
],
});
71 changes: 71 additions & 0 deletions src/eslint/rules/sort-dependencies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const { getDiIdentifier, getDiStatements } = require('../utils');

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Require injectable dependencies to be sorted',
category: 'Stylistic Issues',
recommended: false,
},
fixable: 'code',
schema: [],
messages: {
unsortedInjectable:
'Expected dependencie names to be sorted. ' +
"'{{name}}' should be before '{{prevName}}'.",
},
},
create: function (context) {
let diIdentifier;

const report = (node, prevNode, args, sortedArgs) =>
context.report({
node,
messageId: 'unsortedInjectable',
data: { name: node.name, prevName: prevNode.name },
fix(fixer) {
// grab whatever between 1st arg end / 2nd arg start as separator
const separator = context
.getSourceCode()
.text.slice(args[0].range[1], args[1].range[0]);
const start = args[0].range[0];
const end = args[args.length - 1].range[1];
const sorted = sortedArgs.map((n) => n.name).join(separator);
// fixes all order issues at once
// so avoids the need of multiple saves
return fixer.replaceTextRange([start, end], sorted);
},
});

return {
ImportDeclaration(node) {
if (!diIdentifier) diIdentifier = getDiIdentifier(node);
},

BlockStatement(node) {
if (!diIdentifier) return;

const diStatements = getDiStatements(node, diIdentifier);
// ignore locations where di was not explicitly set
if (!diStatements.length) return;

diStatements.forEach((statement) => {
const args = statement.expression.arguments;
// sort uppercase first, lowercase after
// so we get components and hooks grouped
const sortedArgs = args
.slice()
.sort((a, b) => a.name.localeCompare(b.name));

args.forEach((arg, i) => {
const prevArg = args[i - 1];
if (prevArg && arg.name.localeCompare(prevArg.name) < 0) {
report(arg, prevArg, args, sortedArgs);
}
});
});
},
};
},
};

0 comments on commit dec262b

Please sign in to comment.