eslint 基本原理

477 阅读2分钟

解析js文件

eslint本质上是遍历ast,在遇到各种节点时,会走各类定义好的规则校验,这些规则代码就是访问并校验ast节点的。因此首先需要得到ast,借助的就是各种解析器。

eslint默认通过Espree作为解析器解析js。 但Espree不支持最新的ecmascript语法,因此一般会使用@babel/eslint-parser代替。.eslintrc.js中配置如下:

module.exports = {
  parser: "@babel/eslint-parser",
  parserOptions: {
    sourceType: "module",
    allowImportExportEverywhere: false,
    ecmaFeatures: {
      globalReturn: false,
    },
    babelOptions: {
      configFile: path.resolve(__dirname, '.babelrc') // 引用项目中的.babelrc配置
    }
  }
}

如果项目中使用了typescript,则替换为@typescript-eslint/parser

一个文件经过解析器,会返回一个ast语法树

遍历语法树

eslint/lib/util/traverser.js

traverse(node, options) {
        this._current = null;
        this._parents = [];
        this._skipped = false;
        this._broken = false;
        this._visitorKeys = options.visitorKeys || vk.KEYS;
        this._enter = options.enter || noop;
        this._leave = options.leave || noop;
        this._traverse(node, null);
    }

    _traverse(node, parent) {
        if (!isNode(node)) {
            return;
        }

        this._current = node;
        this._skipped = false;
        this._enter(node, parent);

        if (!this._skipped && !this._broken) {
            const keys = getVisitorKeys(this._visitorKeys, node);

            if (keys.length >= 1) {
                this._parents.push(node);
                for (let i = 0; i < keys.length && !this._broken; ++i) {
                    const child = node[keys[i]];

                    if (Array.isArray(child)) {
                        for (let j = 0; j < child.length && !this._broken; ++j) {
                            this._traverse(child[j], node);
                        }
                    } else {
                        this._traverse(child, node);
                    }
                }
                this._parents.pop();
            }
        }

        if (!this._broken) {
            this._leave(node, parent);
        }

        this._current = parent;
    }

挨个节点访问规则,并在校验不通过时抛错

zhuanlan.zhihu.com/p/53680918 这篇文章中 ESLint 是怎么看待我们传入的 rule 的 这一节中说的比较清楚,这里不再复述

分析代码是否能被访问到

cn.eslint.org/docs/develo…

eslint-plugin-vue

类似于这种有特殊模版的插件,都是先通过定制的解析器,分离js和模版,js生成ast,模版单独解析成ast并挂载在js的ast上,比如vue-eslint-parser是将模版语法树使用属性templateBody挂在跟语法树上

plugins

.eslintrc.js中可以配置插件,比如

module.exports = {
  extends: [
    'plugin:vue/essential'
  ],
  plugins: [
    //这里vue是eslint-plugin-vue的缩写
    //只要eslint-plugin-xx这种格式的插件这里就可以简写为xx
    'vue'
  ],
  rules: {
    'no-unused-vars': 2,
    'no-debugger': 2
  }
}

自动修复

applying-fixes

基本原理是eslint提供了增、删、替换ast节点的能力,和一个修复函数fix,在修复函数中对ast进行操纵实现修复

extends

module.exports = {
  extends: [
    'plugin:vue/essential',
    'standard'
  ],
  plugins: [
    'vue'
  ]
}

这里有几种情况:

  • 如果使用eslint-config-xx这种格式的配置,就可以简写为xx
  • eslint默认有两种可继承的配置:eslint:recommendedeslint:all,定义在eslint-recommended.jseslint-all.js
  • 如果是自定义插件,也可以生成一些可用于继承的配置集,在插件的入口文件中声明configs,比如eslint-plugin-vue的:
module.exports = {
  rules: {
    // 自定义规则,在这里声明
    ...
  },
  configs: {
    base: require('./configs/base'),
    essential: require('./configs/essential'),
    'no-layout-rules': require('./configs/no-layout-rules'),
    recommended: require('./configs/recommended'),
    'strongly-recommended': require('./configs/strongly-recommended'),
    'vue3-essential': require('./configs/vue3-essential'),
    'vue3-recommended': require('./configs/vue3-recommended'),
    'vue3-strongly-recommended': require('./configs/vue3-strongly-recommended')
  },
  processors: {
    // 声明处理器,用于预处理代码和处理lint结果
    ...
  },
  environments: {
    ...
  }
}

使用

仅使用的话直接eslint --init,然后根据提示一步步选择就行 有不懂的地方,官网其实写的很清楚了 英文官网

vscode校验原理

vscode中使用vetur提供eslint能力

源码如下:

import { ESLint, Linter } from 'eslint';
import { configs } from 'eslint-plugin-vue';
import { Diagnostic, Range, DiagnosticSeverity } from 'vscode-languageserver-types';
import type { TextDocument } from 'vscode-languageserver-textdocument';
import { resolve } from 'path';
import { VueVersion } from '../../../utils/vueVersion';

function toDiagnostic(error: Linter.LintMessage): Diagnostic {
  const line = error.line - 1;
  const column = error.column - 1;
  const endLine = error.endLine ? error.endLine - 1 : line;
  const endColumn = error.endColumn ? error.endColumn - 1 : column;
  return {
    range: Range.create(line, column, endLine, endColumn),
    message: `[${error.ruleId}]\n${error.message}`,
    source: 'eslint-plugin-vue',
    severity: error.severity === 1 ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error
  };
}

export async function doESLintValidation(document: TextDocument, engine: ESLint): Promise<Diagnostic[]> {
  const rawText = document.getText();
  // skip checking on empty template
  if (rawText.replace(/\s/g, '') === '') {
    return [];
  }
  const text = rawText.replace(/ {10}/, '<template>') + '</template>';
  const report = await engine.lintText(text, { filePath: document.uri });

  return report?.[0]?.messages?.map(toDiagnostic) ?? [];
}

export function createLintEngine(vueVersion: VueVersion) {
  const SERVER_ROOT = __dirname;

  const versionSpecificConfig: Linter.Config =
    vueVersion === VueVersion.V30 ? configs['vue3-essential'] : configs.essential;
  if (vueVersion === VueVersion.V30) {
    versionSpecificConfig.parserOptions = {
      ...versionSpecificConfig.parserOptions,
      vueFeatures: {
        ...versionSpecificConfig.parserOptions?.vueFeatures,
        interpolationAsNonHTML: true
      }
    };
  }

  const baseConfig: Linter.Config = configs.base;
  baseConfig.ignorePatterns = ['!.*'];

  return new ESLint({
    useEslintrc: false,
    cwd: SERVER_ROOT,
    baseConfig,
    overrideConfig: versionSpecificConfig
  });
}

首先,引用eslint-plugin-vue,并使用其配置初始化eslint插件 然后doESLintValidation这个函数里调用上一步生成的eslint对象的lintText方法校验文本

eslint配置流程如果按官网的来,或者修改了全局eslint配置,但不生效,可以尝试重启vscode

eslint/babel插件的对比

相同点

  • 都是基于访问语法树节点的方式,语法树都是相同的,可以去astexplorer上查看语法树

不同点

  • 访问节点方式

    // babel
    ImportDeclaration: {
      enter() {}
      exit() {}
    }
    
    // eslint
    ImportDeclaration(){}
    'ImportDeclaration:exit'(){}
    

    此外eslint插件支持对路径节点的访问code-path

  • babel有提供很多辅助能力

    • babel-types用于判断和构造节点类型,eslint只能根据节点结构中的属性去判断,也不支持快捷构造节点
    • babel-template快速构建节点树
    • babel在node和path上提供了便捷的查找、新增、修改、替换等方法
  • babel可以直接修改ast

    在babel插件中可以直接操作ast,但eslint中不能直接修改ast,只能在fix方法中提供修改ast的入口

    当然这跟两者的功能区分有关系,babel插件就是用来修改代码的,而eslint本质上只是提供校验,fix方法是用于修复问题代码的,所以才能修改ast