In everyday projects, we often encounter scenarios that require computation. But JavaScript computations have a lot of precision issues, and precision is often ignored when coding code, which increases the workload of QA students and ourselves.

In order to solve the problem of accuracy, the community also emerged a lot of excellent libraries, here I recommend a small and beautiful library bigJS; We can not passively wait until there is a bug to solve the problem, in order to leave work on time, in the daily weight coding, we need to take into account the accuracy of the problem; This is the time to customize an ESLint plugin with some of the team’s specifications.

How does ESLint work? The magic of the AST

Before we dive into creating ESLint rules, we need to understand what AST is and why they are so useful to developers.

An AST or abstract syntax tree represents code as a tree that a computer can read and manipulate.

We write code for computers in advanced, human-understandable languages such as C, Java, JavaScript, Elixir, Python, Rust… But a computer is not human: in other words, it cannot know what we mean to write. We need a way for the computer to parse your code syntactically to understand that this const is a variable declaration, and {} sometimes marks the beginning of an object expression, sometimes the beginning of a function…… And so on. This is done through the AST, which is a necessary step.

In ESLint, esprima is used by default to parse Javascript statements we write, generate abstract syntax trees, intercept them to see if they conform to the way we write them, and display errors, warnings, or passes. However, as we all use typescript today, we need another @typescript-esLint /parser to convert TS code to ast syntax books

Let’s do a simple example

You can see that the abstract syntax tree of TS code has type information compared to the syntax tree parsed by JS code.

Create the plugin

From the ESLint documentation you can have a plugin that mainly has rules associated with some of the configurations we normally define in eslintrc.js

module.exports = {
    rules: {
        "dollar-sign": {
            create: function (context) {
                // rule implementation ...}}},config: {}};Copy the code

@typescript-eslint/eslint-plugin index.ts

import rules from './rules';
import all from './configs/all';
import base from './configs/base';
import recommended from './configs/recommended';
import recommendedRequiringTypeChecking from './configs/recommended-requiring-type-checking';
import eslintRecommended from './configs/eslint-recommended';

export = {
  rules,
  configs: {
    all,
    base,
    recommended,
    'eslint-recommended': eslintRecommended,
    'recommended-requiring-type-checking': recommendedRequiringTypeChecking,
  },
};
Copy the code

Now that we know the basic structure of the plug-in, we can initialize the project.

Create a test project

To debug and verify the functionality of the plug-in, we run

npm i typescript -D
npx tsc --init`
Copy the code

Simply initialize a Typecript project

{
 "compilerOptions": {
   "target": "ES6"."module": "CommonJS"."skipLibCheck": true."moduleResolution": "node"."experimentalDecorators": true."esModuleInterop": true."sourceMap": false."baseUrl": "."."checkJs": false."lib": ["esnext"."DOM"]."paths": {
     // path alias
     "@ / *": [
       "src/*"]}},"include": [
   "src/**/*.ts"."eslint-rules/index.ts"."eslint-rules/no-raw-float-calculation.ts"]."exclude": [
   "node_modules"]},Copy the code

Create a new eslint-Rules directory to store the plugins we need to customize and install the dependencies

// eslint related NPM I eslint @typescript-eslint/eslint-plugin @typescript-eslint/experimental utils @typescript-eslint/parser -d // Node type indicates NPM I @types/node -d // jest related NPM I @types/jest ts-jest jest -dCopy the code

The root directory initializes eslintrc.js

Just follow the instructions step by step,Note that you choose to use typescript in your project.

module.exports = {
    "env": {
        "node": true."es2021": true,},"extends": [
        "eslint:recommended"."plugin:@typescript-eslint/recommended",]."parser": "@typescript-eslint/parser"."parserOptions": {
        "ecmaVersion": "latest"."sourceType": "module".project: ['./tsconfig.json'],},"plugins": [
        "@typescript-eslint"]}Copy the code

After all the steps are completed, the approximate directory structure is as follows:

Write the rule

For details on how to write typescript ESLint rules, we can refer to the custom Rules section of the official documentation to develop custom plug-ins using the templates in the documentation.

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

type MessageIds = 'noRawNumberCalculation';
type Options = [];

// https://typescript-eslint.io/docs/development/custom-rules
const createRule = ESLintUtils.RuleCreator(
  // The rule documentation link
  name= > `https://example.com/rule/${name}`,);/** * Options: rule Supported parameters * MessageIds: id of an upload error */
export default createRule<Options, MessageIds>({
  name: 'no-raw-number-calculation'.meta: {
    type: 'problem'.docs: {
      description:
        'Avoid the four operations of the native JS number type and use bigJs instead'.recommended: 'error'.requiresTypeChecking: true,},messages: {
      noRawNumberCalculation:
        'Avoid the four operations of the native JS number type and use bigJs instead',},schema: null // rule Parameter description; Here's a simple demo with no arguments
  },
  defaultOptions: [].create(context) {
    const parserServices = ESLintUtils.getParserServices(context);
    const checker = parserServices.program.getTypeChecker();

    const getNodeType = (node: TSESTree.Expression | TSESTree.PrivateIdentifier) = > {
      // Mapping esLint ast nodes to TypeScript ts.node equivalents
      const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
      // Get typescript source code type information
      // const a: number = 1
      return checker.getTypeAtLocation(tsNode);
    }

    const checkBinaryExpression = (node: TSESTree.BinaryExpression) = > {
      const leftNodeType = getNodeType(node.left);
      const rightNodetType = getNodeType(node.right);
      The // operator has the number type on both sides
      if (leftNodeType.isNumberLiteral() && rightNodetType.isNumberLiteral()) {
        context.report({
          node,
          messageId: 'noRawNumberCalculation'}); }}return {
      // +-*/
      "BinaryExpression[operator='+']": checkBinaryExpression,
      "BinaryExpression[operator='-']": checkBinaryExpression, 
      "BinaryExpression[operator='*']": checkBinaryExpression,
      "BinaryExpression[operator='/']": checkBinaryExpression
    }
  }
});
Copy the code

At this point, we have finished writing a simple rule that can detect the four operations of number in the code; Of course, this is just a demo, the logic is very simple, and there are many operators to consider, such as: +=, ++ and so on. For example, if it is an integer, it can directly perform the original four operations. Generally involving floating-point type calculations, precision operations need to be considered. But typscript only provides the number type, and we can customize the integer and float types.

declare type integer = number & { readonly__integer__? : unique symbol };declare type float = number & { readonly__float__? : unique symbol };declare type int = integer;
Copy the code

In a real project, the above type can be added to the project’s template declaration file in conjunction with the team’s scaffolding tool. Regardless of that, back in demo, we need to create a new rule written by index.ts export,

import noRawNumberCalculation from './no-raw-number-calculation';

export = {
  rules: {
    'no-raw-number-calculation': noRawNumberCalculation
  }
}
Copy the code

Now that we’ve written a simple rule, how do we use it and debug it? First go back to the eslint-rules file directory: NPM init-y.

{
  "name": "eslint-plugin-demo"./ / eslint - plugin - the beginning
  "version": "1.0.0"."description": ""."main": "index.js".// index.ts compiles to index.js
  "directories": {
    "test": "tests"
  },
  "scripts": {
    "test": "echo 'test'"
  },
  "keywords": []."author": ""."license": "ISC"
}
Copy the code

Test using rule

After returning to the root directory, execute NPM run build:rule, then add the eslint-plugin-demo local dependencies to package.json, and then execute NPM install.

{
  "name": "custom_rule"."version": "1.0.0"."description": "a custom ts lint rule demo"."scripts": {
    "test": "jest".// Single test entry
    "build:rule": "tsc"./ / compile eslint - rule
    "lint": "eslint ./src" // lint code
  },
  "keywords": []."author": ""."license": "ISC"."devDependencies": {
    "@types/jest": "^ 27.4.1"."@types/node": "^ 17.0.21"."@typescript-eslint/eslint-plugin": "^ 5.15.0"."@typescript-eslint/experimental-utils": "^ 5.15.0"."@typescript-eslint/parser": "^ 5.15.0"."eslint": "^ 8.11.0"."eslint-plugin-demo": "file:./eslint-rules".// Local dependency
    "jest": "^ 27.5.1"."ts-jest": "^ 27.1.3"."typescript": "^ 4.6.2." "}}Copy the code

Add eslint-plugin-demo to eslintrc.js

"plugins": [
   // ...
   "demo"]."rules": {
    'demo/no-number-float-calculation': 'error',}Copy the code

Now we can see the effect of writing the rule:

To performnpm run lint

You can see that the plug-in is working properly. Ha ha, you’re done!

The test case

Add the __test__ folder under eslint-rules:

Why create a file.ts file? Official description: It is a blank test file for normal TS tests. (I didn’t quite get to this point, but it must be added otherwise the test case won’t run properly)

Test cases are also written like normal ESLint rules, just consider pass and fail scenarios.

import { ESLintUtils } from '@typescript-eslint/utils';
import rule from '.. /no-raw-number-calculation';

const ruleTester = new ESLintUtils.RuleTester({
  parser: '@typescript-eslint/parser'.parserOptions: {
    tsconfigRootDir: __dirname,
    project: './tsconfig.json',}}); ruleTester.run('my-typed-rule', rule, {
  valid: [
    "const a = 1; const b = '2'; console.log(a + b);"].invalid: [{code: "const a = 2; const b = 4; console.log(a + b)".errors: [{ messageId: 'noRawNumberCalculation'}}]]});Copy the code

Single measurement tsconfig. Json

{
  "extends": ".. /.. /tsconfig.json".// Inherit the root ts configuration
  "include": [
    "file.ts"]}Copy the code

Finally, let’s look at the configuration of jest.config.js, mainly to support parsing TS files.

module.exports = {
  preset: 'ts-jest'.testEnvironment: 'node'.transform: {
    '^.+\\.tsx? $': 'ts-jest',},testTimeout: 60000.testRegex: '/__tests__/.*.(jsx? |tsx?) $'.collectCoverage: false.watchPathIgnorePatterns: ['/node_modules/'.'/dist/'.'/.git/'].moduleFileExtensions: ['ts'.'js'.'json'.'node'].testPathIgnorePatterns: ['/node_modules/']};Copy the code

Finally, in the root directory, run NPM test

The test case also passed! At this point, we are almost done with the development process of writing a custom plugin, followed by the release of NPM, which will not be demonstrated, after all, this is just a learning demo.

Thank you for watching it!