前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >说真的,不如用ESLint插件替代掉部分技术文档

说真的,不如用ESLint插件替代掉部分技术文档

作者头像
源心锁
发布2023-04-23 16:16:34
9670
发布2023-04-23 16:16:34
举报
文章被收录于专栏:前端魔法指南前端魔法指南
theme: devui-blue

1 前言

大家好,我是心锁,23届准毕业生。

近期在尝试编写一个供予项目使用的eslint插件,目的是为了不写一行开发文档即实现项目规范强制落地。

那么如何编写、启动和测试就比较头疼了,于是踩坑了一晚上之后,我决定把相关的开发流程分享出来。

2 目标

本文旨在探索和完成一轮完整的eslint插件开发历程,确保即便是没有eslint插件开发的读者也可以快速开始eslint插件开发。本文概览如下:

  • 学习如何使用yo初始化项目,以及相关命令各行输出的意义
  • 学习多种调试eslint插件的方式
  • 学习测试eslint插件的方式
  • 学习如何开发elint插件

3 初始化

正如大部分教程一样,我们采用yo和generator-eslint来初始化项目。

我们启动项目:

代码语言:javascript
复制
# 基本的脚手架
pnpm add -g yo generator-eslint

3.1 yo eslint:plugin

本步骤的目的是创建一个eslint插件的基本模板。

需要注意的是,我们运行yo eslint:plugin并不会自动创建文件夹,所以需要事先创建文件夹:

代码语言:javascript
复制
mkdir eslint-plugin-super-hero
cd ./eslint-plugin-super-hero

运行yo eslint:plugin 需要回答几个问题:

Untitled
Untitled

这些问题代表:

  • What is your name: 作者是谁? ————见面问的是作者名而不是项目名,6.
  • What is the plugin ID:插件ID,指的是在.eslintrc 文件中进行插件引用时填写的值。具体就是:
Untitled
Untitled
  • Does the plugin contain custom ESLint rules:选了来写自定义规则,选就完事了
  • Does the plugin contain one or more processors:处理器的用处是用来告诉eslint如何处理javascript之外的文件,一般来讲我们无需关注,所以选择No。(jsx、tsx、ts这些前端常见格式无需我们处理,需要处理的时候下载parser即可)

完成上述步骤记得安装一下依赖,建议使用pnpm install

代码语言:javascript
复制
pnpm install
pnpm add typescript -D

这应该是本步骤完成后的目录结构图:

Untitled
Untitled

3.2 yo eslint:rule

运行yo eslint:rule,会为我们生成一条插件下规则的基本模版。

Untitled
Untitled
Untitled
Untitled
  • What is your name?:见面问名字,该名字,注意这是作者名字不是插件名
  • Where will this rule be published? :我专门看了两遍,生成的文件没看到有差异。随便选
  • What is the rule ID?:规则的名字

后边的不重要是,随意填,大概率要改的。

3.3 lib/rules/xxx.js

运行完该命令,会在lib/rules/xxx.js生成如下的文件,该文件即我们定义一条规则需要书写代码的地方。

代码语言:javascript
复制
/**
 * @fileoverview force the import order
 * @author import-sorter // 合着作者名在这儿呗
 */
"use strict";

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
  meta: {
    type: null, // `problem`, `suggestion`, or `layout`
    docs: {
      description: "force the import order",
      recommended: false,
      url: null, // URL to the documentation page for this rule
    },
    fixable: null, // Or `code` or `whitespace`
    schema: [], // Add a schema if the rule has options
  },

  create(context) {
    // 变量定义区

    //----------------------------------------------------------------------
    // Helpers
    //----------------------------------------------------------------------

    // 工具函数区

    //----------------------------------------------------------------------
    // Public
    //----------------------------------------------------------------------

    return {
      // 用来真正分析AST的函数位置
    };
  },
};

3.3.1 meta

来看一下meta,初始生成的meta是以下结构。

代码语言:javascript
复制
meta: {
   type: null, // `problem`, `suggestion`, or `layout`
   docs: {
     description: "force the import order",
     recommended: false,
     url: null, // URL to the documentation page for this rule
   },
   fixable: null, // Or `code` or `whitespace`
   schema: [], // Add a schema if the rule has options
},

这些是一条规则的基本信息,以eslint官方的文档来看,meta对象包含规则的元数据:

  • type: 规则类型,必须是 "problem"、"suggestion" 或 "layout" 中的一种。
    • “problem”,表示规则属于高优先级问题,并且规则应当标识的代码是可能导致错误的代码。
    • “suggestion”,代表规则认为识别出来的代码有更好的实现方式,当然不改变代码也不会发生错误。
    • “layout”,代表规则主要关心代码样式问题。
  • docs用于配置ESLint核心规则:
    • description(string):描述规则。
    • recommended(boolean):是配置文件"extends": "eslint:recommended"中的属性是否启用规则。
    • url(string):指定完整文档的URL(使代码编辑器能够提供有关突出显示的规则违规的有用链接)
  • fixable(string)?: “code”|”whitespace”,
    • “code”: 配置code,代表该条规则可以自动修复
    • “whitespace”:这是为了将来使用。例如,在某些时候,ESLint 可能会提供一种仅修复空白规则或仅修复代码的方法。目前,“空白”应该用于处理间距(缩进、属性之间的间距等)的任何规则,其他一切都标记为“代码”。
    • undefined: 去除该属性,代表无法自动修复
  • schema?: 如果需要让规则接受参数,开启该属性。

其他的属性我们在开发的时候会讲到,这里主要补充一个messages参数,因为这是一个必要参数,但是示例中没有。

  • messages。messages接受一个对象,对象的key值即为messageId ,value值即为对应的报错消息。

3.3.2 create

create函数是插件真正工作的地方,create函数要求返回一个对象,对象的Key值是AST节点的类型,Value值则是调用的函数。

代码语言:javascript
复制
create(context) {
    return {
      // 访问某种类型的AST节点时会调用的函数
    };
  },

其中,create方法的cotext参数类型如下,这里包含了大量我们可能用得到的接口,这里可以简单看一下,实际开发的时候可以针对需要看文档。

代码语言:javascript
复制
interface RuleContext {
    id: string;
    options: any[]; //schema配置后,可以看到参数
    settings: { [name: string]: any }; // 共享设置
    parserPath: string;
    parserOptions: Linter.ParserOptions;
    parserServices: SourceCode.ParserServices; //解析器提供的规则服务的对象

    getAncestors(): ESTree.Node[]; //获取上一级AST节点
    getDeclaredVariables(node: ESTree.Node): Scope.Variable[]; //获取变量表
    getFilename(): string; //获取文件名称,可以实现文件名与代码关联规范
    getPhysicalFilename(): string; //对文件进行 linting 时,它返回磁盘上文件的完整路径,不包含任何代码块信息
    getCwd(): string; //当前工作目录的目录的路径
    getScope(): Scope.Scope; // 获取作用域的信息。官方声明弃用,未来可能移除,可通过SourceCode.getScope(node)替代
    getSourceCode(): SourceCode; // 获取源代码的AST树
    markVariableAsUsed(name: string): boolean; //当前作用域中给定名称的变量标记为已使用。这会影响no-unused-vars规则。true如果找到具有给定名称的变量并将其标记为已使用,则返回,否则返回false。
    report(descriptor: ReportDescriptor): void; //报告代码中的问题,核心函数,在ReportDescriptor中我们可以声明更多信息,包括错误提示、修复方式等
}

这里边最核心的就是context.report方法,用于向eslint报告错误,同时也可以通过该函数传递fix方法用于自动修复错误。

代码语言:javascript
复制
interface ReportDescriptorOptions extends ReportDescriptorOptionsBase {
    suggest?: SuggestionReportDescriptor[] | null | undefined;
}

type ReportDescriptor = ReportDescriptorMessage & ReportDescriptorLocation & ReportDescriptorOptions;
type ReportDescriptorMessage = { message: string } | { messageId: string };
type ReportDescriptorLocation =
     | { node: ESTree.Node }
     | { loc: AST.SourceLocation | { line: number; column: number } };

type ReportFixer = (fixer: RuleFixer) => null | Fix | IterableIterator<Fix> | Fix[];
interface SuggestionReportOptions {
    data?: { [key: string]: string };
    fix: ReportFixer;
}
type SuggestionDescriptorMessage = { desc: string } | { messageId: string };
type SuggestionReportDescriptor = SuggestionDescriptorMessage &amp; SuggestionReportOptions;

通过接口定义我们可以知道,一般情况下,我们可以通过下述代码报告错误。其中的messageId 对应我们定义在meta 中的messages属性

代码语言:javascript
复制
context.report({
  node,
  messageId: "xxxxxx",
  fix(fixer) {
    return fixer.insertTextBefore(
          sourceCode.ast.body[0],
          newImportStatement
        );
  },
});

3.4 tests/lib/rules/xxx.js

在我们运行3.2 的时候,同步生成了测试文件,这也是我们调试代码最便携的地方。下边是生成的代码中最主要的部分。

代码语言:javascript
复制
const ruleTester = new RuleTester();
ruleTester.run("import-sorter", rule, {
  valid: [
    // give me some code that won't trigger a warning
  ],

  invalid: [
    {
      code: "-",
      errors: [{ message: "Fill me in.", type: "Me too" }],
    },
  ],
});

valid指那些不会导致警告和报错的样例,我想了想,这部分总觉得不是很重要没有很细致研究。主要在invalid,invalid接受一个数组,里边是会导致eslint报错的代码:

代码语言:javascript
复制
interface InvalidTestCase extends ValidTestCase {
    errors: number | Array<TestCaseError | string>;
    output?: string | null | undefined;
}

其中,errors中除了可以和基本样例一样对message和type进行测试之外,也支持下边的参数

代码语言:javascript
复制
interface TestCaseError {
   message?: string | RegExp | undefined;
   messageId?: string | undefined;
   type?: string | undefined; //报错AST节点的类型
   data?: any;
   line?: number | undefined;
   column?: number | undefined;
   endLine?: number | undefined;
   endColumn?: number | undefined;
   suggestions?: SuggestionOutput[] | undefined;
}

4 imports-sorters实现

实际操作比理论更有效,我们尝试做一个文件导入排序规则。

4.1 需求

import操作常见于页面的最顶部,我们在导入的时候,应该也会发现如果我们随意排序这些操作,对于阅读并不是很友好。如下边这份代码。短短六行就是六种不同类型的导入。

代码语言:javascript
复制
import { io } from 'socket.io-client'
import { history } from '@renderer/utils'
import type { Socket } from 'socket.io-client'
import type { IMMessage } from './type'
import styles from './index.module.scss'
import './index.scss'

第一行,是第三方包的代码导入。第二行,是通过alias实现的绝对路径项目代码导入。第三行代码是第三方包的类型导入。第四行是项目相对路径的类型导入。第五行是css module静态资源导入,第六行是静态资源导入。

而显然,我们还可能遇见更多的导入类型。

目前我总结了下边的几种类型,按照组合区分优先级。

首先是常规的文件导入方式:

  • 第三方库
  • 绝对(alias)路径引入
  • 相对路径引入

然后是导入类型:

  • 类型导入。见于TypeScript
  • 静态资源导入。常见于前端
  • 普通导入

以及下边导入方法的区分:

  • import { xxx } from “xxx”;
  • import a from “xxx”;
  • import * as {xxx} from “xxx”;
  • import a,{xxx} from “xxx”;
  • import “xxx”;

同时,我们也希望能有一些特殊的规则,作为一名React技术栈走得比较深的前端玩家,我们还可以添加一条规则,让React导入必须在第一条。

4.2 准备环境

由于我们的插件,需要识别import type { xxx } from “xxx”; 我们需要准备typescript解析器。

代码语言:javascript
复制
pnpm add @typescript-eslint/parser

同时,修改我们的配置文件,增加parser

  • 根目录.eslintrc.js
代码语言:javascript
复制
"use strict";

module.exports = {
  root: true,
  parser: "@typescript-eslint/parser",
  extends: [
    "eslint:recommended",
    "plugin:eslint-plugin/recommended",
    "plugin:node/recommended",
  ],
  env: {
    node: true,
  },
  overrides: [
    {
      files: ["tests/**/*.js"],
      env: {
        mocha: true,
      },
    },
  ],
};
  • 在测试文件夹下新建配置文件。tests/lib/config.js
代码语言:javascript
复制
const testConfig = {
  parser: require.resolve('@typescript-eslint/parser'),
  env: {
    es2020: true,
    node: true,
  },
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: "module",
  },
};

module.exports = {
  testConfig,
};

之后在新建测试文件tests/lib/rules/xxx.js 时,应该添加以下代码:

Untitled
Untitled

4.3 开工

首先,我们需要定义一些常量,以便在后面的代码中使用。这些常量定义了不同类型的导入语句和规则。

代码语言:javascript
复制
// 导入种类
const ImportType = {
  ThirdParty: 1,
  Absolute: 2,
  Relative: 3,
  Unknown: 4,
};

// 导入方式
const ImportMethod = {
  Destructuring: 1,
  Default: 2,
  Namespace: 3,
  Mixed: 4,
  SideEffect: 5,
};

// React导入必须放在第一位
const ReactImportRegex = /^react/;

接下来,我们需要编写一个函数,该函数将导入语句按照类型和规则进行排序。这个函数有两个参数:一个是导入语句的数组,另一个是ESLint的上下文对象。该函数的主要流程如下:

  1. 根据导入语句的路径,判断导入语句的种类。
  2. 根据导入语句的方式,判断导入语句的方式。
  3. 根据规则和种类对导入语句进行排序。
  4. 返回排序后的导入语句数组。

距离我们实现代码只有一点点了,在具体实现代码之前,我们需要学习一下AST,否则想写下去是比较困难的。

4.3.1 AST分析

首先,我们需要知道AST的概念,AST是抽象语法树。它是我们程序源代码语法结构的一种抽象表示,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

——听起来是不是很抽象,还好我们并不需要过于深入了解其中原理。

我们现在只需要知道,ESLint的工作流程。

03cf7e926946b7d8a3da902841c3c5b1.jpg
03cf7e926946b7d8a3da902841c3c5b1.jpg

首先会把我们的源代码通过parser解析器转换成AST语法树。没错就是.eslintrc.js 中常见的parser字段,目前我们需要关心的只有如何为我们的插件选择一个第三方解析器,比如“@typescript-eslint/parser”就是一个解析器。

以下边这段代码示例:

代码语言:javascript
复制
import React from "react";
import { Provider } from "react-redux";
import store from "./store";
import "./App.scss";
import { Spin } from "antd";
import { useSDMall } from "@/hooks";

我们直接通过AST在线解析,经过AST解析,出来的结果如下(如果你跑不了,注意网站中可以切换解析器):

image.png
image.png

可以看出,我们只需要在首层的Program节点访问源代码,然后遍历body,由于Import只能在文件最顶部,所以访问从头到最后一个import文件,在排序后重新插入即可。

贴一下第一行的AST代码,实际上在入门eslint规则开发时,我们并不需要记忆下各个节点都有哪些属性,大多是需要的时候现查现场解析,根据需要的节点特征配合https://github.com/estree/estree查阅即可。

代码语言:javascript
复制
        {
      "type": "ImportDeclaration",
      "source": {
        "type": "Literal",
        "value": "react",
        "raw": "\"react\"",
        "range": [
          18,
          25
        ]
      },
      "specifiers": [
        {
          "type": "ImportDefaultSpecifier",
          "local": {
            "type": "Identifier",
            "name": "React",
            "range": [
              7,
              12
            ]
          },
          "range": [
            7,
            12
          ]
        }
      ],
      "importKind": "value",
      "assertions": [],
      "range": [
        0,
        26
      ]
    },

那么现在我们知道了两件事,第一,我们要在访问Program节点时修改AST树。第二,我们知道了要被修改的Node的type是ImportDeclaration。

那应该如何访问Program节点呢?

这个非常好办,我们的rules文件中的create函数,返回值只需要返回一个对象,对象的key值为需要访问节点的type,value则是我们的访问器函数,也就是类似这种结构:

代码语言:javascript
复制
create(context) {
    ...
  return {
    Program() {
        ...
    },
  };
},

4.3.2 代码实现

那么反正我们现在是明白了以下几点:

  • 通过node.type可以获取节点类型
  • 在create函数中,返回一个对象,对象的key为需要访问的节点类型,value为访问到该节点时需要执行的方法

接下来,我们就可以开始实现按照规则对导入语句进行排序的函数了。具体实现代码可以参考下面:

下面是该函数的代码实现:

代码语言:javascript
复制
function sortImports(importStatements, context) {
  // 用于存储排序后的导入语句
  const sortedImports = [];

  // 用于存储不同种类的导入语句
  const importGroups = {
    [ImportType.ThirdParty]: [],
    [ImportType.Absolute]: [],
    [ImportType.Relative]: [],
    [ImportType.Unknown]: [],
  };

  // 将导入语句分组和分类
  for (const importStatement of importStatements) {
    const importPath = importStatement.source.value;
    const importType = getImportType(importPath, context);
    importGroups[importType].push(importStatement);
  }

  // React相关导入必须放在第一位
  const reactImports = importGroups[ImportType.ThirdParty].filter(
    (importStatement) => ReactImportRegex.test(importStatement.source.value)
  );
  if (reactImports.length > 0) {
    sortedImports.push(reactImports);
  }

  // 按照规则和种类对导入语句进行排序
  const sortedAbsoluteImports = sortAbsoluteImports(
    importGroups[ImportType.Absolute]
  );
  const sortedRelativeImports = sortRelativeImports(
    importGroups[ImportType.Relative]
  );
  const sortedThirdPartyImports = sortThirdPartyImports(
    importGroups[ImportType.ThirdParty].filter(
      (importStatement) =>
        !ReactImportRegex.test(importStatement.source.value)
    )
  );
  const sortedUnknownImports = importGroups[ImportType.Unknown];

  const sortMethodTask = [
    sortedThirdPartyImports,
    sortedAbsoluteImports,
    sortedRelativeImports,
    sortedUnknownImports,
  ];

  for (const importStatements of sortMethodTask) {
    // 用于存储不同方式的导入语句
    const importMethods = {
      [ImportMethod.Destructuring]: [],
      [ImportMethod.Default]: [],
      [ImportMethod.Namespace]: [],
      [ImportMethod.Mixed]: [],
      [ImportMethod.SideEffect]: [],
    };
    const resultList = [];
    for (const importStatement of importStatements) {
      const importMethod = getImportMethod(importStatement);
      importMethods[importMethod].push(importStatement);
    }
    resultList.push(...importMethods[ImportMethod.SideEffect]);
    resultList.push(...importMethods[ImportMethod.Default]);
    resultList.push(...importMethods[ImportMethod.Namespace]);
    resultList.push(...importMethods[ImportMethod.Destructuring]);
    resultList.push(...importMethods[ImportMethod.Mixed]);
    sortedImports.push(resultList);
  }

  return sortedImports;
}

在这个函数中,我们使用了一些辅助函数,以便判断导入语句的种类、方式和规则。这些辅助函数的代码如下:

代码语言:javascript
复制
function getImportType(importPath, context) {
  if (/^\.\//.test(importPath)) {
    // 相对路径
    return ImportType.Relative;
  } else if (/^@\//.test(importPath)) {
    // 绝对路径
    return ImportType.Absolute;
  } else if (
    context
      .getScope()
      .through.some((ref) => ref.identifier.name === importPath)
  ) {
    // 未知类型
    return ImportType.Unknown;
  } else {
    // 第三方库
    return ImportType.ThirdParty;
  }
}

function getImportMethod(importStatement) {
  const { specifiers } = importStatement;
  if (
    specifiers.some(
      (specifier) => specifier.type === "ImportDefaultSpecifier"
    )
  ) {
    // 默认导入
    return ImportMethod.Default;
  } else if (
    specifiers.some(
      (specifier) => specifier.type === "ImportNamespaceSpecifier"
    )
  ) {
    // 命名导入
    return ImportMethod.Namespace;
  } else if (specifiers.length === 0) {
    // 无副作用导入
    return ImportMethod.SideEffect;
  } else if (
    specifiers.every((specifier) => specifier.type === "ImportSpecifier")
  ) {
    // 解构导入
    return ImportMethod.Destructuring;
  } else {
    // 混合导入
    return ImportMethod.Mixed;
  }
}

function sortAbsoluteImports(absoluteImports) {
  return absoluteImports.sort((a, b) => {
    const aPath = a.source.value;
    const bPath = b.source.value;

    if (aPath < bPath) {
      return -1;
    } else if (aPath > bPath) {
      return 1;
    } else {
      return 0;
    }
  });
}

function sortRelativeImports(relativeImports) {
  return relativeImports.sort((a, b) => {
    const aPath = a.source.value;
    const bPath = b.source.value;

    if (aPath < bPath) {
      return -1;
    } else if (aPath > bPath) {
      return 1;
    } else {
      return 0;
    }
  });
}

function sortThirdPartyImports(thirdPartyImports) {
  return thirdPartyImports.sort((a, b) => {
    const aPath = a.source.value;
    const bPath = b.source.value;

    if (aPath < bPath) {
      return -1;
    } else if (aPath > bPath) {
      return 1;
    } else {
      return 0;
    }
  });
}

function sortImports(importStatements, context) {
  // 用于存储排序后的导入语句
  const sortedImports = [];

  // 用于存储不同种类的导入语句
  const importGroups = {
    [ImportType.ThirdParty]: [],
    [ImportType.Absolute]: [],
    [ImportType.Relative]: [],
    [ImportType.Unknown]: [],
  };

  // 将导入语句分组和分类
  for (const importStatement of importStatements) {
    const importPath = importStatement.source.value;
    const importType = getImportType(importPath, context);
    importGroups[importType].push(importStatement);
  }

  // React导入必须放在第一位
  const reactImports = importGroups[ImportType.ThirdParty].filter(
    (importStatement) => ReactImportRegex.test(importStatement.source.value)
  );
  if (reactImports.length > 0) {
    sortedImports.push(reactImports);
  }

  // 按照规则和种类对导入语句进行排序
  const sortedAbsoluteImports = sortAbsoluteImports(
    importGroups[ImportType.Absolute]
  );
  const sortedRelativeImports = sortRelativeImports(
    importGroups[ImportType.Relative]
  );
  const sortedThirdPartyImports = sortThirdPartyImports(
    importGroups[ImportType.ThirdParty].filter(
      (importStatement) =>
        !ReactImportRegex.test(importStatement.source.value)
    )
  );
  const sortedUnknownImports = importGroups[ImportType.Unknown];

  // 将排序后的导入语句存储到数组中
  const sortMethodTask = [
    sortedThirdPartyImports,
    sortedAbsoluteImports,
    sortedRelativeImports,
    sortedUnknownImports,
  ];

  for (const importStatements of sortMethodTask) {
    // 用于存储不同方式的导入语句
    const importMethods = {
      [ImportMethod.Destructuring]: [],
      [ImportMethod.Default]: [],
      [ImportMethod.Namespace]: [],
      [ImportMethod.Mixed]: [],
      [ImportMethod.SideEffect]: [],
    };
    const resultList = [];
    for (const importStatement of importStatements) {
      const importMethod = getImportMethod(importStatement);
      importMethods[importMethod].push(importStatement);
    }
    resultList.push(...importMethods[ImportMethod.SideEffect]);
    resultList.push(...importMethods[ImportMethod.Default]);
    resultList.push(...importMethods[ImportMethod.Namespace]);
    resultList.push(...importMethods[ImportMethod.Destructuring]);
    resultList.push(...importMethods[ImportMethod.Mixed]);
    sortedImports.push(resultList);
  }

  return sortedImports;
}

最后,我们需要将该函数与ESLint集成,以便在规则中使用它。在ESLint规则中,我们可以使用context.getSourceCode()方法获取源代码,并使用sortImports()函数对导入语句进行排序。下边这是一份完整的代码。

代码语言:javascript
复制
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
  meta: {
    type: "suggestion",
    docs: {
      description: "force the import order",
      recommended: false,
      url: null,
    },
    fixable: "code",
    schema: [],
    messages: {
      "import-sorter": "Import statements are not sorted",
    },
  },

  create(context) {
    function getImportType(importPath, context) {
      if (/^\.\//.test(importPath)) {
        // 相对路径
        return ImportType.Relative;
      } else if (/^@\//.test(importPath)) {
        // 绝对路径
        return ImportType.Absolute;
      } else if (
        context
          .getScope()
          .through.some((ref) => ref.identifier.name === importPath)
      ) {
        // 未知类型
        return ImportType.Unknown;
      } else {
        // 第三方库
        return ImportType.ThirdParty;
      }
    }

    function getImportMethod(importStatement) {
      const { specifiers } = importStatement;
      if (
        specifiers.some(
          (specifier) => specifier.type === "ImportDefaultSpecifier"
        )
      ) {
        // 默认导入
        return ImportMethod.Default;
      } else if (
        specifiers.some(
          (specifier) => specifier.type === "ImportNamespaceSpecifier"
        )
      ) {
        // 命名导入
        return ImportMethod.Namespace;
      } else if (specifiers.length === 0) {
        // 无副作用导入
        return ImportMethod.SideEffect;
      } else if (
        specifiers.every((specifier) => specifier.type === "ImportSpecifier")
      ) {
        // 解构导入
        return ImportMethod.Destructuring;
      } else {
        // 混合导入
        return ImportMethod.Mixed;
      }
    }

    function sortAbsoluteImports(absoluteImports) {
      return absoluteImports.sort((a, b) => {
        const aPath = a.source.value;
        const bPath = b.source.value;

        if (aPath < bPath) {
          return -1;
        } else if (aPath > bPath) {
          return 1;
        } else {
          return 0;
        }
      });
    }

    function sortRelativeImports(relativeImports) {
      return relativeImports.sort((a, b) => {
        const aPath = a.source.value;
        const bPath = b.source.value;

        if (aPath < bPath) {
          return -1;
        } else if (aPath > bPath) {
          return 1;
        } else {
          return 0;
        }
      });
    }

    function sortThirdPartyImports(thirdPartyImports) {
      return thirdPartyImports.sort((a, b) => {
        const aPath = a.source.value;
        const bPath = b.source.value;

        if (aPath < bPath) {
          return -1;
        } else if (aPath > bPath) {
          return 1;
        } else {
          return 0;
        }
      });
    }

    function sortImports(importStatements, context) {
      // 用于存储排序后的导入语句
      const sortedImports = [];

      // 用于存储不同种类的导入语句
      const importGroups = {
        [ImportType.ThirdParty]: [],
        [ImportType.Absolute]: [],
        [ImportType.Relative]: [],
        [ImportType.Unknown]: [],
      };

      // 将导入语句分组和分类
      for (const importStatement of importStatements) {
        const importPath = importStatement.source.value;
        const importType = getImportType(importPath, context);
        importGroups[importType].push(importStatement);
      }

      // React导入必须放在第一位
      const reactImports = importGroups[ImportType.ThirdParty].filter(
        (importStatement) => ReactImportRegex.test(importStatement.source.value)
      );
      if (reactImports.length > 0) {
        sortedImports.push(reactImports);
      }

      // 按照规则和种类对导入语句进行排序
      const sortedAbsoluteImports = sortAbsoluteImports(
        importGroups[ImportType.Absolute]
      );
      const sortedRelativeImports = sortRelativeImports(
        importGroups[ImportType.Relative]
      );
      const sortedThirdPartyImports = sortThirdPartyImports(
        importGroups[ImportType.ThirdParty].filter(
          (importStatement) =>
            !ReactImportRegex.test(importStatement.source.value)
        )
      );
      const sortedUnknownImports = importGroups[ImportType.Unknown];

      // 将排序后的导入语句存储到数组中
      const sortMethodTask = [
        sortedThirdPartyImports,
        sortedAbsoluteImports,
        sortedRelativeImports,
        sortedUnknownImports,
      ];

      for (const importStatements of sortMethodTask) {
        // 用于存储不同方式的导入语句
        const importMethods = {
          [ImportMethod.Destructuring]: [],
          [ImportMethod.Default]: [],
          [ImportMethod.Namespace]: [],
          [ImportMethod.Mixed]: [],
          [ImportMethod.SideEffect]: [],
        };
        const resultList = [];
        for (const importStatement of importStatements) {
          const importMethod = getImportMethod(importStatement);
          importMethods[importMethod].push(importStatement);
        }
        resultList.push(...importMethods[ImportMethod.SideEffect]);
        resultList.push(...importMethods[ImportMethod.Default]);
        resultList.push(...importMethods[ImportMethod.Namespace]);
        resultList.push(...importMethods[ImportMethod.Destructuring]);
        resultList.push(...importMethods[ImportMethod.Mixed]);
        sortedImports.push(resultList);
      }

      return sortedImports;
    }

    return {
      Program() {
        const sourceCode = context.getSourceCode();
        const importStatements = sourceCode.ast.body.filter(
          (node) => node.type === "ImportDeclaration"
        );

        if (importStatements.length <= 1) {
          return;
        }
        const comments = sourceCode.getAllComments();
        const firstImportIndex = importStatements[0].range[0];
        const leadingComments = comments.filter(
          (comment) => comment.range[1] < firstImportIndex
        );
        const hasDisable = leadingComments.some(
          (comment) =>
            comment.value === " eslint-disable " ||
            comment.value.includes("eslint-disable import-sorter")
        );
        if (hasDisable) {
          return;
        }

        const sortedImports = sortImports(importStatements, context);
        const importCode = sortedImports
          .filter((result) => result.length > 0)
          .map((result) =>
            result
              .map((importStatement) => sourceCode.getText(importStatement))
              .join("\n")
          )
          .join("\n\n");

        const oldImportCode = importStatements
          .map((importStatement) => sourceCode.getText(importStatement))
          .join("");
        if (
          oldImportCode.replaceAll("\n", "") === importCode.replaceAll("\n", "")
        )
          return;
        const start = importStatements[0].range[0];
        const end = importStatements[importStatements.length - 1].range[1];
        context.report({
          loc: { start, end },
          messageId: "import-sorter",
          fix(fixer) {
            return fixer.replaceTextRange([start, end], importCode);
          },
        });
      },
    };
  },
};

在这个规则中,我们使用了ESLint的fixable属性,以便在规则报告中提供自动修复的选项。如果用户选择修复,ESLint将使用sortImports()函数对导入语句进行排序,并替换源代码中的导入语句。替换的时候,我们用到了前文说过的context.report 在report方法中声明并完成一个fix函数,fix函数中可以返回多个fixer完成修复。

4.3.3 引入参数

显然,并不是所有的项目都是以@/ 表示绝对路径,所以我们以这个需求作为示例,演示如何给eslint规则引入参数。

首先,我们需要修改meta ,我们添加了用于定义绝对路径前缀的可选项。

代码语言:javascript
复制
meta: {
    type: "suggestion",
    docs: {
      description: "force the import order",
      recommended: false,
      url: null,
    },
    fixable: "code",
    schema: [
      // custom absolute path prefix
      {
        type: "object",
        properties: {
          absolutePathPrefix: {
            type: "string",
          },
        },
        additionalProperties: false,
      },
    ],
    messages: {
      "import-sorter": "import statements are not sorted",
    },
  },

这个例子中,我们为absolutePathPrefix指定了值"@/src",表示项目中的绝对路径都以"@/src"开头。在规则代码中,我们使用该值来判断导入语句的类型。这使得该规则适用于不同的

代码语言:javascript
复制
{
  "rules": {
    "import-sorter": [
      "error",
      {
        "absolutePathPrefix": "@/src"
      }
    ]
  }
}

这个例子中,我们为absolutePathPrefix指定了值"@/src",表示项目中的绝对路径都以"@/src"开头。在规则代码中,我们使用该值来判断导入语句的类型。这使得该规则适用于不同的项目。另外,我们也可以尝试将不同的排序逻辑抽象为单独的函数,以便更好地重用和测试。同时,我们也可以添加更多的选项,例如允许用户自定义排序规则,或者在某些情况下忽略某些导入语句。

4.4 调试

在开发过程中你或许会发现,为什么你写的规则没有生效?

原因很简单,规则没有配置,记得extends 属性么?我们应当导出一份推荐属性用来方便插件使用。下边是一个简单的例子:

  • lib/index.js
代码语言:javascript
复制
module.exports.configs = {
    recommended: {
        rules: {
            "super-hero/import-sorter": "error"
        }
    }
}

这之后要使用,只需要在你的.eslintrc.js配置文件中,添加以下内容来使用这个插件和规则:

代码语言:javascript
复制
module.exports = {
  plugins: ["super-hero"],
  extends: ["plugin:super-hero/recommended"],
  rules: {
    // 在这里添加其他规则
  },
};

这将启用super-hero插件和其中的推荐规则。你也可以在rules中添加其他自定义规则。

解决了这个小插曲,我们试着进行调试,方便地调试有利于我们更好地开发。

4.4.1 eslint命令

代码语言:javascript
复制
npx eslint --fix ./xxx.ts

这方法就跟手打C编译器编译命令再启动一样,我们可以试试vscode eslint插件

4.4.2 eslint vscode插件

前端开发者们大多安装了该插件,我们可以设定插件的restart快捷键,在每次更新自定义插件时快速重启,该插件会在我们进入任意文件时自动执行一次eslint插件找到问题。

Untitled
Untitled

4.4.3 测试即调试

官方模版中的的mocha,本身即具备了调试能力,在其中完全可以打印、debugger。由于mocha提供了非常便携的--inspect-brk 选项,我们添加一行debug 命令

代码语言:javascript
复制
mocha tests --recursive --inspect-brk
Untitled
Untitled

此时,当弹出该消息,我们可以打开任意一个页面的Chrome的控制台。

Untitled
Untitled
Untitled
Untitled

单击上述按钮,我们就可以在熟悉的控制台debugger node代码了。使用时,可以在代码需要断点的地方输入debugger 即可调试。

5 总结

这份代码显然具有多个可改进的问题。

  • 我是通过手动获取comments节点的方式实现的通过/* eslint-disable */ 屏蔽规则,我也很困惑为什么该规则在import-sorter 上不生效,难道是因为我访问Program节点?我的其他没有访问Program节点,是可以通过disable 屏蔽的。
  • sortImports()函数中,我们将导入语句分组并排序,然后将它们保存到一个数组中。这种方法比较简单,但是效率可能不够高。
  • 这份代码的通用性不足,非常具有个性化,适合业务项目。通用化应开发更多如允许用户自定义排序顺序的选项。

通过eslint plugin的强制规范,我们可以让项目具备更强有效的规范性,一位新人将技术文档吃透的时间成本、导致代码混乱熵增加的程度,完全可以用代码的形式大幅降低与遏制,让技术文档不必形于markdown文档,而是贯彻在每一个角落。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-04-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 前言
  • 2 目标
  • 3 初始化
    • 3.1 yo eslint:plugin
      • 3.2 yo eslint:rule
        • 3.3 lib/rules/xxx.js
          • 3.3.1 meta
          • 3.3.2 create
        • 3.4 tests/lib/rules/xxx.js
        • 4 imports-sorters实现
          • 4.1 需求
            • 4.2 准备环境
              • 4.3 开工
                • 4.3.1 AST分析
                • 4.3.2 代码实现
                • 4.3.3 引入参数
              • 4.4 调试
                • 4.4.1 eslint命令
                • 4.4.2 eslint vscode插件
                • 4.4.3 测试即调试
            • 5 总结
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档