eslint-plugin-rn-a11y というプラグインを作ってるので話

Others

ESLint Plugin 作ってますという無限にあるエントリの 1 つ。
https://github.com/grgr-dkrk/eslint-plugin-rn-a11y/

これは何

React Native のアクセシビリティチェックプラグイン。

実は React Native でアクセシビリティをチェックするプラグインは eslint-plugin-react-native-a11y としてすでにあるけど、ルールのほとんどが TypeScript の型でカバー可能、対応バージョンが古く、さらにメンテがあまりされていないので、fork 的な形で作った。
一部ルールはそちらのプラグインから移植している。それらのルールの権利はそちらに帰する。

何をするの

まだできることは少ない。

  • deprecated な props の使用禁止(types の更新が最近まで忘れ去られていたものなので、一時的に追加)
  • touchable なコンポーネントをアクセシブルにするためのチェック
  • 画像に代替テキストを設定するための提案
  • accessibilityLabel や accessibilityHint の valid
  • E2E のテスト ID 代わりに accessibilityLabel を設定していないかのチェック
  • アクセシブルでなくなる props の使用禁止(本当は RN 側で修正したい)

その他実験的なルールの実装を進めてる。
簡易ボタンサイズ・コントラストチェッカーなど。

特徴

React の eslint-plugin-jsx-a11y は HoC の対応に弱い。
一部ルールにしか対応していない。styled-component が読めないなどをよく聞く。

このプラグインでは .eslintrc.js のオプションとして、汎用的に設定できるようカバー。
具体的には以下コードのように、各ルールにコンポーネント名の配列を渡すようにできる。

eslintrc.js

// eslintrc.js

const Image = ['MyPict']
const Touchable = ['MyTouchableOpacity', 'MyPressable']

const CustomComponents = {
  Image,
  Touchable,
}

module.exports = {
  /* ... */
  rules: {
    'rn-a11y/no-nested-touchables': ['error', CustomComponents],
    'rn-a11y/touchable-has-alt': ['error', CustomComponents],
    'rn-a11y/no-long-alt': ['error', { ...CustomComponents, limit: 100 }],
  },
}

ユースケース的に、コンポーネント名を列挙するスクリプトを書く、という信頼性に欠けるものではある。
あと React と違って使うシーンは限られる…。

実装時の話

ただの宣伝になってはいけないので、実装時に詰まったことを書き残す。

型定義

このライブラリは TypeScript で実装している。

既存の JSX 用 ESLint プラグインは JavaScript での実装なので気にならなかったが、TypeScript ではかなり引っかかる。
というのも ESLint の Visitor 部分や jsx-ast-utils が対応する型定義を持ってないため。
ESLint はオリジナルの型を extends すればいいけど、 jsx-ast-utils はそもそも型定義がないので作った。

JSX 向けに ast-types-flow という Flow 用の型定義があるので、これをローカル側に持って jsx-ast-utils の宣言にマージしている。
ただ、普通にローカルの型定義を import することはできず、型宣言が無効になってしまう。
inline import で対処してなんとかなった。

jsx-ast-utils.d.ts

export function elementType(
  // これ
  node: import('../ast').JSXElement,
): string

これに対する要望的な Issue が TypeScript 辺りで見た気がするものの失念したので省略。

TypeScript の parser

テストは Jest と、ESLint の RuleTester インスタンスを使う。
今回は TypeScript に対してやるので、さらに @typescript-eslint/parser が必須になる。
ところが以下は NG。

myRule.test.ts

const ruleTester = new RuleTester({
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
  },
  // ここ。NG
  parser: '@typescript-eslint/parser',
})

ESLint のバージョン 6 で破壊的変更があり、parser オプションに絶対パスを使用しなければならなくなった。
Migrating to v6.0.0 - ESLint

そのため parserrequire.resolve() を使うことで、ようやくパースできる。

myRule.test.ts

const ruleTester = new RuleTester({
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
  },
  // ここ
  parser: require.resolve('@typescript-eslint/parser'),
})

Option 使用時のエラー

schema でオプションの type を設定しておかないと、オプションを指定しても ESLint 側でエラーになる。
これを完全に失念していて詰まっていた。

myRule.ts

export const rule: Rule.RuleModule = {
  meta: {
    type: 'problem',
    docs: {
      description: 'Disallow using deprecated props.',
    },
    // これ
    schema: {
      type: 'object',
      items: { type: 'array' },
      uniqueItems: true,
    },
  },
  // 省略

ちなみに RuleTester では options を用いた時のテストもできる。便利。

myRule.test.ts

valid: [
  {
    code: `<Image accessible accessibilityLabel="${'a'.repeat(
      130,
    )}" source={require('test.jpg')} />`,
    options: [
      {
        limit: 130,
      },
    ],
  },
]

Publish

eslint-plugin-react-native-a11y という名前はすでにあるので、 eslint-plugin-rn-a11y という胡散臭い名前にしている。
これが胡散臭すぎたのか npm のスパムチェックにひっかかってしまった。

指示に応じてメールを送ると、メールじゃなくてサポートに送ってねという返信が来た。
詳細は書いてなかったけど年末だったから?とりあえずサポートページで問い合わせた。

  • スパム扱いされた
  • このライブラリは eslint-plugin-react-native-a11y の拡張でありコピー品ではない

ことを伝えてようやく解除された。

オチ

ありません