Creating custom Eslint Rule. Grouping up imports by type.

Pryvalov Bogdan
6 min readSep 10, 2023

--

In this article i gonna show several ways to create custom plugin for Eslint.

First way to create rule it’s create js file with rule and export it.
The structure for both cases:

It’s React based structure

So inside plugins folder we need to create index file:

// index.js
const noDeprecatedMethod = require('./no-deprecated-method.js');

module.exports = {
rules: {
'no-deprecated-method': noDeprecatedMethod,
},
};

Here we will import rules what we need and how it will be named inside eslintrc.

The simple example for the custom rule with replacing deprecatedMethod for newMethod will look like this:

module.exports = {
meta: {
messages: {
avoidName: "Avoid using variables named '{{ name }}'", // Custom error message for the rule
},
type: 'problem', // The type of ESLint rule (problem, suggestion, etc.)
fixable: 'code', // Indicates that this rule can automatically fix code issues
},
create(context) {
return {
Identifier(node) {
if (node.name === 'deprecatedMethod') {
context.report({
node, // The AST node that triggered the error
messageId: 'avoidName', // The identifier for the custom error message
data: {
name: 'deprecatedMethod', // Data to be used in the error message
},
fix(fixer) {
// The fixer function for automatically fixing the reported error
return fixer.replaceText(node, 'newMethod'); // Replaces 'deprecatedMethod' with 'newMethod'
},
});
}
},
};
},
};

Here we defining the error messages, finding by name nodes and replacing it with newMethod.

And now we need to define path to our plugin in package.json

 "devDependencies": {
"eslint-plugin-no-deprecated": "file:plugins/no-deprecated",
}

Also we need to add our plugin to eslintrc:

{
"root": true,
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"plugin:storybook/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"no-deprecated", // Adding our custom plugin
],
"rules": {
"no-deprecated/no-deprecated-method": 2, // Setting our custom rule type (as error in this case)
},
"env": {
"browser": true,
"node": true
},
}

Don’t forget we added path in package json as “eslint-plugin-no-deprecated” but inside plugins we discard “eslint-plugin” part and impoting just “no-deprecated”.

This is how error will look:

The second way similar to previous but it will be bundle with typescript.

For the begin we can generate vanilla ts project with:

yarn create vite

Remove everything what inside src folder and modify package json.

{
"name": "eslint-plugin-custom-rule",
"version": "1.0.0",
"main": "cjs/index.js", // Changing main js for bundled files
"typings": "cjs/index.d.ts", // Changing typing for bundled files
"private": true,
"dependencies": {
"@typescript-eslint/utils": "^6.1.0"
},
"devDependencies": {
"@typescript-eslint/parser": "^6.1.0",
"@typescript-eslint/rule-tester": "^6.1.0",
"eslint": "^8.45.0",
"typescript": "^5.1.6",
"vitest": "^0.33.0"
},
"scripts": {
"build": "yarn tsc -b", // Creatating build
"test": "vitest"
}
}

Same was as was in js example we need to create index file with rules:

import { TSESLint } from '@typescript-eslint/utils';

import groupUpImportsByType from './group-up-imports-by-type';

export const rules = {
'new-line-import-group': groupUpImportsByType,
} satisfies Record<string, TSESLint.RuleModule<string, Array<unknown>>>;

For typing we can use @typescript-eslint/utils package

The custom rule to group up imports by type will look like this:

import { TSESLint, TSESTree } from '@typescript-eslint/utils';

// Define custom message identifiers for ESLint messages
type MessageIds = 'messageIdNoLine';

// Define the ESLint rule and its properties
const groupUpImportsByType: TSESLint.RuleModule<MessageIds> = {
defaultOptions: [], // Default options for the rule (none in this case)
meta: {
type: 'problem', // The type of ESLint rule (problem, suggestion, etc.)
fixable: 'code', // Indicates that this rule can automatically fix code issues
messages: {
messageIdNoLine: 'No new line before multiline import', // Custom error message
},
schema: [], // Configuration schema for this rule (none in this case)
},
create(context: TSESLint.RuleContext<MessageIds, []>) {
let lastGroupType = ''; // Initialize a variable to track the last import group

return {
ImportDeclaration(node: TSESTree.ImportDeclaration) {
const importPath = node.source.value; // Get the path of the import statement

let currentGroupType = ''; // Initialize a variable to track the current import group type

// Determine the import group type based on the import path
if (importPath.startsWith('.')) {
currentGroupType = 'local folder files';
} else if (importPath.startsWith('@')) {
currentGroupType = 'internal modules';
} else {
currentGroupType = 'external modules';
}

const sourceCode = context.getSourceCode();
const prevNodeToken = sourceCode.getTokenBefore(node); // Get the token before the current import statement

// Check if there is a token before the current import statement
if (!prevNodeToken) {
return; // If there is no token before, exit the function
}

// Calculate the index of the previous token's location in the source code
const prevNodeIndex = sourceCode.getIndexFromLoc(prevNodeToken.loc.start);

// Get the node (AST node) corresponding to the previous token's location
const prevNode = sourceCode.getNodeByRangeIndex(prevNodeIndex);

// Check if the previous node is an 'ImportDeclaration' node
const isPrevNodeImportType = prevNode?.type === 'ImportDeclaration';

// If the previous node is not an 'ImportDeclaration', like 'Punctuator', etc..., exit the function
if (!isPrevNodeImportType) {
return;
}

// Calculate whether a newline is needed before the current import statement
const isNewlineNeeded = node.loc.start.line - 1 === prevNode.loc.end.line;

// Check if a new line is needed before the import statement and report an error if necessary
if (lastGroupType !== '' && lastGroupType !== currentGroupType) {
if (isNewlineNeeded) {
context.report({
node, // The AST node that triggered the error
messageId: 'messageIdNoLine', // The custom message identifier ('messageIdNoLine' in this case)
fix(fixer: TSESLint.RuleFixer) {
// A function to provide a fix for the reported error
return fixer.insertTextBefore(node, '\n'); // The fixer inserts a newline before the 'node'
},
});
}
}
lastGroupType = currentGroupType; // Update the last import group type
},
};
},
};

export default groupUpImportsByType;

Here we getting importPath and creating type for which we will add empty line before to group up imports.

Before adding rule to package json we need to run yarn build to create bundle.

After the same way we will add rule to package json and elsintrc:

{
"name": "i18next-react-config",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --open",
"build": "tsc && vite build",
"lint": "eslint --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"test": "jest --coverage",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
...
},
"devDependencies": {
...
"eslint-plugin-no-deprecated": "file:plugins/no-deprecated",
"eslint-plugin-group-imports": "file:plugins/group-imports"
}
}
{
"root": true,
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"plugin:storybook/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"no-deprecated",
"group-imports"
],
"rules": {
"no-deprecated/no-deprecated-method": 2,
"group-imports/new-line-import-group": 2,
},
"env": {
"browser": true,
"node": true
}
}

This how the error for imports will look before fix:

After eslint fix:

There also could be lag with WebStorm/IntelliJIdea that custom rule doesn’t applied but it’s working with terminal command. For this case we can add manually custom rule:

Hint: in most cases we don’t need it, reload WebStorm will help.

Documentation for eslint custom rules: https://eslint.org/docs/latest/extend/custom-rules
Snippet where you can debug your custom rule:
https://astexplorer.net/#/gist/f121a2a9edea666731e75aae1d013c9d/latest

That’s it, i hope it was interesting for u.

If you have any suggestions fell free to add a comment.

Source code: https://github.com/pryvalovbogdan/i18next-react-config/tree/add-custom-eslint-rule

Subscribe if you are interested in such examples.

--

--