Yuto Blog
speaker-deck-iconyoutube-iconrss-iconx-icongithub-icon

@eslint/compatのfixupPluginRulesとは何をやっているのか

目次

Intro

eslint/compatにはfixupPluginRulesというAPIが提供されています。 このAPIは、少し前にeslint公式が公開したIntroducing ESLint Compatibility Utilitiesというブログで登場していますが、あまり多くは語られていないです。 とりあえず使っておけば確かに動くような気がしますが、今回は個人的興味もあり、このAPIは一体なんなのかまとめたいと思います。

fixupPluginRulesの使い方

そもそもFlat Configとは何かみたいな説明は今回は省こうと思います。(気になる方はこちらから)

fixupPluginRuleseslint.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;
}

処理の流れとしては以下のような流れになっています。

  1. fixedUpPluginReplacementsにsetされていればそれを返すように(キャッシュを使う)
  2. fixedUpPluginsに追加(add)されている、もしくは引数で受け取ったプラグインにrulesが定義されていなければそのまま返す
  3. プラグインを展開し、key valueの形式に変換される。{ ruleId: fixupRule(ruleDefinition) }のようにして返す
  4. fixedUpPluginReplacementsfixedUpPluginsに追加する

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) {
  ...

引数にはFixupRuleDefinitionFixupLegacyRuleDefinitionを受け取り、返り値は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という二つのプロパティを持っているのに対し、FixupLegacyRuleDefinitionRuleModuleのcreate型になっているということです。 そのため先ほどのconst isLegacyRule = typeof ruleDefinition === "function";は正しく判定できていることがわかりますね。

ちなみにflat configでカスタムルールを作ろうとすると、metaとcreateというルールは見ることになるかと思います。(doc

つまりfixupPluginRulesとはなんなのか

少し話がごちゃついてしまいましたが、なんとなく実装コードを読んだことで、fixupPluginRulesとは、Flat Config対応前とFlat Config対応後でプラグインの異なっているインターフェースをFlat Config用に変換してくれる関数であるということが理解できました。

実際に使った感想でもなんとなくそんなイメージかなとは思っていましたが、実装をみたことで動きとして理解でき、しっくりきました。

関連/参考

0