@eslint/compatのfixupPluginRulesとは何をやっているのか
目次
Intro
eslint/compatにはfixupPluginRules
というAPIが提供されています。
このAPIは、少し前にeslint公式が公開したIntroducing ESLint Compatibility Utilitiesというブログで登場していますが、あまり多くは語られていないです。
とりあえず使っておけば確かに動くような気がしますが、今回は個人的興味もあり、このAPIは一体なんなのかまとめたいと思います。
fixupPluginRulesの使い方
そもそもFlat Configとは何かみたいな説明は今回は省こうと思います。(気になる方はこちらから)
fixupPluginRules
はeslint.config.js
を作成し、以下のようにして使用します。(参考)
import { fixupPluginRules } from "@eslint/compat";
import somePlugin from "eslint-plugin-some-plugin";
export default [
{
plugins: {
// insert the fixed plugin instead of the original
somePlugin: fixupPluginRules(somePlugin),
},
rules: {
"somePlugin/rule-name": "error",
},
},
];
importしたプラグインを引数にしてfixupPluginRules
を渡すだけで良いです。とてもシンプルなインターフェースになっています。
どんな実装になっているのか
では次に、具体的にどのような実装になっているか見て行きましょう。fixupPluginRules
のAPIの実装はpackages/compat/src/fixup-rules.jsという中に書かれています。
fixupPluginRulesについて
必要箇所だけピックアップすると、以下のようになります。
/**
* Tracks the original plugin definition and the fixed-up plugin definition.
* @type {WeakMap<FixupPluginDefinition,FixupPluginDefinition>}
*/
const fixedUpPluginReplacements = new WeakMap();
/**
* Tracks all of the fixed up plugin definitions so we don't duplicate effort.
* @type {WeakSet<FixupPluginDefinition>}
*/
const fixedUpPlugins = new WeakSet();
...
/**
* Takes the given plugin and creates a new plugin with all of the rules wrapped
* to provide the missing methods on the `context` object.
* @param {FixupPluginDefinition} plugin The plugin to fix up.
* @returns {FixupPluginDefinition} The fixed-up plugin.
*/
export function fixupPluginRules(plugin) {
// first check if we've already fixed up this plugin
if (fixedUpPluginReplacements.has(plugin)) {
return fixedUpPluginReplacements.get(plugin);
}
/*
* If the plugin has already been fixed up, or if the plugin
* doesn't have any rules, we can just return it.
*/
if (fixedUpPlugins.has(plugin) || !plugin.rules) {
return plugin;
}
const newPlugin = {
...plugin,
rules: Object.fromEntries(
Object.entries(plugin.rules).map(([ruleId, ruleDefinition]) => [
ruleId,
fixupRule(ruleDefinition),
]),
),
};
// cache the fixed up plugin
fixedUpPluginReplacements.set(plugin, newPlugin);
fixedUpPlugins.add(newPlugin);
return newPlugin;
}
処理の流れとしては以下のような流れになっています。
fixedUpPluginReplacements
にsetされていればそれを返すように(キャッシュを使う)fixedUpPlugins
に追加(add)されている、もしくは引数で受け取ったプラグインにrulesが定義されていなければそのまま返す- プラグインを展開し、key valueの形式に変換される。
{ ruleId: fixupRule(ruleDefinition) }
のようにして返す fixedUpPluginReplacements
とfixedUpPlugins
に追加する
newPlugin
を定義する際に、ルールに対してfixupRule
という関数を使っています。これはどのような処理を行っているのでしょうか。
fixupRuleについて
少しだけ、fixupRule
関数についてもみておきましょう。実装はこちらです
まずインターフェースをみてみましょう。
/**
* Takes the given rule and creates a new rule with the `create()` method wrapped
* to provide the missing methods on the `context` object.
* @param {FixupRuleDefinition|FixupLegacyRuleDefinition} ruleDefinition The rule to fix up.
* @returns {FixupRuleDefinition} The fixed-up rule.
*/
export function fixupRule(ruleDefinition) {
...
引数にはFixupRuleDefinition
かFixupLegacyRuleDefinition
を受け取り、返り値はFixupRuleDefinition
を返すようになっています。
このインターフェースから、FixupRuleDefinition
はFlat Config用の型で、FixupLegacyRuleDefinition
は旧ESLintの型のことだとわかります。
そして実装のなかで、const isLegacyRule = typeof ruleDefinition === "function";
という処理があります。古いルールは関数になっているようです。
より理解度を深めるために、先ほどの型の中身を見てみましょう。
/** @typedef {import("eslint").Rule.RuleModule} FixupRuleDefinition */
/** @typedef {FixupRuleDefinition["create"]} FixupLegacyRuleDefinition */
export namespace Rule {
interface RuleModule {
create(context: RuleContext): RuleListener;
meta?: RuleMetaData | undefined;
}
ここでわかるのは、FixupRuleDefinition
はcreateとmetaという二つのプロパティを持っているのに対し、FixupLegacyRuleDefinition
はRuleModule
のcreate型になっているということです。
そのため先ほどのconst isLegacyRule = typeof ruleDefinition === "function";
は正しく判定できていることがわかりますね。
ちなみにflat configでカスタムルールを作ろうとすると、metaとcreateというルールは見ることになるかと思います。(doc)
つまりfixupPluginRulesとはなんなのか
少し話がごちゃついてしまいましたが、なんとなく実装コードを読んだことで、fixupPluginRules
とは、Flat Config対応前とFlat Config対応後でプラグインの異なっているインターフェースをFlat Config用に変換してくれる関数であるということが理解できました。
実際に使った感想でもなんとなくそんなイメージかなとは思っていましたが、実装をみたことで動きとして理解でき、しっくりきました。
関連/参考
0