eslint相关知识

前置知识

抽象语法树(Abstract Syntax Tree)

  • 抽象语法树(Abstract Syntax Tree,简称AST)是源代码语法结构的一种抽象表示
  • 它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构

image-20240629153048779

抽象语法树用途

  • 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码的错误提示、代码的自动补全等等。
  • 优化变更代码、改变代码结构使达到想要的结构

JavaScript Parser

  • JavaScript Parser是把JavaScript源码转化为抽象语法树的解析器
  • 我们可以通过https://astexplorer.net/在线查看源代码对应的ast结构

代码转换

  • 通过esprima将代码转换成ast语法树
  • 通过estraverse深度优先遍历,遍历ast抽象语法树
  • 通过escodegen进行代码生成
pnpm i esprima estraverse escodegen -S
const esprima = require("esprima");
const estraverse = require("estraverse");
const escodegen = require("escodegen");
const code = `function ast(){}`;
// 1.将代码转换成ast语法树
const ast = esprima.parseScript(code);
// 2.深度优先遍历,遍历ast抽象语法树
let level = 0;
const padding = () => " ".repeat(level);
// 遍历节点的时候,会有两个过程,进入和离开
estraverse.traverse(ast, {
  enter(node) {
    console.log(padding() + "enter:" + node.type);
    level += 2;
  },
  leave(node) {
    level -= 2;
    console.log(padding() + "leave:" + node.type);
  },
});
// 3.代码⽣成
const result = escodegen.generate(ast);
console.log(result);

image-20240629161753264

estraverse.traverse(ast, {
enter(node) {
if (node.type === "FunctionDeclaration") {
node.id.name = "myFunc";
 }
 },
});

babel插件

转换箭头函数

  • @babel/core Babel 的编译器,核⼼ API 都在这⾥⾯,⽐如常⻅的 transform、parse,并实现了插件功能
  • @babel/types ⽤于 AST 节点的 Lodash 式⼯具库, 它包含了构造、验证以及变换 AST 节点的⽅法,对编写处 理 AST 逻辑⾮常有⽤
  • babel-plugin-transform-es2015-arrow-functions 转换箭头函数插件
const babel = require("@babel/core");
const types = require("@babel/types");
const arrowFunctions = require("babel-plugin-transform-es2015-arrow-functions");
const code = `const sum = (a,b)=> a+b`;
// 转化代码,通过arrowFunctions插件
const result = babel.transform(code, {
plugins: [arrowFunctions],
});
console.log(result.code);

image-20240629162511620

const arrowFunctions = {
visitor: {
// 访问者模式,遇到箭头函数表达式会命中此⽅法
ArrowFunctionExpression(path) {
let { node } = path;
node.type = "FunctionExpression"; // 将节点转换成函数表达式
let body = node.body;
// 如果不是代码块,则增加代码块及return语句
if (!types.isBlockStatement(body)) {
node.body = types.blockStatement([types.returnStatement(body)]);
 }
 }
 },
};

@babel/types 主要就是对类型的判断和创建,接下来,我们继续处理箭头函数中的this问题!

https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md

const sum = (a,b)=> console.log(this) // 源代码
----transform ----
var _this = this; // 转换后的代码
const sum = function (a, b) {
return console.log(_this);
};

需要找到上级作⽤域增加this的声明语句

function getThisPaths(path) {
const thisPaths = [];
path.traverse({
ThisExpression(path) {
thisPaths.push(path);
 },
 });
return thisPaths; // 获得所有⼦路径中的thisExpression
}
function hoistFunctionEnvironment(path) {
const thisEnv = path.findParent((parent) => {
return (
 (parent.isFunction() && !path.isArrowFunctionExpression()) ||
parent.isProgram()
 );
 });
const thisBindings = "_this"; // 找到this表达式替换成_this
const thisPaths = getThisPaths(path); // 遍历⼦路径
if (thisPaths.length > 0) {
if (!thisEnv.scope.hasBinding(thisBindings)) {
// 在作⽤域下增加 var _this = this
thisEnv.scope.push({
id: types.identifier(thisBindings),
init: types.thisExpression(),
 });
 }
 }
// 替换this表达式
thisPaths.forEach((thisPath) =>
thisPath.replaceWith(types.identifier(thisBindings))
 );
}

类编译为 Function

class Person {
constructor(name) {
this.name = name;
 }
getName() {
return this.name;
 }
setName(newName) {
this.name = newName;
 }
}
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
};
Person.prototype.setName = function () {
this.name = newName;
};

image-20240629162730832

const arrowFunctions = {
  visitor: {
    ClassDeclaration(path) {
      const { node } = path;
      const id = node.id;
      const methods = node.body.body; // 获取类中的⽅法
      const nodes = [];
      methods.forEach((method) => {
        if (method.kind === "constructor") {
          let constructorFunction = types.functionDeclaration(
            id,
            method.params,
            method.body
          );
          nodes.push(constructorFunction);
        } else {
          // Person.prototype.getName
          const memberExpression = types.memberExpression(
            types.memberExpression(id, types.identifier("prototype")),
            method.key
          );
          // function(name){return name}
          const functionExpression = types.functionExpression(
            null,
            method.params,
            method.body
          );
          // 赋值
          const assignmentExpression = types.assignmentExpression(
            "=",
            memberExpression,
            functionExpression
          );
          nodes.push(assignmentExpression);
        }
      });
      // 替换节点
      if (node.length === 1) {
        path.replaceWith(nodes[0]);
      } else {
        path.replaceWithMultiple(nodes);
      }
    },
  },
}

Eslint使⽤

  • ESLint 是⼀个开源的⼯具cli,ESLint采⽤静态分析找到并修复 JavaScript 代码中的问题
    • ESLint 使⽤Espree进⾏ JavaScript 解析。
    • ESLint 使⽤ AST 来评估代码中的模式。
    • ESLint 是完全可插拔的,每⼀条规则都是⼀个插件,你可以在运⾏时添加更多。

扯⽪⼀下这些解析器的关系~~~

  • esprima - 经典的解析器
  • acorn - 造轮⼦媲美Esprima
  • @babel/parser (babylon)基于acorn的
  • espree 最初从Esprima中fork出来的,现在基于acorn
pnpm init
pnpm i eslint -D # 安装eslint
pnpm create @eslint/config # 初始化eslint的配置⽂件

⽣成的配置⽂件是:

module.exports = {
  env: { // 指定环境
    browser: true, // 浏览器环境 document
    es2021: true, // ECMAScript语法 Promise
    node: true, // node环境 require
  },
  extends: "eslint:recommended",
  parserOptions: { // ⽀持语⾔的选项, ⽀持最新js语法,同时⽀持jsx语法
    ecmaVersion: "latest", // ⽀持的语法是
    sourceType: "module", // ⽀持模块化
    ecmaFeatures: {
      "jsx": true
    }
  },
  rules: { // eslint规则
    "semi": ["error", "always"],
    "quotes": ["error", "double"]
  },
  globals: { // 配置全局变量
    custom: "readonly" // readonly 、 writable
  }
};

parser:可以指定使⽤何种parser来将code转换成estree。举例:转化ts语法

pnpm install @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest
typescript -D
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended" // 集成规则和插件
],
"parser": "@typescript-eslint/parser" // 解析typescript

Eslint运⾏流程

Eslint 代码检查的过程

  • 配置文件:ESLint使用一个配置文件(如.eslintrc文件)来定义规则集。这个文件是一个JSON格式的文件,其中包含了各种规则、插件和其他配置选项。开发者可以根据项目的需求和规范,自定义ESLint配置,以满足不同的代码风格和规范要求

  • 解析代码:ESLint首先会将代码解析为抽象语法树(AST,Abstract Syntax Tree),这是一种将源代码语1 2法结构转换为数据结构和节点对象的表示方式,以便后续的分析和处理

    • 解析输入代码:ESLint将输入的代码解析成一系列的token。Token是代码的基本组成单元,例如关键字、变量名、字符串、数字等
    • 逐个检查代码中的每个token:ESLint会将解析后的token逐个送到规则集中进行检查,根据规则集判断token是否符合规则,并报告不符合规则的token及其原因
  • 加载规则:ESLint会加载预定义的一系列规则,这些规则定义了代码应该符合的规范。开发者也可以根据项目需求自定义规则

  • 执行规则:ESLint会遍历抽象语法树,根据规则对代码进行检查。当发现代码不符合规范时,ESLint会将检查结果以可读性良好的格式输出对应的问题信息,供开发者查看和处理

流程图

1

ESLint 核⼼API
 - lintFiles 检验⽂件
 - lintText 校验⽂本
 - loadFormatter 加载formatter
 - calculateConfigForFile 通过⽂件获取配置
 - isPathIgnored 此路径是否是被忽略的
 - static outputFixes 输出修复的⽂件
 - static getErrorResults 获得错误结果
CLIEngine 脚⼿架核⼼
- getRules 获取规则
- resolveFileGlobPatterns 解析⽂件成glob模式
 - executeOnFiles 根据⽂件执⾏逻辑
 - executeOnText 根据⽂本执⾏逻辑
 - getConfigForFile 获取⽂件的配置
 - isPathIgnored 此路径是否是被忽略的
 - getFormatter 获取输出的格式
 - static getErrorResults 获取错误结果
 - static outputFixes 输出修复的结果
Linter 校验js⽂本
 - verifyAndFix 校验和修复
 - verify 校验⽅法
 - _verifyWithConfigArray 通过配置⽂件校验
 - _verifyWithoutProcessors 校验没有processors
 - _verifyWithProcessor 校验有processors

Eslint插件开发

Eslint官⽅提供了可以使⽤Yeoman 脚⼿架⽣成插件模板

npm install yo generator-eslint -g

模板初始化

mkdir eslint-plugin-zlint
cd eslint-plugin-zlint
yo eslint:plugin # 插件模板初始化

image-20240629165104860

yo eslint:rule # 规则模版初始化

image-20240629165139821

实现no-var

module.exports = {
"env": {
"browser": true,
"es2021": true,
"node": true
 },
"plugins": ['zlint'],
"rules": {
'zlint/no-var': ['error', 'always']
 }
}
module.exports = {
  meta: {
    docs: {
      description: "代码中不能出现var",
      recommended: false,
    },
    fixable: "code",
    messages: {
      unexpectedVar: 'Unexpected var'
    }
  },
  create(context) {
    const sourceCode = context.getSourceCode();
    return {
      "VariableDeclaration:exit"(node) { // 如果类型是var,拦截var声明
        if (node.kind === 'var') {
          context.report({ // 报警告
            node,
            messageId: 'unexpectedVar',
            fix(fixer) { // --fix
              const varToken = sourceCode.getFirstToken(node, {
                filter: t => t.value
                  === 'var'
              });
              // 将var 转换成 let
              return fixer.replaceText(varToken, 'let')
            }
          })
        }
      }
    };
  },
};
单元测试

const ruleTester = new RuleTester({
  parserOptions: {
    ecmaVersion: 'latest',
  },
});
ruleTester.run("no-var", rule, {
  valid: [
    {
      code: "let a = 1"
    }
  ],
  invalid: [
    {
      code: "var a = 1",
      errors: [{
        messageId: "unexpectedVar",
      }],
      output: 'let a = 1'
    },
  ],
});

实现eqeqeq

module.exports = {
  env: { // 指定环境
    browser: true, // 浏览器环境
    es2021: true, // ECMAScript2021
    node: true, // node环境
  },
  plugins: ['zlint'],
  rules: {
    "zlint/eqeqeq": ['error', "always", { null: 'never' }]
  }
}
module.exports = {
  meta: {
    type: 'suggestion', // `problem`, `suggestion`, or `layout`
    docs: {
      description: "尽量使⽤三等号",
      recommended: false,
    },
    fixable: 'code',
    schema: {
      type: "array",
      items: [
        {
          enum: ["always"]
        },
        {
          type: "object",
          properties: {
            null: {
              enum: ["always", "never"]
            }
          }
        }
      ],
    },
    messages: {
      unexpected: "期望 '{{expectedOperator}}' ⽬前是 '{{actualOperator}}'."
    }
  },
  create(context) {
    // 处理null的情况
    const config = context.options[0] || 'always';
    const options = context.options[1] || {};
    const nullOption = (config === 'always') ? options.null || 'always' : ''
    const enforceRuleForNull = (nullOption === 'never')
    function isNullCheck(node) {
      let rightNode = node.right;
      let leftNode = node.left;
      return (rightNode.type === 'Literal' && rightNode.value === null) ||
        (leftNode.type === 'Literal' && leftNode.value === null)
    }
    const sourceCode = context.getSourceCode();
    function report(node, expectedOperator) {
      const operatorToken = sourceCode.getFirstTokenBetween(node.left, node.right, {
        filter: t => t.value === node.operator
      });
      // 左右两边是不是相同类型
      function areLiteralsAndSameType(node) {
        return node.left.type === "Literal" && node.right.type === "Literal" &&
          typeof node.left.value === typeof node.right.value;
      }
      context.report({
        node,
        loc: operatorToken.loc,
        data: { expectedOperator, actualOperator: node.operator },
        messageId: 'unexpected',
        fix(fixer) {
          if (areLiteralsAndSameType(node)) {
            return fixer.replaceText(operatorToken, expectedOperator);
          }
          return null
        }
      })
    }
    return {
      BinaryExpression(node) {
        const isNull = isNullCheck(node);
        // 如果是两等号的情况
        if (node.operator !== '==' && node.operator !== '!=') {
          if (enforceRuleForNull && isNull) { // 当遇到null的时候不进⾏转换
            report(node, node.operator.slice(0, -1));
          }
          return
        }
        report(node, `${node.operator}=`)
      }
    };
  },
};
单元测试
const ruleTester = new RuleTester();
ruleTester.run("eqeqeq", rule, {
  valid: [
    {
      code: '1===1'
    }
  ],
  invalid: [
    {
      code: "1==1",
      errors: [{
        data: { expectedOperator: '===', actualOperator: '==' },
        messageId: 'unexpected',
      }],
      output: "1===1"
    },
    {
      code: "null === undefined",
      options: ["always", { null: 'never' }],
      errors: [{
        data: { expectedOperator: '==', actualOperator: '===' },
        messageId: 'unexpected',
      }],
    },
  ],
});

实现no-console

rules: {
  "zlint/no-console": ['warn', { allow: ['log'] }],
    "zlint/no-var": 'error'
}
module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: "禁⽌使⽤console",
      recommended: false,
    },
    schema: [
      {
        type: 'object',
        properties: {
          allow: {
            type: 'array',
            items: {
              type: 'string'
            }
          }
        }
      }
    ]
  },
  create(context) {
    const allowed = context.options[0]?.allow || [];
    function isMemberAccessExceptAllowed(reference) {
      const node = reference.identifier
      const parent = node.parent;
      if (parent.type === 'MemberExpression') { // 获得⽗节点,看下属性名字
        const { name } = parent.property;
        return !allowed.includes(name);
      }
    }
    function report(reference) {
      const node = reference.identifier.parent; // console 表达式
      context.report({
        node,
        loc: node.loc,
        message: 'Unexpected console statement.'
      })
    }
    return {
      "Program:exit"() {
        // 1. 当前作⽤域
        let scope = context.getScope();
        // 2. 获得console变量
        const variable = scope.set.get('console');
        // 3. 获得references
        const references = variable.references;
        references.filter(isMemberAccessExceptAllowed).forEach(report)
      }
    };
  },
};
单元测试
const ruleTester = new RuleTester({
  env: {
    browser: true
  }
});
ruleTester.run("no-console", rule, {
  valid: [
    {
      code: "console.log('hello')",
      options: [{ allow: ['log'] }]
    }
  ],
  invalid: [
    {
      code: "console.info('hello')",
      errors: [{ message: "Unexpected console statement." }],
    },
  ],
});

extends 使⽤

module.exports = {
  rules: requireIndex(__dirname + "/rules"),
  configs: {
    // 导出⾃定义规则 在项⽬中直接引⽤
    recommended: {
      plugins: ['zlint'], // 引⼊插件
      rules: {
        // 开启规则
        'zlint/eqeqeq': ['error', "always", { null: 'never' }],
        'zlint/no-console': ['error', { allow: ['log'] }],
        'zlint/no-var': 'error'
      }
    }
  },
  processors: {
    '.vue': {
      preprocess(code) {
        console.log(code)
        return [code]
      },
      postprocess(messages) {
        console.log(messages)
        return []
      }
    }
  }
};
module.exports = {
"env": {
"browser": true,
"es2021": true,
"node": true
 },
extends: [
'plugin:zlint/recommended' // 直接集成即可
 ]
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值