ESLint v9: flat config 之匹配与合并

336 阅读5分钟

flat config 的匹配规则

ESLint flat configs 是由多个 config 对象组成的数组。那么针对某个文件(filePath)去做代码校验和格式化的时候,应用了哪些 config 对象呢?

整体思路是:

  1. 先找出与 filePath 匹配的 config 对象;
  2. 然后将这些 config 对象按照一定的策略合并成一个对象(每个属性都有自己的合并策略)。

config 对象有以下属性:

// config 对象
{
  files: [string, function, [string, function]] // files 类型是数组,支持的元素类型是 string、function, 或者 string, function 构成的数组
  ignores,
  language,
  languageOptions: {
      ecmaVersion,
      sourceType,
      globals,
      parser,
      parserOptions
  },
  plugins
  processor: { `preprocess()` 和 `postprocess()` }
  rules
}

如何找到与 filePath 匹配的 config 对象?

  1. 如果 config 对象 没有 files

    • 没有 ignores ,则该 config 对象是匹配的。
    • 有 ignores,需要检查 filePath 是否与 ignores 中的模式匹配。如果匹配,意味着 filePath 是属于被忽略的文件,此时该 config 对象是不匹配的。反之,如果 filePath 不匹配任何 ignores 模式,则该 config 对象是匹配的。
  2. 如果 config 对象 有 files

    • files 数组的任何一个元素是 pattern * or patterns ending in /** or /*,那么该 config 对象是匹配的。

    • 如果 filePath 匹配了 files(files 数组中只要有一个匹配就算匹配,如果 files 的元素是数组,那么需要这个数组每个元素都匹配才算匹配, 参见 demo1)。 接下来还需要检查 ignores 中的规则。如果 filePathignores 中的某个模式所匹配,意味着该文件路径是被忽略的,此时该 config 对象是不匹配的。反之,如果 filePath 不在 ignores 的范围内,则该 config 对象是匹配的(参见 demo2)。

      // demo1
      { 
          files: [
              '**/*.js', 
              [
                  '**/*.mjs', 
                  (filePath) => filePath.includes('app')
              ]
          ]
      }
      // 如果 filePath 是 test.js, 它匹配 `files[0]`,也就是 `'**/*.js'`,所以 filePath 匹配了 files 
      // 如果 filePath 是 test.mjs, 它不匹配 `files[0]`,`files[1]` 中只匹配了`'**/*.mjs'`,不匹配 `filePath.includes('app')`,所以是不匹配 `files[1]` 的,因为 `files[0] & files[1]` 都不匹配,所以 filePath 不匹配 files
      
      // demo2
      const config = {
          files: [
              "src/**/*.js",  // 匹配 src 目录下的所有 JS 文件
              "src/utils/*.js" // 匹配 src/utils 目录下的所有 JS 文件
          ],
          ignores: [
              "src/utils/test.js", // 忽略 src/utils/test.js 文件
              "src/**/*.spec.js"    // 忽略 src 目录下所有以 .spec.js 结尾的文件
          ]
      };
      
      // 要检查的文件路径
      const filePath = "src/utils/test.js";
      // 尽管 `filePath` `src/utils/test.js` 匹配了 `files` 中的模式,但由于它也被 `ignores` 中的规则匹配,因此该条 config item 不会被添加
      

如何将匹配的 config 对象合并?

匹配的 config 对象是按照一定的策略合并的。eslint 内部对 config 对象的每个属性都定义了合并的策略(参见:/xxx-project/node_modules/eslint/lib/config/flat-config-schema.js -> flatConfigSchema)。

flatConfigSchema 中定义了 language, languageOptions, rules, plugins 等校验(validate)以及合并(merge)的方式。

image.png

image.png

合并逻辑伪代码:

合并多个配置对象(filteredConfigObjects),并根据每个属性的合并策略(configSchema)来决定如何合并这些对象的属性。

// filteredConfigObjects: 某个 filePath 匹配的 config 对象
filteredConfigObjects.reduce((result, obj) => {
   return merge(result, obj)
}, {})

// configSchema: 每个属性的合并策略
merge(result, obj) {
  for (const [key, strategy] of configSchema) {
      if (key in result || key in obj) {
          const mergeFunc = strategy.merge
          const value = mergeFunc(result[key], obj[key])
          
          if (value !== undefined) {
		result[key] = value;
	   }
      }
  }
  
  return result;
}

languageOptions 的合并逻辑

1. languageOptions 有哪些配置?configure Language Options
languageOptions: {
  ecmaVersion, // 默认:"latest",指示正在检查的代码的 ECMAScript 版本。不同的 ECMAScript 版本可能会引入不同的全局变量。例如,ES6 引入了 `Promise` 和 `Map` 等全局对象。设置 `ecmaVersion` 可以确保 ESLint 正确识别和处理这些全局变量。
  sourceType, // 默认:script。其他可选:module, commonjs
  globals, // 
  parser, // 默认是 espree, 可指定其他兼容 eslint 的 parser, 例如:@babel/eslint-parser, @typescript-eslint/parser
  parserOptions: {}, // 如果是自定义 parser,parserOptions 的值会直接传给自定义 parser,用户可查看相关 parser 的文档看支持哪些 options.
  
   // 如果使用默认 parser(espree), 那么支持以下配置(默认都是 false)
  parserOptions: {
    allowReserved,
    ecmaFeatures: {
      globalReturn,
      impliedStrict,
      jsx // 是否启用 jsx
    }
  },
}
2. languageOptions 如何合并?

parser 属性是后者替换前者,其他属性(ecmaVersion, sourceType, globals 等)是 deepMerge。

deepMerge:

const config1 = {
    languageOptions: {
      sourceType: 'commonjs',
      globals: {
        performance: true, 
        Storage: false
      },
      parser: {
          mats: {name: 'typescript-eslint/parser', version: '8.17.0'}, 
          parse: function(code, options) {},
          parseForESLint: function(code, parserOptions) {}
      },
      parserOptions: {}
    }
};

const config2 = {
  languageOptions: {
    sourceType: 'module',
    globals: {
      onhashchange: true,
      performance: false
    },
    parser: {
        meta: { name: 'vue-eslint-parser', version: '9.4.3' },
        parse: function(code, options) {},
        parseForESLint: function(code, parserOptions) {}
    },
    parserOptions: {
      parser: {
        js: 'espree',
        jsx: 'espree',
        ts: {
          meta: {
            name: 'typescript-eslint/parser',
            version: '8.17.0'
          }
        },
        tsx: {
          meta: {
            name: 'typescript-eslint/parser',
            version: '8.17.0'
          }
        }
      }
    }
  }
};

deepMerge(config1, config2) => {
  languageOptions: {
    sourceType: 'module',
    globals: {
      performance: false, 
      Storage: false,
      onhashchange: true
    },
    parser: {
      meta: { name: 'vue-eslint-parser', version: '9.4.3' },
      parse: function(code, options) {},
      parseForESLint: function(code, parserOptions) {}
    },
    parserOptions: {
      parser: {
        js: 'espree',
        jsx: 'espree',
        ts: {
          meta: {
            name: 'typescript-eslint/parser',
            version: '8.17.0'
          }
        },
        tsx: {
          meta: {
            name: 'typescript-eslint/parser',
            version: '8.17.0'
          }
        }
      }
    }
  }
}
  1. 如果属性是基础类型 or 数组类型,后者替换前者(res.sourceType = 'module')
  2. 如果属性是对象类型(例如 config1.globals 和 config2.globals),不同的属性合并,相同的属性后者替换前者(languageOptions.globals = { performance: false, Storage: false, onhashchange: true }

plugins 的合并逻辑

  1. 如果 firstsecond 同时存在相同的 key(例如:vue),但是值不相同就会报错。
  2. 优先使用 second 中的值,如果不存在,则使用 first 中的值。
[   
    // config 1 ----> first
    {
        plugins: {
          @: {},
          vue: {},
        },
    },
    // config 2 ----> second
    {
         plugins: {
          @typescript-eslint: {},
        }
    }
]

// 合并之后的结果是
{
    "plugins": {
        @: {},
        vue: {},
        @typescript-eslint: {},
    },
}

源代码:

/xxx-project/node_modules/@eslint/config-array/dist/cjs/index.cjs ConfigArray -> getConfigWithStatus

  1. 找到与 filePath 匹配的 config 对象 image.png

  2. 将匹配的 config 对象合并 image.png

language, languageOptions, parser, parserOptions

language, languageOptions 是顶层配置,parser, parserOptions 属于 languageOptions 中的配置,parserOptions 中还可以配置 parser

{
    language,
    languageOptions: {
        parser,
        parserOptions: {
            parser: {
                js ='espree'
                jsx ='espree'
                ts ={meta: {…}, parseForESLint: ƒ}
                tsx ={meta: {…}, parseForESLint: ƒ}
            }
        }
    }
}
  • languageOptions.parser:全局指定 ESLint 使用的解析器。
  • languageOptions.parserOptions.parser:在特定情况下(如不同文件类型)指定使用的解析器,允许更细粒度的控制。