精华内容
下载资源
问答
  • 前端代码质量进阶:自定义 eslint 规则校验业务逻辑
    2018-05-02 08:35:39

    自定义 eslint 规则校验代码业务逻辑

    eslint 是 JavaScript 社区中主流的 lint 工具,提供的大量规则有效的保障了许多项目的代码质量。本文将介绍如何通过自定义 eslint 检查规则,校验项目中特有的一些业务逻辑,如 i18n、特殊作用域、特殊 API 使用规范性等。

    代码静态分析与 eslint

    代码静态分意指是不需要实际执行代码就能获取到程序中的部分信息并加以使用,lint 就是其中一种常见的实践,通常为检查代码中错误的写法或是不符合标准的代码风格。许多编程语言都自带 lint 工具,甚至直接将其植入到编译器中。

    但这一重要的功能对于 JavaScript 来说却是一大痛点,作为动态且弱类型的语言 JavaScript 没有编译阶段也就无从进行静态分析,这导致程序错误只能在运行时被发现,部分错误非常低级例如variable is undefined。而当程序变得更为复杂时,这类错误甚至难以在开发、测试阶段暴露,只会在用户实际使用的过程中遇到,造成严重的后果。

    为了弥补语言天生的弱点,社区开发出了一些 lint 工具,在所谓预编译阶段完成代码的静态分析检查,而 eslint 就是其中的佼佼者。现在社区已经普遍接受使用 eslint 作为代码规范工具,也延伸出了许多常用的规则与规则集。但实际上 eslint 拓展性极佳,我们还可以基于 eslint 提功的静态分析能力对代码进行业务逻辑的检查,本文将讲解一些笔者所在项目中的静态分析实践,以说明这一方案的适用场景和优缺点。

    eslint 基本原理

    首先快速说明 eslint 工作的基本流程,帮助理解它将给我们提供哪些方面的能力以及如何编写我们的自定义规则。

    配置规则与插件

    eslint 主要依靠配置决定执行哪些规则的校验,例如我们可以通过配置no-extra-semi决定是否需要写分号,这类规则中不包含具体的业务逻辑,而是对所有项目通用,因此会被集成在 eslint 的内置规则中。

    而还有一些规则也不包含业务逻辑,但只在部分项目场景中使用,如 React 相关的大量规则,那么显然不应该集成在内置规则中,但也应该自成一个集合。这种情况下 eslint 提供了另一种规则单位——插件,可以作为多个同类规则的集合被引入到配置中。

    如果我们准备自定义一些规则用于校验项目中的业务逻辑,那么也应该创建一套自用的插件,并将自用的规则都存放其中。推荐使用 eslint 的 yeoman generator 脚手架新建插件或规则,该脚手架能够生成插件项目的目录结构、规则文件、文档以及单元测试等模版,下文中我们将通过示例理解这些文件的的作用。

    JavaScript 解析

    如上文所说,要实现静态分析则需要自建一个预编译阶段对代码进行解析,eslint 也不例外。

    首先我们看看大部分编译器工作时的三个阶段:

    1. 解析,将未经处理的代码解析成更为抽象的表达式,通常为抽象语法树,即 AST。
    2. 转换,通过修改解析后的代码表达式,将其转换为符合预期的新格式。
    3. 代码生成,将转换后的表达式生成为新的目标代码。

    如果想快速的加深对编译器工作原理的理解,推荐阅读 the-super-tiny-compiler

    对于 eslint 而言,主要是将 JavaScript 代码解析为 AST 之后,再在遍历 AST 的过程中对代码进行各个规则的校验。因此 eslint 也有一个解析器用于将原始代码解析为特定的 AST,目前所使用的解析器是 eslint 基于 Acorn 开发的一个名为 Espree 的项目。而对于我们编写自定义规则来说更关心的是解析器生成的 AST 节点的结构,在阅读 eslint 文档之后会了解到包括 Espree 在内的许多编译器项目都需要一套 JavaScript 的 AST 规范,而为了保证规范的一致性以及实效性,社区共同维护了一套规范:estree

    在接下来讲解规则编写与执行的过程中,我们将直接引用 estree 的各种 AST 结构。

    规则的执行

    eslint 中一般一个规则存放在一个文件中,以 module 的形式导出并挂载,其结构如下:

    module.exports = {
      meta: {
        docs: {
          description: 'disallow unnecessary semicolons',
          category: 'Possible Errors',
          recommended: true,
          url: 'https://eslint.org/docs/rules/no-extra-semi',
        },
        fixable: 'code',
        schema: [], // no options
      },
      create: function(context) {
        return {
          // callback functions
        };
      },
    };

    其中meta部分主要包括规则的描述、类别、文档地址、修复方式以及配置下 schema 等信息,对于项目中自用的规则来说可以只填写基本的描述和类别,其余选项在有需要时再根据文档补充,并不会影响规则的检验逻辑。

    create则需要定义一个函数用于返回一个包含了遍历规则的对象,并且该函数会接收context对象作为参数,context对象中除了包含report等报告错误的方法之外,还提供了许多帮助方法,可以简化规则的编写。下文中我们会通过几个示例理解create函数的使用方式,但首先可以通过一段代码建立初步的印象:

    module.exports = {
      create: function(context) {
        // declare the state of the rule
        return {
          ReturnStatement: function(node) {},
          'FunctionExpression:exit': function(node) {},
          'ArrowFunctionExpression:exit': function(node) {},
        };
      },
    };

    在这段代码中我们可以看到create返回的所谓“包含了遍历规则的对象”的基本结构。对象的 value 均为一个接收当前 AST 节点的函数,而 key 则是 eslint 的节点 selector。selector 分为两部分,第一部分为必须声明的 AST 节点类型,如ReturnStatementFunctionExpression。第二部分则是可选的:exit标示,因为在遍历 AST 的过程中会以“从上至下”再“从下至上”的顺序经过节点两次,selector 默认会在下行的过程中执行对应的访问函数,如果需要再上行的过程中执行,则需要添加:exit

    那么 eslint 解析出的 AST 有哪些节点类型,每种节点的数据结构又是什么,则需要通过查看上文提到的 estree 定义文档进行了解。

    适用场景与示例

    接下来我们会看到 eslint 自定义规则校验的一些具体示例,但首先我们先要明确它的适用场景以及与一些常见代码 QA 手段的异同。

    适用场景

    我们可以通过以下方法判断一个工具的质量:

    工具质量 = 工具节省的时间 / 开发工具消耗的时间

    对于静态分析来说,要想提高“工具节省的时间”,应该要让检查的规则尽量覆盖全局性的且经常发生的问题,如使用最为广泛的检查:是否使用了未定义的变量。同时还需要考虑当问题发生后 debug 所消耗的时间,例如有的项目有 i18n 需求,而在代码的个别地方又直接使用了中文的字符串,虽然问题很小,但是人工测试覆盖却很麻烦,如果能够通过工具进行覆盖,那么原来用于 debug 的时间也应该归入“工具节省的时间”当中。

    另一方面则是对比“开发工具消耗的时间”,首先要强调通过静态分析去对逻辑进行判断,不论是学习成本还是实际编写成本都较高,如果一类问题可以通过编写简单的单元测试进行覆盖,那么应该优先考虑使用单元测试。但有的时候代码逻辑对外部依赖较多,单元测试的开销很大,例如我们有一段 e2e 测试的代码,需要在目标浏览器环境中执行一段代码,但是常规的 eslint 并不能判断某个函数中的代码实际执行在另一个作用域下,部分检查就会失效,例如浏览器运行时引用的变量实际定义在本地运行时中,eslint 无法察觉。而如果通过单元测试覆盖,则需要实际运行对应的 e2e 代码,或者 mock 其执行环境的各种依赖,都是非常重的工作,取舍之下通过静态分析覆盖会事半功倍。

    最后还需要考虑到使用体验,许多编辑器都有 eslint 的集成插件,可以在编程的过程中实时检测各个规则,在实时性方面远强于单元测试等 QA 手段的使用体验。

    示例 1:i18n

    许多项目都有国际化的需求,因此项目中的文案需要避免直接使用中文,常见的方案包括用变量代替字符串或者使用全局的翻译函数处理字符串,例如:

    // 错误:直接只用中文字符串
    console.log('中文');
    // 使用变量
    const currentLocale = 'cn';
    const T = {
      str_1: {
        cn: '中文',
      },
    };
    console.log(T.str_1[currentLocale]);
    // 使用翻译函数处理
    console.log(t('中文'));

    如果出现了直接使用中文字符串的错误,其实在代码运行过程中也不会有任何错误提示,只能靠 code review 和人工观察测试来发现。我们尝试自定义一条 eslint 规则解决它,此处假设项目中使用的是将所有中文内容存放在一个变量中,其余地方直接引用变量的方法。

    const SYMBOL_REGEX = /[\u3002\uff1b\uff0c\uff1a\u201c\u201d\uff08\uff09\u3001\uff1f\u300a\u300b]/;
    const WORD_REGEX = /[\u3400-\u9FBF]/;
    
    function hasChinese(value) {
      return WORD_REGEX.test(value) || SYMBOL_REGEX.test(value);
    }
    
    module.exports = {
      create: function(context) {
        return {
          Literal: function(node) {
            const { value } = node;
            if (hasChinese(value)) {
              context.report({
                node,
                message: '{{ str }} contains Chinese, move it to T constant.',
                data: {
                  str: node.value,
                },
              });
            }
          },
        };
      },
    };

    在这段代码中,我们在create里遍历所有Literal类型节点,因为我们需要检查的对象是所有字符串。根据 estree 的定义,我们会知道Literal类型阶段结构如下:

    interface Literal <: Expression {
        type: "Literal";
        value: string | boolean | null | number | RegExp;
    }

    那么需要做的就是判断该节点的 value 是否包含中文,在这里我们用的是正则表达式进行判断,当含有中文字符或标点时,就调用context.report方法报告一个错误。在应用这条规则之后,全局所有直接使用中文字符串的代码都会报错,只需要对统一存放中文的变量T所在的代码部分禁用这条规则,就可以避免误判。

    在笔者所在项目中我们使用的是“通过翻译函数处理”的方式,所以规则会更为复杂一些,需要判断当前字符串的父节点是否为我们的翻译函数,Espree 会在每个节点上都记录对应的父节点信息,因此我们可以通过类似node.parent.callee.name === 't'这样的方式进行判断。不过实际情况中还需要做更安全、全面的判断,例如正确识别这样的使用方式t('你好' + '世界'),后一个字符串的父节点是加法运算符。

    在这个示例中我们主要理解了遍历函数的工作方式以及如何使用合理的节点类型实现需求,因此不再过度展开实际场景中的细节实现。不过相信读者已经可以感受到写一条自定义规则需要非常全面的考虑代码中的各类场景,这也是为什么 eslint 要求自定义规则要遵循 TDD 的开发方式,用足够多的单元测试保证规则使用时符合预期,在最后我们会介绍 eslint 提供的单测框架。

    示例 2:特殊作用域

    首先构建一个场景用于展示这类规则:

    不论是以及非常成熟的 Node.JS + selenium 体系还是较新的 headless chrome 生态,这类端到端工具一般都会提供在目标浏览器上执行一段 JavaScript 的能力,例如这样:

    client.execute(
      function(foo, bar) {
        document.title = foo + bar;
      },
      ['foo', 'bar']
    );

    client.execute方法接收两个参数,第一个为在浏览器端执行的函数,第二个则是从当前代码传递给执行函数的参数,而浏览器端也只能使用传递的参数而不能直接使用当前代码中的变量。在这种场景下,很容易出现类似这样的问题:

    const foo = 'foo';
    const bar = 'bar';
    client.execute(function() {
      document.title = foo + bar;
    });

    对于 eslint 来说并不知道document.title = foo + bar;将在浏览器端的作用域中执行,而又发现有同名变量foobar被定义在当前代码中,则不会认为这段代码有错误,这种情况下我们就可以尝试自定义规则来对这个特殊场景做检查:

    module.exports = {
      create: function(context) {
        return {
          'Program:exit': function() {
            const globalScope = context.getScope();
            const stack = globalScope.childScopes.slice();
    
            while (stack.length) {
              const scope = stack.pop();
              stack.push.apply(stack, scope.childScopes);
    
              if (scope.block.parent.callee.property.name === 'execute') {
                const undefs = scope.through.forEach((ref) =>
                  context.report({
                    node: ref.identifier,
                    message: "'{{name}}' is not defined.",
                    data: ref.identifier,
                  })
                );
              }
            }
          },
        };
      },
    };

    以上代码中继续省略一些过于细节的实现,例如判断子作用域是否为client.execute的第一个参数以及将浏览器中的全局变量加入未定义变量的白名单等等,重点关注 eslint 为我们提供的一些帮助方法。

    这次我们的节点选择器为Program:exit,也就是下行完毕、开始上行完整的 AST 时执行我们的自定义检查,Program类型的节点对应的是完整的源码树,在 eslint 中即是当前文件。

    在检查时,首先我们使用context.getScope获取了当前正在遍历的作用域,又由于我们处在Program节点中,这个作用域即为这个代码文件中的最高作用域。之后我们构建一个栈,通过不断地把 childScopes 压入栈中在读取出来的方式,实现递归的访问到所有的子作用域。

    之后在处理每个子作用域时,都做了一个简单的判断(同样是简化过后的版本),来确定该作用域是否为我们需要独立判断的client.execute方法中第一个函数内的作用域。

    当找到该函数内的作用域之后,我们就可以使用scope对象上的各种方法进行判断了。事实上作用域是静态分析中较为复杂的部分,如果完全独立的去判断作用域中的引用等问题相对复杂,好在 eslint 对外暴露了 scope manager interface,让我们可以最大程度的复用封装好的各类作用域接口。

    在 scope manager interface 中可以看到scope.through方法的描述:

    The array of references which could not be resolved in this scope.

    正是我们需要的!所以最后只需要简单的遍历scope.through返回的未定义引用数组,就可以找到该作用域下所有的未定义变量。

    通过这个示例,可以看出 eslint 本身已经对许多常用需求做了高阶的封装,直接复用可以大大缩减“开发工具消耗的时间”。

    示例 3:保证 API 使用规范

    继续构建一个场景:假如我们在业务中我们有一个内部 API "Checker",用于校验某些操作(action)是否可执行,而校验的方式是判断 action 对应的规则(rule)是否全部通过,代码如下:

    const checker = new Checker({
      rules: {
        ruleA(value) {},
        ruleB(value) {},
      },
      actions: {
        action1: ['ruleA', 'ruleB'],
        action2: ['ruleB'],
      },
    });

    在 Checker 这个 API 使用的过程中,我们需要:

    1. 所有 action 依赖的 rule 都在rules属性中被定义。
    2. 所有定义的 rule 都被 action 使用。

    由于 action 和 rule 的关联性只靠 action value 数组中的字符串名称与 rule key 值保持一致来维护,所以第一条要求如果出了问题只能在运行时发现错误,而第二条要求甚至不会造成任何错误,但在长期的迭代下可能会遗留大量无用代码。

    当然这个场景我们很容易通过单元测试进行覆盖,但如果 Checker 是一个在项目各种都会分散使用的 API,那么单元测试即使有一个通用的用例,也需要开发者手动导出 checker 再引入到测试代码中去,这本身就存在一定遗漏的风险。

    从开发体验出发,我们也尝试用 eslint 的自定义规则完成这个需求,实现一个实时的 Checker API 使用方式校验。

    首先我们需要在静态分析阶段分辨代码中的一个 Class 是否为 Checker Class,从而进一步做校验,单纯从变量名称判断过于粗暴,容易发生误判;而从 Class 来源分析很可能出现跨文件引用的情况,又过于复杂。所以我们借鉴一些编程语言中处理类似场景的做法,在需要编译器特殊处理的地方加一些特殊的标记帮助编译器定位,例如这样:

    // [action-checker]
    const checker = new Checker({});

    在构造 checker 实例的前一行写一个注释// [action-checker],表明下一行开始的代码是使用了 Checker API,在这基础上,我们就可以开始编写 eslint 规则:

    const COMMENT_MARKER = '[action-checker]';
    
    function getStartLine(node) {
      return node.loc.start.line;
    }
    
    module.exports = {
      create: function(context) {
        const sourceCode = context.getSourceCode();
        const markerLines = {};
    
        return {
          Program: function() {
            const comments = sourceCode.getAllComments();
            comments.forEach((comment) => {
              if (comment.value.trim() === COMMENT_MARKER) {
                markerLines[getStartLine(comment)] = comment;
              }
            });
          },
          ObjectExpression: function(expressionNode) {
            const startLine = getStartLine(expressionNode);
            if (markLines[startLine - 1]) {
              // check actions and rules
            }
          },
        };
      },
    };

    在这个示例中,我们使用了context.getSourceCode获取 sourceCode 对象,和上个例子中的 scope 类似,也是 eslint 封装过后的接口,例如可以继续通过sourceCode.getAllComments获取代码中的所有注释。

    为了实现通过注释定位 checker 实例的目的,我们在markLines对象中存储了带有特殊标记的注释的行数,获取行数的方式则是node.loc.start.line。这里的loc也是 eslint 给各个 AST 节点增加的一个重要属性,包含了节点对应代码在源代码中的坐标信息。

    之后遍历所有ObjectExpression类型节点,通过markLines中存储的位置信息,确定某个ObjectExpression节点是否为我们需要校验的 checker 对象,再根据 estree 中定义的ObjectExpression结构,找到我们需要的 actions values 和 rules keys 进行比较,此处不对细节处理做进一步展开。

    这个示例说明注释作为静态分析中非常重要的元素有很好的利用价值,许多项目也提供从一定格式(例如 JSDoc)的注释中直接生成文档的功能,也是代码静态分析常见的应用,除了示例中用到的sourceCode.getAllComments可以获取所有注释,还提供sourceCode.getJSDocComment这样只获取 JSDoc 类型注释的方法。

    总而言之,基于 eslint 提供的强大框架,我们可以拓展出很多极大提高开发体验和代码质量的用法。

    杂项

    借鉴社区

    eslint 本身提供的功能很强但也很多,光从文档中不一定能找到最适用的方法,而 eslint 本身已经有大量的 通用规则,很多时候直接从相近的规则中学习会更加有效。例如示例 2 中对作用域的判断就是从社区的通用规则no-undef中借鉴了很多大部分思路。

    TDD

    上文提到,静态分析需要非常全面的考虑编译器会遇到的各类代码,但如果每次编写规则都需要在一个很大的 code base 中进行测试效率也很低。因此 eslint 提倡用测试驱动开发的方式,先写出对规则的预期结果,再实现规则。

    如果通过上文提到的 eslint yeoman 脚手架新建一个规则模版,会自动生成一个对应的测试文件。以示例 1 为例,内容如下:

    const rule = require('../../../lib/rules/use-t-function');
    const RuleTester = require('eslint').RuleTester;
    
    const parserOptions = {
      ecmaVersion: 8,
      sourceType: 'module',
      ecmaFeatures: {
        experimentalObjectRestSpread: true,
        jsx: true,
      },
    };
    
    const ruleTester = new RuleTester({ parserOptions });
    ruleTester.run('use-t-function', rule, {
      valid: [
        { code: 'fn()' },
        { code: '"This is not a chinese string."' },
        { code: "t('名称:')" },
        { code: "t('一' + '二' + '三')" },
      ],
    
      invalid: [
        {
          code: '<Col xs={6}>名称:</Col>',
          errors: [
            {
              message: '名称: contains Chinese, use t function to wrap it.',
              type: 'Literal',
            },
          ],
        },
      ],
    });

    核心的部分是require('eslint').RuleTester提供的单测框架 Class,传入一些参数例如解析器配置之后就可以实例化一个 ruleTester。实际执行时需要提供足够的 valid 和 invalid 代码场景,并且对 invalid 类型代码报告的错误信息做断言,当所有测试用例通过后,就可以认为规则的编写符合预期了。

    完整示例代码

    自定义 eslint 规则在我们的实际项目中已经有所应用,示例中的实际完整规则代码都存放在公网 Github 仓库中,如果对文中跳过的细节实现感兴趣可以自行翻看。

    更多相关内容
  • 使用eslint和editorconfig规范代码 为什么要用这些: 代码规范有利于团队协作 纯手工规范耗时耗力而且不能保证准确性 能配合编辑器自动提醒错误,提高开发效率 eslint 随着ECMAScript版本一直更新的Js lint工具,...
  • 自定义eslint

    2021-07-16 17:19:47
    背景:项目里面规定只有client.ts可以引入...首先创建一个eslint(自认为)项目 npm i -g yo npm i -g generator-eslint // 创建一个plugin yo eslint:plugin // 创建一个规则 yo eslint:rule 完后是这样的,现在可以

    背景:项目里面规定只有client.ts可以引入node模块与electron模块,其余文件引用报错
    这就要求我们去创建一个自己的eslint规则了,这里设计到AST树等知识,可以参考https://m.sohu.com/a/330228148_463970
    首先创建一个eslint(自认为)项目

    npm i -g yo
    npm i -g generator-eslint
    // 创建一个plugin
    yo eslint:plugin
    // 创建一个规则
    yo eslint:rule
    

    在这里插入图片描述在这里插入图片描述在这里插入图片描述完后是这样的,现在可以打开工具看ast树了,地址https://astexplorer.net/,
    在这里插入图片描述可以慢慢看里面的参数
    然后在rule的choose-node.js这样写,根据自己的需求来哈

     module.exports = {
         meta: {
             docs: {
                 description: "choose-node",
                 category: "Fill me in",
                 recommended: false
             },
             fixable: null,  // or "code" or "whitespace"
             schema: [] //这里好像可以接受参数类型传参,create可以通过context.options接收到项目的eslintrc.js rules里面的参数数组,后面的博客会研究到
         },
     
         create: function(context) {
             const name = context.getFilename();
             function getFunctionLoc(node) {
                 return {
                   start: node.source.start,
                   end: node.source.end,
                   name,
                 };
             }
             function isImport(node){
                 const module = ['fs', 'os', 'http', 'child_press', 'events', 'path', 'election'] // 刚会,所以暂时写死,可以在eslintrc.js的rule传参,这里通过context.options接受,暂时没研究明白
                 return module.some(item => {
                     return node.source.value == item
                 })
             }
             return {
                 ImportDeclaration: (node) => {  //这里的ImportDeclaration一定要和工具的函数名对上啊,不然没用,这是import的,const let var就是VariableDeclaration这个了
                     let {start, end, name} = getFunctionLoc(node)
                     let isModule = isImport(node)
                     if (!name.match('client') && isModule) {
                       context.report({
                         loc: {start, end},
                         node,
                         message: "不能import Node和electron模块",
                       });
                     }
                 },
             };
         }
     };
    

    测试小弟暂时没写,在test里面的choose-node.js里面,当然,我写了一个不能import,那么require呢?
    所以我又创建了一个rule choose-electron,其实我已开始是choose-node是无论是import 还是require Node的模块我都放里面的,后面为了偷下懒,少写几行代码就给弄成choose-node不能import node electron的模块,choose-electron不能require node electron的模块,大家别学我,规范重要
    在这里插入图片描述

    我看有npm-link本地测试的,但我用的是笨方法,直接上传到npm,然后拉下来,改node_module里面的eslint-plugin-zlint测试,哈哈对了,项目名要eslint-plugin开头,不然没用,上传npm的话去注册一下,然后npm login,填写账号密码邮箱,报错的原因之一是你换源成淘宝的了,换回来,然后npm publish,这样就上传到npm平台了,下次改了在package.json换下版本,可以覆盖。
    然后是怎么用,这是你的项目,例如我创建了一个空的vue项目
    在这里插入图片描述eslint-plugin-可以省略,我的包在npm上是eslint-plugin-zlint,因为package.json的name是这个(之前的eslint项目)
    rules那里可以传参,咋传暂时不会,可以把不让用哪些node模块传过去,省的写死啥的。。。
    然后0是不管,1是报警,2是报错
    看结果 client.ts没报错
    在这里插入图片描述
    其他的报错 a.js client.js
    在这里插入图片描述
    在这里插入图片描述会点皮毛,如有错误,还请指正

    展开全文
  • 如何自定义Eslint插件

    2021-10-21 11:14:00
    如何自定义Eslint插件一. 准备工作二. 编写自定义组件三. 开启本地调试 一. 准备工作 我们需要基于generator-eslint来进行快速开发Eslint自定义插件。 1.下载Yeoman generator: npm i -g yo 2.然后: npm i -g ...

    一. 准备工作

    我们需要基于generator-eslint来进行快速开发Eslint自定义插件。

    1.下载Yeoman generator:

    npm i -g yo
    

    2.然后:

    npm i -g generator-eslint
    

    3.我们准备一个文件夹,可以取名为My-Eslint,在该文件夹的路径下通过命令生成创建一个插件Plugin,然后根据提示来输入对应的内容。

    yo eslint:plugin
    

    案例如下:ID也就是你的插件的名称,生成后可以在package.json文件里查看name属性,默认以eslint-plugin-为开头。
    在这里插入图片描述
    4.紧接着在相同路径输入命令,生成对应的规则包:

    yo eslint:rule
    

    案例如下:
    在这里插入图片描述
    5.生成的目录结构如下:index.js相当于入口文件,所有rules目录下定义的规则都会通过该入口来暴露。
    在这里插入图片描述

    二. 编写自定义组件

    从上一个环节,可以发现我们定义了个max-params.js文件,我们来看下他的内容:
    参考官网

    module.exports = {
        meta: {
            docs: {
                description: "一个方法的参数最大数量不能超过3个",
            },
        },
        create: function (context) {
            function getFunctionParamsLoc(node) {
                const paramsLength = node.params.length;
                return {
                    start: node.params[0].loc.start,
                    end: node.params[paramsLength - 1].loc.end,
                };
            }
            return {
                FunctionDeclaration: (node) => {
                    if (node?.params?.length > 3) {
                        context.report({
                            loc: getFunctionParamsLoc(node),
                            node,
                            message: "一个方法的参数最大数量不能超过3个",
                        });
                    }
                },
            };
        },
    };
    

    然后该规则的对应test文件内容如下(可以不写):(tests/lib/rules目录下)

    var ruleTester = new RuleTester();
    ruleTester.run("max-params", rule, {
      valid: ["function test(d, e, f) {}"],
      invalid: [
        {
            code: "function test(a, b, c, d) {}",
            errors: [{
                message: "参数最多不能超过3个",
            }]
        },
      ],
    });
    

    三. 开启本地调试

    在实际开发过程中,要用到自己定义好的插件,这个项目肯定是要发布出去的,但在本篇文章中,以本地为主,用npm link来进行模拟。

    1.在该项目目录下输入npm link命令,如图:假设图中的名字为eslint-plugin-my-test
    在这里插入图片描述
    2.在测试项目中输入命令

    npm link eslint-plugin-my-test
    

    3.在测试项目中的.eslintrc.js文件中添加如下内容:(看下你自定义Eslint项目的name,这里为my-test
    rules的格式:"插件名/规则名": "属性值",

    可参考官网

    // .eslintrc.js
    module.exports = {
        plugins: ['my-test'],
        rules: {
            "my-test/max-params": "error",
        }
    }
    

    4.添加好后,重启Vscode,让Eslint规则生效,然后写个test.js文件:

    function foo(s,s1,s3,s4){
    
    }
    

    5.可以发现代码报错,有对应的提示:
    在这里插入图片描述
    到这里就完成啦

    展开全文
  • ????这是第60篇不掺水的原创,想要了解更多,请戳上方蓝色字体:政采云前端团队关注我们吧~本文首发于政采云前端团队博客:自定义 ESLint 规则,让代码持续美丽https:...

    ????  这是第 60 篇不掺水的原创,想要了解更多,请戳上方蓝色字体:政采云前端团队 关注我们吧~

    本文首发于政采云前端团队博客:自定义 ESLint 规则,让代码持续美丽

    https://www.zoo.team/article/eslint-rules

    背景

    一段真实的代码发展历史

    很久很久以前,有一个需求,然后产出了一段代码,代码优雅而简洁

    export const getConfig = (param1, param2) => {
      return ...
    };
    

    不久又来了个需求,加个参数扩展,so easy!

    export const getConfig = (param1, param2, param3) => {
      return ...
    };
    

    经过多次产品需求迭代后,现在的代码

    export const getConfig = (param1, param2, param3, param4, param5, param6, param7……) => {
      return ...
    };
    

    在产品迭代过程中,上面的 case 一个函数的参数从 2 个发展到了 7 个,优雅的代码逐渐变为不可维护。这是什么问题?这归咎于日益增长的需求,快速响应和代码质量之间的矛盾。

    那如何避免呢?

    • 制定代码规范

    • 靠开发同学的自我修养

    • 进行 Code Review

    • 工具提示

    • 发版控制,不允许发版

    制定代码规范肯定是需要的,那如何约束代码呢?规范文档宣讲,再凭借开发同学的自我修养?答案是:无法保证。

    Code Review ?但难免也有落网之鱼。发版控制?能有效解决但是开发体验不好。

    如果我们在开发者写代码的时候就及时给到提示和建议,那开发体验就很棒了,而 ESLint 的自定义规则就可以实现在开发过程中给开发同学友好的提示。

    ESLint 原理

    ESLint 是一个代码检查工具,通过静态的分析,寻找有问题的模式或者代码。默认使用 Espree (https://github.com/eslint/espree) 解析器将代码解析为 AST 抽象语法树,然后再对代码进行检查。

    看下最简单的一段代码使用 espree 解析器转换成的抽象语法树结构,此处可以使用 astexplorer (https://astexplorer.net/) 快速方便查看解析成 AST 的结构:

    代码片段:

    var a = 1;
    

    转换出的结果:

    {
      "type": "Program",
      "start": 0,
      "end": 10,
      "range": [
        0,
        10
      ],
      "body": [
        {
          "type": "VariableDeclaration",
          "start": 0,
          "end": 10,
          "range": [
            0,
            10
          ],
          "declarations": [
            {
              "type": "VariableDeclarator",
              "start": 4,
              "end": 9,
              "range": [
                4,
                9
              ],
              "id": {
                "type": "Identifier",
                "start": 4,
                "end": 5,
                "range": [
                  4,
                  5
                ],
                "name": "a"
              },
              "init": {
                "type": "Literal",
                "start": 8,
                "end": 9,
                "range": [
                  8,
                  9
                ],
                "value": 1,
                "raw": "1"
              }
            }
          ],
          "kind": "var"
        }
      ],
      "sourceType": "module"
    }
    

    代码转换为 AST 后,可以很方便的对代码的每个节点对代码进行检查。

    自定义 ESLint 规则开发

    怎么自定义

    语法树分析

    对目标代码进行语法树解析,可使用 astexplorer (https://astexplorer.net/)

    编写规则

    下面是一个规则简单的结构(官方 API 文档说明:https://eslint.org/docs/developer-guide/working-with-rules#rule-basics)

    module.exports = {
      meta: {
        docs: {
          description: "最多参数允许参数",
        },
      },
      create: function (context) {
        return {
          FunctionDeclaration: (node) => {
            if (node.params.length > 3) {
              context.report({
                node,
                message: "参数最多不能超过3个",
              });
            }
          },
        };
      },
    };
    
    • meta(对象)包含规则的元数据

    • create ( function ) 返回一个对象,其中包含了 ESLint 在遍历 JavaScript 代码的抽象语法树 AST ( ESTree 定义的 AST ) 时,用来访问节点的方法

    • context.report ( )  用来发布警告或错误,并能提供自动修复功能(取决于你所使用的配置)

    最简单的示例(只使用 node 和 message 参数):

    context.report({
        node,
        message: "参数最多不能超过3个",
    });
    

    使用上面的这个规则,结合编辑器就有了对整个 node 节点的提示,如果需要更精确的错误或警告提示,我们可以使用 loc 参数,API 文档说明 (https://eslint.org/docs/developer-guide/working-with-rules#context-report)。

    image

    如何使用自定义规则

    使用自定义的 ESLint 规则,你需要自定义一个 ESLint 的插件,然后将规则写到自定义的 ESLint 插件中,然后在业务代码中添加 ESLint 配置,引入 ESLint 插件。

    ESLint 插件

    创建

    创建一个 ESLint plugin,并创建 一个 ESLint rule

    基于 Yeoman generator (https://yeoman.io/authoring/) ,可以快速创建 ESLint plugin 项目。

    npm i -g yo
    npm i -g generator-eslint
    // 创建一个plugin
    yo eslint:plugin
    // 创建一个规则
    yo eslint:rule
    

    创建好的项目目录结构:

    • rules 文件夹存放的是各个规则文件

    • tests 文件夹存放单元测试文件

    • package.json 是你的 ESLint 插件 npm 包的说明文件,其中的 name 属性就是你的 ESLint  插件的名称,命名规则:带前缀 eslint-plugin-

    示例代码:

    lib/rules/max-params.js

    module.exports = {
      meta: {
        docs: {
          description: "最多参数",
        },
      },
      create: function (context) {
        /**
         * 获取函数的参数的开始、结束位置
         * @param {node} node AST Node 
         */
        function getFunctionParamsLoc(node) {
          const paramsLength = node.params.length;
          return {
            start: node.params[0].loc.start,
            end: node.params[paramsLength - 1].loc.end,
          };
        }
        return {
          FunctionDeclaration: (node) => {
            if (node.params.length > 3) {
              context.report({
                loc: getFunctionParamsLoc(node),
                node,
                message: "参数最多不能超过3个",
              });
            }
          },
        };
      },
    };
    

    补充测试用例

    /tests/lib/rules/max-params.js

    var ruleTester = new RuleTester();
    ruleTester.run("max-params", rule, {
      valid: ["function test(d, e, f) {}"],
      invalid: [
        {
            code: "function test(a, b, c, d) {}",
            errors: [{
                message: "参数最多不能超过3个",
            }]
        },
      ],
    });
    

    ESLint 插件安装

    在需要的业务代码中安装你的 ESLint 插件。(eslint-plugin-my-eslist-plugin 是你的 ESLint 插件 npm 包的包名)

    npm install eslint-plugin-my-eslist-plugin 
    

    如果你的 npm 包还未发布,需要进行本地调试:

    可使用 npm link 本地调试,npm link 的使用 (https://www.baidu.com/s?ie=UTF-8&wd=npm%20link)。

    配置

    添加你的 plugin 包名(eslint-plugin- 前缀可忽略) 到 .eslintrc 配置文件的 plugins 字段。

    .eslintrc 配置文件示例:

    {
        "plugins": [
            "zoo" // 你的 ESLint plugin 的名字
        ]
    }
    

    rules 中再将 plugin 中的规则导入。⚠️ ESlint更新后,需要重启 vsCode,才能生效。( vsCode  重启快捷方式:CTRL +SHITF + P,输入 Reload Window

    此处涉及 ESLint 的规则设置(参考说明:https://eslint.org/docs/user-guide/configuring#configuring-rules)

    {
        "rules": {
            "zoo/rule-name": 2
        }
    }
    

    效果

    image

    实际应用案例

    函数、方法的入参个数控制,其实已经在 ESLint 的规则中了。在业务场景中,我们需要对我们的业务规则编写自定义的 ESLint 规则。

    一个简单的业务场景:业务中通常会出现跳转到很多不同的业务域名的操作,不同的环境有不同的域名,我们需要从配置中取出域名使用,而不是采取硬编码域名的方案。

    由此我们产生出了一个规则:禁止硬编码业务域名。

    规则为:

    module.exports = {
      meta: {
        type: "suggestion",
        docs: {
          description: "不允许硬编码业务域名",
        },
        fixable: "code",
      },
    
      create: function (context) {
        const sourceCode = context.getSourceCode();
    
        function checkDomain(node) {
          // 匹配硬编码的业务域名的正则
          const Reg = /^(http:\/\/|https:\/\/|\/\/)(.*.){0,1}zcygov(.com|cn)(.*)/;
          const content =
            (node.type === "Literal" && node.value) ||
            (node.type === "TemplateLiteral" && node.quasis[0].value.cooked);
    
          const domainNode =
            (node.type === "Literal" && node) ||
            (node.type === "TemplateLiteral" && node.quasis[0]);
    
          if (Reg.test(content)) {
            context.report({
              node,
              // 错误/警告提示信息
              message: "不允许硬编码业务域名",
              // 修复
              fix(fixer) {
                
                const fixes = [];
                
                let domainKey = content.match(Reg)[2];
                domainKey = domainKey
                  ? domainKey.substr(0, domainKey.length - 1)
                  : "";
    
                if (node.type === "Literal") {
                  fixes.push(
                    fixer.replaceTextRange(
                      [domainNode.start + 1, domainNode.end - 1],
                      content.replace(Reg, `$4`)
                    )
                  );
                }
    
                if (node.type === "TemplateLiteral") {
                  fixes.push(
                    fixer.replaceTextRange(
                      [domainNode.start, domainNode.end],
                      content.replace(Reg, `$4`)
                    )
                  );
                }
                 
                if (
                  node.type === "Literal" &&
                  node.parent.type === "JSXAttribute"
                ) {
                  fixes.push(fixer.insertTextBefore(node, "{"));
                  fixes.push(fixer.insertTextAfter(node, "}"));
                }
    
                fixes.push(
                  fixer.insertTextBefore(
                    node,
                    `window.getDomain('${domainKey}') + `
                  )
                );
    
                return fixes;
              },
            });
          }
        }
        return {
          // 文本
          Literal: checkDomain,
          // 模板字符串
          TemplateLiteral: checkDomain,
        };
      },
    };
    

    补充测试用例

    /tests/lib/rules/no-zcy-domain.js

    var rule = require("../../../lib/rules/no-zcy-domain"),
        RuleTester = require("eslint").RuleTester;
    
    var ruleTester = new RuleTester();
    ruleTester.run("no-zcy-domain", rule, {
      valid: [
        "bar",
        "baz",
        `
      var s = {
        x: "zcygov"
      };
      `,
      ],
      invalid: [
        {
          code: `
                  var s = "//zcygov.cn"
                `,
          errors: [
            {
              message: "不允许硬编码业务域名",
            },
          ],
        },
        {
          code: `
                var s = {
                  x: "http://bidding.zcygov.cn"
                };
                `,
          errors: [
            {
              message: "不允许硬编码业务域名",
            },
          ],
        },
      ],
    });
    

    结合 vsCode 保存自动修复 ESLint 错误的功能,效果如下:

    更多的应用场景

    除了上面说的硬编码的场景,还可以将沉淀出的最佳实践和业务规范通过自定义 ESLint 的方式来提示开发者,这对于多人协助、代码维护、代码风格的一致性都会有很大的帮助。

    更多的应用场景有:

    • Input 必须要有 maxlength 属性,防止请求的后端接口数据库异常

    • 代码中不能出现加减乘除等计算,如果需要计算应该引入工具函数,来控制由于前端浮点数计算引起的 Bug

    • 规范限制,单位元的两边的括号要用英文括号,不能用中文括号,来达到交互展示统一的效果

    • 代码中不能使用 OSS 地址的静态资源路径,应该使用 CDN 地址的资源路径

    • ...

    参考文献

    • https://developer.mozilla.org/zh-CN/docs/Mozilla/Projects/SpiderMonkey/Parser_API

    • https://eslint.org/docs/developer-guide/working-with-rules

    推荐阅读

    1、你不知道的前端异常处理(万字长文,建议收藏)

    2、你不知道的 TypeScript 泛型(万字长文,建议收藏)

    3、你不知道的 Web Workers (万字长文,建议收藏)

    4、lucifer与它的《力扣加加》来啦

    5、或许是一本可以彻底改变你刷 LeetCode 效率的题解书

    6、想去力扣当前端,TypeScript 需要掌握到什么程度?

    7、如果张东升是个程序员

    关注加加,星标加加~

    如果觉得文章不错,帮忙点个在看呗

    展开全文
  • 用于Fluidly代码库的自定义eslint插件 { "rules": { "fluidly/no-new-date-for-parsing": 2, }, "extends": [ "plugin:fluidly/recommended" ], "plugins": [ "fluidly" ] } 流畅/没有新的解析日期 不要...
  • eslint-规则 一小部分自定义规则。 注意:此 repo 仍在积极维护,但对象相关规则已被提取并捆绑为 ESLint 插件 - - 带有测试数据等。这更易于使用,也是安装这些特定规则的推荐方法. - 不允许每个 var 声明有多个...
  • 看我如何玩转自定义ESLint规则

    千次阅读 2018-02-26 17:47:00
    来了淘宝之后各种忙,最近正好在弄自定义ESLint相关的东西,写篇文章mark下。最近开通了自己的微信公共号“王和阳的航海日志”,上面记录着自己的学习、思考、实践和成长的过程,欢迎关注,欢迎交流。 基本思路...
  • GPA / LAB ESLint配置 该程序包提供了一个自以为是但可扩展的ESLint配置,该配置可实施GPA / LAB样式指南定义JavaScript编码首选项。 安装 从项目的根目录运行以下命令以安装配置。 npm install @gpa-lab/eslint-...
  • eslint配置方式有两种: 注释配置:使用js注释来直接嵌入ESLint配置信息到一个文件里 配置文件:使用一个js,JSON或者YAML文件来给整个目录和它的子目录指定配置信息。这些配置可以写在一个文件名为.eslintrc.*的...
  • eslint-config-cbtnuggets 用于设置CBT ESLint规则的配置。 安装 项目安装 该脚本将安装并保存(作为devDependencies)在执行脚本的项目中运行我们的eslint规则所需的所有信息: 使用NPM export PKG=eslint-...
  • eslint导入解析器-babel模块 用于解析器 安装 npm install --save-dev eslint-plugin-import eslint-import-resolver-babel-module 用法 在您的.eslintrc文件中,将此解析器传递给eslint-plugin-import : ...
  • 禁止未经消毒的代码(无未经消毒的) 这些规则禁止可能导致安全漏洞的不安全编码实践。 在不使用预定义转义功能的情况下,我们将不允许分配(例如,对innerHTML)和调用(例如,对insertAdjacentHTML)。...
  • webstorm 自定义eslint格式化代码

    千次阅读 2018-09-13 14:55:20
    vscode上配置eslint格式化代码使用的Prettier 插件,一键格式化。 webstorm上的配置流程如下:https://blog.csdn.net/qq_29329037/article/details/80100450
  • eslint-import-resolver-babel-module, babel插件模块解析器的自定义eslint解析 eslint-import-resolver-babel-module 用于eslint-plugin-import的babel-plugin-module-resolver 解析程序。安装npm install -
  • 方案二 在package.json 中的 script 命令 添加环境变量EXTEND_ESLINT=treu 开启自定义eslint 我们可以打开react-script 源码看到 webpack.config.js 文件 当环境变量设定为true时候,react-script会认为我们...
  • eslint-config-divisio ESLint可共享配置的文档 共享配置是一种创建编码样式的模式的好方法 :smiling_face_with_sunglasses: 安装 最好的选择是使用所有必要的对等依赖项来安装此软件包,因此您可以使用: npx ...
  • Create-react-app 配置自定义eslint

    千次阅读 2019-12-20 13:22:07
    但是在实际项目中我们往往需要自定义一些配置,但又希望保留这种跟着包升级配置的优势。 eslint是项目中非常常用的工具,它的配置也是因人而异,因项目团队而异。而create-react-app默认是有一...
  • 快速入门带有Redux,React测试库,eslint和stylelint配置的 (CRA)模板。 请参阅。 原始Create React App自述文件可 用法 npx create-react-app %PROJECT_NAME% --template quickstart-redux 要么 yarn create ...
  • ESLint 是什么? ESLint 是一个可组装的javascript和jax代码检测工具。 官网 ESLint 怎么使用? 1、 在项目根目录创建一个.eslintrc.js文件 2、对Eslint代码检测进行配置 module.exports = { // ...
  • 自定义eslint,并传参

    2021-07-20 11:22:10
    接上篇文章,我说client.ts不报错,好像是有误的,因为只检测js文件,没检测ts文件。坐我边上的大佬说可以开检测ts的。 然后说传参,在eslintrc.js rules里面传了一个对象,包含files,哪些文件不检测,检测哪些模块 ...
  • 这时候如果我们想要在项目中配置自定义eslint,可以参照以下步骤进行。 一、首先检查是否安装了 eslint和babel-eslint 查看package.json文件,一般npm run eject之后都会看到已经安装了。 如果尚未安装,则需要安装...
  • ESLint于2013年6月份推出,至今4个年头,最新版本v4.8.0。它是目前主流的用于Javascript和JSX代码规范检查的利器,很多大公司比如Airbnb和Google均有一套自己的Javascript编码规范,而规范的实施背后离不开ESLint的...
  • npm run eject =>...2、webpack.config.js 文件中eslint配置处开启useEslintrc: true (注意开启关闭规则时设置cache: true 的值为false否则缓存可能为影响校验展示结果) { test: /\.(js|mjs|jsx|t...
  • 前面已经写了两篇关于eslint的文章了,想必都对eslint应该有一个简单的认识了,在平常的项目中使用应该是问题不大,面试应该也是问题不大的,大家有兴趣可以看看前面两篇文章: 前端框架系列之(eslint入门) 前端...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 13,393
精华内容 5,357
关键字:

自定义eslint