🤖

stylelint開発ガイドールール

に公開

内容の目的

本内容は、stylelint開発ガイドールール を翻訳し、個人活用のために整理しています。

ルールの作成

Stylelint のルールの作成、改善、バグ修正にご協力ください!

ルールの追加

コード貢献の準備 をしましょう。

ルールを定義する

ルールは以下の条件を満たすべきです:

  • 標準的な CSS 構文のみに対応すること
  • 特定の癖のあるパターンに依存しない、一般的に有用なものであること

そして、次のような特徴を持っていることが望ましいです:

  • 明確に定義された「完了状態」があること
  • 単一の目的を持つこと

ルール名は 2 部構成です:

  • 対象となるもの(例:at-rule
  • チェックする内容(例:disallowed-list

ただし、ソース全体に適用される場合は前半部分は不要です。

テストを書く

以下のようなパターンに対してテストケースを書く必要があります:

  • 問題があるとされるパターン
  • 問題がないとされるパターン

推奨されるスタイル:

  • 現実的な CSS を使用し、... のような省略表記は避ける
  • セレクタをターゲットにする場合は空のルールを使う(例:.foo {})
  • { } ではなく {} を使う
  • デフォルトでは a タイプセレクタ、@media アットルール、color プロパティ、red 値、(min-)width メディア特性を使用
  • クラス名や ID、カスタムプロパティには .foo, #bar, --baz を使用

以下にも注意しましょう:

  • テスト全体で行番号と列位置を変える
  • 2 つの警告を含むテストを 1 つ以上含める
  • 非標準構文のテストは isStandardSyntax* ユーティリティに任せ、ルール本体では扱わない

見落としがちな境界ケース

ルールが次のようなケースをどう扱うか考慮してください:

  • 変数(例:var(--custom-property)
  • CSS 全体のキーワード(例:initial
  • 文字列(例:content: "..."
  • コメント(例:/* コメント */
  • 空の関数(例:var()
  • url() 関数、特に data URI
  • ベンダープレフィックス(例:@-webkit-keyframes
  • 大文字小文字の違い(例:@KEYFRAMES
  • 擬似クラスと擬似要素の組み合わせ(例:a:hover::before
  • ネスト構文(例:& a {}
  • 空白と句読点(例:rgb(0,0,0)rgb(0, 0, 0) の比較)

ルールを実装する

次の方針を守ってください:

  • デフォルトでは厳しく
  • 柔軟性を持たせるためにセカンダリ ignore オプションを用意する
  • SCSS のような言語拡張に依存しないこと

次を活用してください:

  • PostCSS API
  • 構文専用パーサー
  • ユーティリティ関数

PostCSS API

PostCSS API を使用して CSS の構文木をナビゲート・解析します。ノードのループ処理には walk 系のイテレータ(例:walkDecls)を使いましょう。

ノードに対して findsome を使う場合は、type を明示的に確認しましょう:

const hasProperty = nodes.find(
  ({ type, prop }) => type === "decl" && prop === propertyName
);

PostCSS AST から生の文字列(raw strings)を取得する際は、node.raw() の代わりに node.raws を使用してください。

構文要素ごとの専用パーサー

ルールによっては、次のようなパーサーの使用も推奨されます:

これらのパーサーを、正規表現や indexOf 検索の代わりに使用することで、大きな利点があります(たとえ、常に最もパフォーマンスが高い方法ではないとしても)。

ユーティリティ関数

Stylelint には、既存のルールで使用されている ユーティリティ関数 が用意されており、あなたの開発にも役立つ可能性があります。どのような関数が利用可能かを把握するためにも、一通り目を通してみてください。(もし汎用的に役立ちそうな新しい関数があれば、リストに追加しましょう!)

以下のユーティリティ関数は必ず使用してください:

  • validateOptions():無効なオプションに対してユーザーに警告を出すための関数
  • isStandardSyntax*():ノードや文字列をチェックする前に、非標準構文を除外するための関数群
  • report():Lint の問題を報告するための関数
report() の位置引数について

report() を使用する際、問題の位置を以下のようなさまざまな方法で指定できます:

  • node のみ:指定されたノード全体の範囲を暗黙的に対象とします
  • word:シリアライズされたノード内の単語の最初の出現箇所
  • indexendIndex のオフセット:ノード内のインデックス範囲
  • startend の位置情報:ノード内の 範囲

これらの方法には、それぞれ以下の観点でメリットとデメリットがあります:

  • 利便性(convenience) — 使いやすさ
  • 狭さ(narrowness) — 問題の位置との一致精度
  • 正確さ(correctness) — バグや位置の誤りの可能性
  • パフォーマンス(performance) — 位置情報の計算コスト

たとえば、nodeword のみを使用する方法は便利ですが、位置の精度や正確さを犠牲にすることがあります。

以下は word を使用した例で、この場合の位置はルール内のセレクタとなります:

root.walkRules((ruleNode) => {
  const { selector } = ruleNode;

  report({
    result,
    ruleName,
    message: messages.rejected(),
    node: ruleNode,
    word: selector
  });
});

word を使用する方法は、ノード内の位置を特定する手段として最も遅い方法です。特に子ノードが多い場合(例:アットルール)ではその傾向が顕著です。

オフセットや位置情報を使用する際には、nodeFieldIndices ユーティリティ(例:declarationValueIndex())を使って、ノードの一部のインデックスを取得することができます。これらのユーティリティは PostCSS AST の raw フィールドも考慮しています。

以下は、indexendIndex、および declarationValueIndex() ユーティリティを使用して、宣言の値全体を対象とした位置指定の例です:

root.walkDecls((declNode) => {
  const { prop, value } = declNode;

  const index = declarationValueIndex(decl);
  const endIndex = index + value.length;

  report({
    result,
    ruleName,
    message: messages.rejected(),
    node: declNode,
    index,
    endIndex:
  });
});

このアプローチは、構文要素ごとの専用パーサーを使用する場合にも効果的です。

root.walkDecls((declNode) => {
  const { prop, value: declValue } = declNode;

  valueParser(declValue).walk(({ value, sourceIndex }) => {

    const index = declarationValueIndex(decl) + sourceIndex;
    const endIndex = index + value.length;

    report({
      result,
      ruleName,
      message: messages.rejected(),
      node: declNode,
      index,
      endIndex:
    });
  });
});

オプションの追加

各ルールは、必須の「プライマリオプション」と、任意の「セカンダリオプション」を受け取ることができます。

未使用の機能でツールを乱雑にしないよう、明確に要望された ユースケースに対応する場合のみ、ルールにオプションを追加してください。

プライマリオプション(Primary)

すべてのルールには 必ず プライマリオプションが必要です。たとえば:

  • "font-weight-notation": "numeric" の場合、プライマリオプションは "numeric"
  • "selector-max-type": [2, { "ignoreTypes": ["custom"] }] の場合、プライマリオプションは 2

ルール名は、プライマリオプションを明示的に指定できるように命名されています。
たとえば、font-weight-numeric: "always"|"never" ではなく、font-weight-notation: "numeric"|"named-where-possible" のようにします。
なぜなら、font-weight-named: "never" は暗黙的に「常に numeric」を意味しますが、font-weight-notation: "numeric" はそれを 明示的 に表現しているためです。

セカンダリオプション(Secondary)

一部のルールでは、特殊なケース(エッジケース)に対応するために、追加の柔軟性が求められます。そうした場合には、任意のセカンダリオプションオブジェクトを使用することができます。例えば:

  • "font-weight-notation": "numeric" の場合、セカンダリオプションはありません
  • "selector-max-type": [2, { "ignore": ["descendant"] }] の場合、セカンダリオプションオブジェクトは { "ignore": ["descendant"] } です

最も一般的なセカンダリオプションは "ignore": []"except": [] です。

"ignore" および "except" キーワード

"ignore""except" オプションには、あらかじめ定義されたキーワードの配列を指定します(例:["relative", "first-nested", "descendant"]):

  • "ignore" は特定のパターンを無視します
  • "except" は特定のパターンに対してプライマリオプションの動作を反転させます
ユーザー定義の "ignore*"

一部のルールでは、ユーザー定義による無視リストを受け付けます。形式は "ignore<項目名>": [] のようになります(例:"ignoreAtRules": [])。

ignore* オプションにより、ユーザーは 設定レベルで 非標準構文を無視できるようになります。例えば:

  • CSS Modules で導入される :global:local 擬似クラス
  • SCSS で導入される @debug@extend のアットルール

開発手法や言語拡張は移り変わりが激しいため、この仕組みにより、不要なコードがコードベースに溜まっていくのを防ぐことができます。

ルールがプライマリオプションとして配列を受け取れるようにしたい場合は、ルール関数に primaryOptionArray = true プロパティを設定する必要があります。例:

function rule(primary, secondary) {
  return (root, result) => {
    /* .. */
  };
}

rule.primaryOptionArray = true;

module.exports = rule;

ここで1つ注意点があります:ルールがプライマリオプションとして配列を受け取る場合、同時にオブジェクトも受け取ることはできません。可能な限り、プライマリオプションとして配列を受け取らせたいのであれば、さまざまなデータ構造を許可するのではなく、配列のみを受け入れるようにしてください。

問題メッセージを追加する

問題メッセージは次の形式で追加してください:

  • "Expected [何か] [ある状況で]"
  • "Unexpected [何か] [ある状況で]"

ルールに自動修正(autofix)がある場合は、以下の形式を使用してください:

  • 短い文字列には: 'Expected "[未修正の値]" to be "[修正後の値]"'
  • 長い文字列には: 'Expected "[プライマリオプション]" ... notation'

自動修正(autofix)の追加

ルールによっては、PostCSS API を使用して PostCSS の抽象構文木(AST)を変更することで、問題を自動的に修正することが可能です。

ルールに meta.fixable = true を設定してください:

const meta = {
  url: /* .. */,
+ fixable: true,
};

fix コールバックを report ユーティリティ に渡してください:

function rule(primary, secondary) {
  return (root, result) => {
    /* .. */

+   const fix = () => { /* put your mutations here */ };

    report({
      result,
      ruleName,
      message,
      node,
+     fix
    });
  };
}

languageOptions サポートの追加

ルールによっては、languageOptions 設定プロパティへの対応が必要になる場合があります。

例えば:

import { basicKeywords } from '../../reference/keywords.mjs';

function rule(primary, secondary) {
  return (root, result) => {
    /* .. */

    if (!validOptions) return;

+   const languageCssWideKeywords = result.stylelint.config?.languageOptions?.syntax?.cssWideKeywords ?? [];

+   const cssWideKeywords = new Set([...basicKeywords, ...languageCssWideKeywords]);

    /* .. */
  };
}

Context(コンテキスト)

context は以下のプロパティを持つオブジェクトです:

  • configurationComment(文字列): /* stylelint-disable */ のような設定コメントの前に付けられる文字列。
  • fix(ブール値): true の場合、ルールは自動修正(autofix)を適用できます。
  • newline(文字列): 現在 Lint 対象となっているファイルで使用されている改行コード。

[!警告]
context.fix プロパティに基づいて修正適用の可否を制限する手法は非推奨となり、代わりに 設定コメント を正しく処理できる fix コールバックの使用が推奨されています。

context.fixtrue の場合は、PostCSS API を使用して root を変更し、report() を呼び出す前に早期リターンしてください。

function rule(primary, secondary, context) {
  return (root, result) => {
    if (context.fix) {
      // Apply fixes using PostCSS API
      return; // Return and don't report a problem
    }

    report(/* .. */);
  };
}

README を作成する

各ルールには、以下の形式で README を付けてください:

  1. ルール名
  2. 一行での説明文
  3. 典型的なコード例
  4. 必要に応じて、拡張された説明
  5. オプションの説明
  6. (各オプション値に対する)問題とされるパターンの例
  7. (各オプション値に対する)問題とされないパターンの例
  8. (該当する場合)任意の追加オプション

一行説明の形式は次の通りです:

  • no 系ルール: "Disallow ..."(...を禁止する)
  • max 系ルール: "Limit ..."(...を制限する)
  • "always""never" を受け入れるルール: "Require ..."(...を要求する)
  • それ以外: "Specify ..."(...を指定する)

以下の点に注意してください:

  • テストケースから例を選ぶこと
  • 例やオプションには標準の CSS 構文のみを使用すること
  • ルールの意図を伝えるのに必要最小限の例のみを追加し、境界ケースの列挙は避けること
  • css コードブロックの前に <!-- prettier-ignore --> を付けること
  • "this rule"(このルール)という表現でルールに言及すること(例:"This rule ignores ...")
  • 拡張説明内には関連する先行事例の箇条書きを含めること
  • コード例内の矢印は、強調する構文の先頭に揃えること
  • コード例のテキストはできるだけ左端に揃えること

例えば:

 @media screen and (min-width: 768px) {}
/**                 ↑          ↑
  *       These names and values */

他のルールの README を見て、より慣習的なパターンを参考にしてください。

ルールの接続

最後のステップとして、新しいルールへの参照を次の場所に追加します:

ルールにオプションを追加する

次の手順を実行してください:

  1. コード貢献の準備 を整える。
  2. オプションをテストする新しいユニットテストを追加する。
  3. ルールのバリデーションを変更して、新しいオプションを許容するようにする。
  4. テストが通るように、できるだけ少ないロジックをルールに追加する。
  5. 新しいオプションに関するドキュメントを追加する。
  6. オプションを ルールの型定義 に追加する。

ルールのバグを修正する

次の手順を実行してください:

  1. コード貢献の準備 を整える。
  2. バグを示す失敗するユニットテストを書く。
  3. 新しいテストが通るようにルールを調整する。

ルールを非推奨にする

ルールを非推奨にすることは頻繁には行いませんが、実施する場合は次の手順を実行してください:

  1. rule.meta = { deprecated: true } のように、非推奨であることを示すメタデータを追加する。
  2. stylelintType'deprecation' に設定する。
  3. 必要に応じて stylelintReference を設定し、そのルールのドキュメントの特定バージョンへのリンクを指定することで、常に参照可能な状態にしておく。

例えば:

result.warn(
  `"your-namespace/old-rule" has been deprecated and will be removed in 7.0. Use "your-namespace/new-rule" instead.`,
  {
    stylelintType: "deprecation",
    stylelintReference:
      "https://github.com/your-org/your-stylelint-plugin/blob/v6.3.0/src/rules/old-rule/README.md"
  }
);

ルールのパフォーマンスを改善する

任意のルールに対して、有効な設定でベンチマークを実行するには以下のコマンドを使用します:

npm run benchmark-rule -- ruleName ruleOptions [config]

ruleOptions 引数が文字列またはブール値以外の場合は、引用符で囲まれた有効な JSON である必要があります。

npm run benchmark-rule -- value-keyword-case lower
npm run benchmark-rule -- value-keyword-case '["lower", {"camelCaseSvgKeywords": true}]'

config 引数を指定する場合も、同様の形式で指定します:

npm run benchmark-rule -- value-keyword-case '["lower", {"camelCaseSvgKeywords": true}]' '{"fix": true}'

このスクリプトは Bootstrap の CSS(CDN から)を読み込み、指定されたルールに従って処理を行います。

最終的には、以下のような簡単な統計情報が表示されます:

Warnings: 1441
Mean: 74.17598357142856 ms
Deviation: 16.63969674310928 ms

新しいルールの作成時や既存ルールのリファクタリング時には、これらの測定結果を参考にしてコードの効率を判断してください。

Stylelint のルールは、非常に大規模な CSS コードベースにおいて、すべての宣言ノードの値を何度もチェックすることがあるため、パフォーマンスに注意を払い、改善の余地があれば積極的に対応する価値があります。

ルールのパフォーマンス改善は、短時間で取り組める貢献方法としてもおすすめです。
気になるルールを1つ選んで、スピードアップできる点がないかぜひ確認してみてください。

プルリクエストには、必ずベンチマークの測定結果を含めるようにしましょう!


目次に戻る

Discussion