eslint-plugin-smarthr 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/libs/format_styled_components.js +1 -1
  3. package/package.json +4 -4
  4. package/rules/a11y-help-link-with-support-href/index.js +1 -1
  5. package/rules/a11y-image-has-alt-attribute/index.js +0 -1
  6. package/rules/a11y-prohibit-useless-sectioning-fragment/index.js +9 -44
  7. package/rules/a11y-trigger-has-button/index.js +18 -36
  8. package/rules/best-practice-for-async-current-target/index.js +14 -21
  9. package/rules/best-practice-for-button-element/index.js +10 -23
  10. package/rules/best-practice-for-data-test-attribute/index.js +9 -12
  11. package/rules/best-practice-for-date/index.js +16 -29
  12. package/rules/best-practice-for-nested-attributes-array-index/index.js +7 -15
  13. package/rules/best-practice-for-remote-trigger-dialog/index.js +10 -23
  14. package/rules/best-practice-for-tailwind-prohibit-root-margin/index.js +5 -93
  15. package/rules/design-system-guideline-prohibit-double-icons/index.js +6 -31
  16. package/rules/prohibit-export-array-type/index.js +6 -9
  17. package/rules/require-i18n-text/README.md +123 -0
  18. package/rules/require-i18n-text/index.js +94 -0
  19. package/rules/trim-props/index.js +13 -16
  20. package/test/a11y-help-link-with-support-href.js +2 -1
  21. package/test/a11y-prohibit-useless-sectioning-fragment.js +7 -7
  22. package/test/a11y-trigger-has-button.js +8 -7
  23. package/test/best-practice-for-button-element.js +0 -3
  24. package/test/best-practice-for-data-test-attribute.js +9 -8
  25. package/test/best-practice-for-remote-trigger-dialog.js +3 -9
  26. package/test/best-practice-for-tailwind-prohibit-root-margin.js +16 -7
  27. package/test/design-system-guideline-prohibit-double-icons.js +1 -3
  28. package/test/require-i18n-text.js +170 -0
  29. package/test/trim-props.js +15 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [2.2.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v2.1.0...eslint-plugin-smarthr-v2.2.0) (2025-11-07)
6
+
7
+
8
+ ### Features
9
+
10
+ * a11y-help-link-with-support-hrefで対象となる変数名のバグを修正 ([#890](https://github.com/kufu/tamatebako/issues/890)) ([c9c4984](https://github.com/kufu/tamatebako/commit/c9c49848293fd88640e886c6586ae8d92a99f40c))
11
+ * trim-propsでtemplate literalもautofixの対象にする ([#884](https://github.com/kufu/tamatebako/issues/884)) ([4072950](https://github.com/kufu/tamatebako/commit/40729507f5084308128ca39d5887ee81f2f67dd5))
12
+
13
+ ## [2.1.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v2.0.0...eslint-plugin-smarthr-v2.1.0) (2025-10-29)
14
+
15
+
16
+ ### Features
17
+
18
+ * prohibit-export-array-typeの型チェックをArray<any>の場合も対象にする ([#869](https://github.com/kufu/tamatebako/issues/869)) ([ee4a647](https://github.com/kufu/tamatebako/commit/ee4a64755c36880d8f43c31ebd47244d71ea2fe9))
19
+ * コンポーネントの子要素やプロパティの文字列リテラルを多言語化の観点で検査するルールを追加 ([#815](https://github.com/kufu/tamatebako/issues/815)) ([9dc8bda](https://github.com/kufu/tamatebako/commit/9dc8bda98d1a978dd0e4d61b91c858ef1c117d9c))
20
+
5
21
  ## [2.0.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.11.0...eslint-plugin-smarthr-v2.0.0) (2025-10-16)
6
22
 
7
23
 
@@ -123,4 +123,4 @@ const generateTagFormatter = ({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }) =>
123
123
  }
124
124
  }
125
125
 
126
- module.exports = { generateTagFormatter, checkImportStyledComponents, getStyledComponentBaseName }
126
+ module.exports = { generateTagFormatter }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
7
7
  "main": "index.js",
8
8
  "engines": {
9
- "node": ">=22.20.0"
9
+ "node": ">=22.21.1"
10
10
  },
11
11
  "scripts": {
12
12
  "test": "jest"
@@ -26,7 +26,7 @@
26
26
  "json5": "^2.2.3"
27
27
  },
28
28
  "devDependencies": {
29
- "typescript-eslint": "^8.46.0"
29
+ "typescript-eslint": "^8.46.3"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "eslint": "^9"
@@ -37,5 +37,5 @@
37
37
  "eslintplugin",
38
38
  "smarthr"
39
39
  ],
40
- "gitHead": "dd35a6760dd9022d4b28747996c2016022e94dc6"
40
+ "gitHead": "56a52ee0be0573e3c6117ff2b368543fbbac5c4f"
41
41
  }
@@ -1,7 +1,7 @@
1
1
  const SCHEMA = []
2
2
 
3
3
  const SUPPORT_URL_PREFIX_REGEX = /(\/|\.)support\./
4
- const SUPPORT_IDENTIFIER_REGEX = /(S|(^|_)s)upport(.*)(H(ref|REF)|U(rl|URL))$/
4
+ const SUPPORT_IDENTIFIER_REGEX = /(S|(^|_)s)upport(.*)(H(ref|REF)|U(rl|RL))$/
5
5
  const PATH_OBJ_REGEX = /^((p|P)ath|PATH)\./
6
6
  const SUPPORT_PATH_MEMBER_REGEX = /\.support\./
7
7
 
@@ -1,4 +1,3 @@
1
- const REGEX_IMG = /((i|I)mg|Image)$/ // HINT: Iconは別途テキストが存在する場合が多いためチェックの対象外とする
2
1
  const IMG_ELEMENT = 'JSXOpeningElement[name.name=/((i|I)mg|Image)$/]'
3
2
  const ALT_LIKE_ATTRIBUTE = 'JSXAttribute[name.name=/^(alt|aria-describedby)$/]'
4
3
 
@@ -1,24 +1,9 @@
1
- const BARE_SECTIONING_TAG_REGEX = /^(article|aside|nav|section)$/
2
- const SECTIONING_REGEX = /((A(rticle|side))|Nav|Section)$/
3
- const SECTIONING_FRAGMENT = 'SectioningFragment'
4
- const LAYOUT_REGEX = /((C(ent|lust)er)|Reel|Sidebar|Stack|Base(Column)?)$/
5
- const AS_REGEX = /^(as|forwardedAs)$/
6
-
7
- const includeSectioningAsAttr = (a) => AS_REGEX.test(a.name?.name) && BARE_SECTIONING_TAG_REGEX.test(a.value.value)
8
-
9
- const searchSectioningFragment = (node) => {
10
- switch (node.type) {
11
- case 'JSXElement':
12
- return SECTIONING_FRAGMENT === node.openingElement.name?.name ? node.openingElement : null
13
- case 'Program':
14
- return null
15
- }
16
-
17
- return searchSectioningFragment(node.parent)
18
- }
19
-
20
1
  const SCHEMA = []
21
2
 
3
+ const SECTIONING_FRAGMENT_ELEMENT = 'JSXElement[openingElement.name.name="SectioningFragment"]'
4
+ const SECTIONING_CONTENT_ELEMENT = 'JSXOpeningElement[name.name=/((A(rticle|side))|Nav|Section)$/]'
5
+ const SECTIONING_LAYOUT_ELEMENT = 'JSXOpeningElement[name.name=/((C(ent|lust)er)|Reel|Sidebar|Stack|Base(Column)?)$/]:has(JSXAttribute[name.name=/^(as|forwardedAs)$/][value.value=/^(article|aside|nav|section)$/])'
6
+
22
7
  /**
23
8
  * @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
24
9
  */
@@ -29,31 +14,11 @@ module.exports = {
29
14
  },
30
15
  create(context) {
31
16
  return {
32
- JSXOpeningElement: (node) => {
33
- const name = node.name?.name || ''
34
- let hit = null
35
- let asAttr = null
36
-
37
- if (SECTIONING_REGEX.test(name)) {
38
- hit = true
39
- } else {
40
- asAttr = LAYOUT_REGEX.test(name) && node.attributes.find(includeSectioningAsAttr)
41
-
42
- if (asAttr) {
43
- hit = true
44
- }
45
- }
46
-
47
- if (hit) {
48
- result = searchSectioningFragment(node.parent.parent)
49
-
50
- if (result) {
51
- context.report({
52
- node: result,
53
- message: `無意味なSectioningFragmentが記述されています。子要素である<${name}${asAttr ? ` ${asAttr.name.name}="${asAttr.value.value}"` : ''}>で問題なくセクションは設定されているため、このSectioningFragmentは削除してください`
54
- })
55
- }
56
- }
17
+ [`${SECTIONING_FRAGMENT_ELEMENT}:has(:matches(${SECTIONING_CONTENT_ELEMENT}, ${SECTIONING_LAYOUT_ELEMENT}))`]: (node) => {
18
+ context.report({
19
+ node,
20
+ message: `無意味なSectioningFragmentが記述されています。子要素で問題なくセクションは設定されているため、このSectioningFragmentは削除してください`
21
+ })
57
22
  },
58
23
  }
59
24
  },
@@ -1,5 +1,4 @@
1
1
  const TRIGGER_REGEX = /(Dropdown|Dialog)Trigger$/
2
- const HELP_DIALOG_TRIGGER_REGEX = /HelpDialogTrigger$/
3
2
  const BUTTON_REGEX = /(B|^b)utton$/
4
3
  const ANCHOR_BUTTON_REGEX = /AnchorButton$/
5
4
  const FALSY_TEXT_REGEX = /^\s*\n+\s*$/
@@ -18,54 +17,37 @@ module.exports = {
18
17
  },
19
18
  create(context) {
20
19
  return {
21
- JSXElement: (parentNode) => {
22
- // HINT: 閉じタグが存在しない === 子が存在しない
23
- // 子を持っていない場合はおそらく固定の要素を吐き出すコンポーネントと考えられるため
24
- // その中身をチェックすることで担保できるのでskipする
25
- if (!parentNode.closingElement) {
26
- return
27
- }
28
-
29
- const node = parentNode.openingElement
30
-
31
- if (!node.name.name) {
32
- return
33
- }
34
-
35
- const match = node.name.name.match(TRIGGER_REGEX)
36
-
37
- if (!match || HELP_DIALOG_TRIGGER_REGEX.test(node.name.name)) {
38
- return
39
- }
40
-
20
+ [`JSXElement[openingElement.name.name=${TRIGGER_REGEX}]:not([openingElement.name.name=/HelpDialogTrigger$/])`]: (parentNode) => {
41
21
  const children = filterFalsyJSXText(parentNode.children)
42
22
 
43
23
  if (children.length > 1) {
24
+ const node = parentNode.openingElement
25
+ const match = node.name.name.match(TRIGGER_REGEX)
26
+
44
27
  context.report({
45
28
  node,
46
- message: `${match[1]}Trigger の直下には複数のコンポーネントを設置することは出来ません。buttonコンポーネントが一つだけ設置されている状態にしてください`,
29
+ message: `${match[1]}Trigger の直下には複数のコンポーネントを設置することは出来ません。button要素が一つだけ設置されている状態にしてください`,
47
30
  })
48
-
49
- return
50
- }
51
-
52
- children.forEach((c) => {
53
- // `<DialogTrigger>{button}</DialogTrigger>` のような場合は許可する
54
- if (c.type === 'JSXExpressionContainer') {
55
- return false
56
- }
31
+ } else {
32
+ const c = children[0]
57
33
 
58
34
  if (
59
- c.type !== 'JSXElement' ||
60
- !BUTTON_REGEX.test(c.openingElement.name.name) ||
61
- ANCHOR_BUTTON_REGEX.test(c.openingElement.name.name)
35
+ // `<DialogTrigger>{button}</DialogTrigger>` のような場合は許可する
36
+ c &&
37
+ c.type !== 'JSXExpressionContainer' && (
38
+ c.type !== 'JSXElement' ||
39
+ !BUTTON_REGEX.test(c.openingElement.name.name) ||
40
+ ANCHOR_BUTTON_REGEX.test(c.openingElement.name.name)
41
+ )
62
42
  ) {
43
+ const match = parentNode.openingElement.name.name.match(TRIGGER_REGEX)
44
+
63
45
  context.report({
64
46
  node: c,
65
- message: `${match[1]}Trigger の直下にはbuttonコンポーネントのみ設置してください`,
47
+ message: `${match[1]}Trigger の直下にはbutton要素のみ設置してください(AnchorButtonはa要素のため設置できません)`,
66
48
  })
67
49
  }
68
- })
50
+ }
69
51
  },
70
52
  }
71
53
  },
@@ -33,16 +33,12 @@ module.exports = {
33
33
  const getSourceCodeText = (node) => context.sourceCode.getText(node)
34
34
 
35
35
  return {
36
- MemberExpression: (node) => {
37
- if (node.property && node.property.name === 'currentTarget') {
38
- const eventObjectName = node.object.name
39
-
40
- if (eventObjectName) {
41
- switch (checkFunctionTopVariable(node.parent, eventObjectName, getSourceCodeText)) {
42
- case 1:
43
- context.report({
44
- node,
45
- message: `currentTargetはイベント処理中以外に参照するとnullになる場合があります。awaitの宣言より前にcurrentTarget、もしくはcurrentTarget以下の属性を含む値を変数として宣言してください
36
+ [`:matches(FunctionExpression, ArrowFunctionExpression) MemberExpression[property.name="currentTarget"][object.name]`]: (node) => {
37
+ switch (checkFunctionTopVariable(node.parent, node.object.name, getSourceCodeText)) {
38
+ case 1:
39
+ context.report({
40
+ node,
41
+ message: `currentTargetはイベント処理中以外に参照するとnullになる場合があります。awaitの宣言より前にcurrentTarget、もしくはcurrentTarget以下の属性を含む値を変数として宣言してください
46
42
  - 参考: https://developer.mozilla.org/ja/docs/Web/API/Event/currentTarget
47
43
  - NG例:
48
44
  const onChange = async (e) => {
@@ -55,13 +51,13 @@ module.exports = {
55
51
  await hoge()
56
52
  fuga(value)
57
53
  }`,
58
- })
54
+ })
59
55
 
60
- break
61
- case 2:
62
- context.report({
63
- node,
64
- message: `currentTargetはイベント処理中以外に参照するとnullになる場合があります。イベントハンドラ用関数のスコープ直下でcurrentTarget、もしくはcurrentTarget以下の属性を含む値を変数として宣言してください
56
+ break
57
+ case 2:
58
+ context.report({
59
+ node,
60
+ message: `currentTargetはイベント処理中以外に参照するとnullになる場合があります。イベントハンドラ用関数のスコープ直下でcurrentTarget、もしくはcurrentTarget以下の属性を含む値を変数として宣言してください
65
61
  - 参考: https://developer.mozilla.org/ja/docs/Web/API/Event/currentTarget
66
62
  - React/useStateのsetterは第一引数に関数を渡すと非同期処理になるためこの問題が起きる可能性があります
67
63
  - イベントハンドラ内で関数を定義すると参照タイミングがずれる可能性があるため、イベントハンドラ直下のスコープ内にcurrentTarget関連の参照を変数に残すことをオススメします
@@ -74,12 +70,9 @@ module.exports = {
74
70
  const value = e.currentTarget.value
75
71
  setItem((current) => ({ ...current, value }))
76
72
  }`,
77
- })
78
-
79
- break
80
- }
81
- }
73
+ })
82
74
 
75
+ break
83
76
  }
84
77
  },
85
78
  }
@@ -1,12 +1,6 @@
1
- const { checkImportStyledComponents, getStyledComponentBaseName } = require('../../libs/format_styled_components')
2
-
3
1
  const ERRORMESSAGE_SUFFIX = `
4
2
  - button要素のtype属性のデフォルトは "submit" のため、button要素がformでラップされていると意図しないsubmitを引き起こす可能性があります
5
3
  - smarthr-ui/Button, smarthr-ui/UnstyledButtonのtype属性のデフォルトは "button" になっているため、buttonから置き換えることをおすすめします`
6
- const ERRORMESSAGE_REQUIRED_TYPE_ATTR = `button要素を利用する場合、type属性に "button" もしくは "submit" を指定してください${ERRORMESSAGE_SUFFIX}`
7
- const ERRORMESSAGE_PROHIBIT_STYLED = `"styled.button" の直接利用をやめ、smarthr-ui/Button、もしくはsmarthr-ui/UnstyledButtonを利用してください${ERRORMESSAGE_SUFFIX}`
8
-
9
- const findTypeAttr = (a) => a.type === 'JSXAttribute' && a.name.name === 'type'
10
4
 
11
5
  const SCHEMA = []
12
6
 
@@ -21,24 +15,17 @@ module.exports = {
21
15
  },
22
16
  create(context) {
23
17
  return {
24
- ImportDeclaration: (node) => {
25
- checkImportStyledComponents(node, context)
26
- },
27
- JSXOpeningElement: (node) => {
28
- if (node.name.name === 'button' && !node.attributes.find(findTypeAttr)) {
29
- context.report({
30
- node,
31
- message: ERRORMESSAGE_REQUIRED_TYPE_ATTR,
32
- });
33
- }
18
+ [`JSXOpeningElement[name.name="button"]:not(:has(JSXAttribute[name.name="type"]))`]: (node) => {
19
+ context.report({
20
+ node,
21
+ message: `button要素を利用する場合、type属性に "button" もしくは "submit" を指定してください${ERRORMESSAGE_SUFFIX}`,
22
+ });
34
23
  },
35
- VariableDeclarator: (node) => {
36
- if (getStyledComponentBaseName(node) === 'button') {
37
- context.report({
38
- node,
39
- message: ERRORMESSAGE_PROHIBIT_STYLED,
40
- });
41
- }
24
+ [`MemberExpression[object.name="styled"][property.name="button"]`]: (node) => {
25
+ context.report({
26
+ node,
27
+ message: `"styled.button" の直接利用をやめ、smarthr-ui/Button、もしくはsmarthr-ui/UnstyledButtonを利用してください${ERRORMESSAGE_SUFFIX}`,
28
+ });
42
29
  },
43
30
  }
44
31
  },
@@ -12,20 +12,17 @@ module.exports = {
12
12
  },
13
13
  create(context) {
14
14
  return {
15
- JSXAttribute: (node) => {
16
- const hit = node.name.name.match(PROHIBIT_ATTR_REGEX)
17
-
18
- if (hit) {
19
- context.report({
20
- node,
21
- message: `テストのために要素を指定するために、${hit[1]} 属性を利用するのではなく、他の方法で要素を指定することを検討してください。
22
- - 方法1: click_link, click_button等を利用したりすることで、利用しているテスト環境に準じた方法で要素を指定することを検討してください。
15
+ 'JSXAttribute[name.name=/^(data-(spec|testid))$/]': (node) => {
16
+ context.report({
17
+ node,
18
+ message: `テストしたい要素を指定するためにテスト用の属性は利用せず、他の方法を検討してください
19
+ - 方法1: click_link, click_button等を利用することで、テスト環境に準じた方法で要素を指定することを検討してください
23
20
  - 参考(Testing Library): https://testing-library.com/docs/queries/about
24
21
  - 参考(Capybara): https://rubydoc.info/github/jnicklas/capybara/Capybara/Node/Finders
25
- - 方法2: テスト環境のメソッド等で要素が指定できない場合はrole属性、name属性、id属性等を利用した方法で要素を指定することを検討してください。
26
- - 方法3: 上記の方法でも要素が指定できない場合は、'eslint-disable-next-line' 等を利用して、このルールを無効化してください。`,
27
- });
28
- }
22
+ - 方法2: テスト環境のメソッド等で要素が指定できない場合はrolename、aria系などユーザーが認識できる属性を利用した方法で要素を指定することを検討してください
23
+ - 画像の場合、alt属性が利用できます
24
+ - id, class属性は基本的にユーザーが認識出来ないため利用しないでください`,
25
+ });
29
26
  },
30
27
  }
31
28
  },
@@ -1,10 +1,3 @@
1
- const MESSAGE_NEW_DATE = `'new Date(arg)' のように引数を一つだけ指定したDate instanceの生成は実行環境によって結果が異なるため、以下のいずれかの方法に変更してください
2
- - 'new Date(2022, 12 - 1, 31)' のように数値を個別に指定する
3
- - dayjsなど、日付系ライブラリを利用する (例: 'dayjs(arg).toDate()')`
4
- const MESSAGE_PARSE = `Date.parse は実行環境によって結果が異なるため、以下のいずれかの方法に変更してください
5
- - 'new Date(2022, 12 - 1, 31).getTime()' のように数値を個別に指定する
6
- - dayjsなど、日付系ライブラリを利用する (例: 'dayjs(arg).valueOf()')`
7
-
8
1
  const SEPARATOR = '(\/|-)'
9
2
  const DATE_REGEX = new RegExp(`^([0-9]{4})${SEPARATOR}([0-9]{1,2})${SEPARATOR}([0-9]{1,2})`)
10
3
 
@@ -36,29 +29,23 @@ module.exports = {
36
29
  },
37
30
  create(context) {
38
31
  return {
39
- NewExpression: (node) => {
40
- if (
41
- node.callee.name === 'Date' &&
42
- node.arguments.length == 1
43
- ) {
44
- context.report({
45
- node,
46
- message: MESSAGE_NEW_DATE,
47
- fix: (fixer) => fixAction(fixer, node),
48
- });
49
- }
32
+ 'NewExpression[callee.name="Date"][arguments.length=1]': (node) => {
33
+ context.report({
34
+ node,
35
+ message: `'new Date(arg)' のように引数を一つだけ指定したDate instanceの生成は実行環境によって結果が異なるため、以下のいずれかの方法に変更してください
36
+ - 'new Date(2022, 12 - 1, 31)' のように数値を個別に指定する
37
+ - dayjsなど、日付系ライブラリを利用する (例: 'dayjs(arg).toDate()')`,
38
+ fix: (fixer) => fixAction(fixer, node),
39
+ });
50
40
  },
51
- CallExpression: (node) => {
52
- if (
53
- node.callee.object?.name === 'Date' &&
54
- node.callee.property?.name === 'parse'
55
- ) {
56
- context.report({
57
- node,
58
- message: MESSAGE_PARSE,
59
- fix: (fixer) => fixAction(fixer, node, '.getTime()'),
60
- });
61
- }
41
+ 'CallExpression[callee.object.name="Date"][callee.property.name="parse"]': (node) => {
42
+ context.report({
43
+ node,
44
+ message: `Date.parse は実行環境によって結果が異なるため、以下のいずれかの方法に変更してください
45
+ - 'new Date(2022, 12 - 1, 31).getTime()' のように数値を個別に指定する
46
+ - dayjsなど、日付系ライブラリを利用する (例: 'dayjs(arg).valueOf()')`,
47
+ fix: (fixer) => fixAction(fixer, node, '.getTime()'),
48
+ });
62
49
  },
63
50
  }
64
51
  },
@@ -1,7 +1,5 @@
1
1
  const SCHEMA = []
2
2
 
3
- const NOINDEX_ARRAY_REGEX = /\[\]\[/
4
-
5
3
  /**
6
4
  * @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
7
5
  */
@@ -11,24 +9,18 @@ module.exports = {
11
9
  schema: SCHEMA,
12
10
  },
13
11
  create(context) {
14
- const checker = (node, value) => {
15
- if (NOINDEX_ARRAY_REGEX.test(value)) {
16
- context.report({
17
- node,
18
- message: `入力要素のname属性に対して、配列に当たる部分の連番を指定しない場合(例: a[xxx][][yyy] )、配列内アイテムの属性が意図せず入れ替わってしまう場合がありえるため、常にindexを設定してください。
12
+ const checker = (node) => {
13
+ context.report({
14
+ node,
15
+ message: `入力要素のname属性に対して、配列に当たる部分の連番を指定しない場合(例: a[xxx][][yyy] )、配列内アイテムの属性が意図せず入れ替わってしまう場合がありえるため、常にindexを設定してください。
19
16
  - 例のyyyに当たる値が配列内の別アイテムに紐づいてしまう場合があります。
20
17
  - 詳しくは https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-nested-attributes-array-index を参照してください`,
21
- })
22
- }
18
+ })
23
19
  }
24
20
 
25
21
  return {
26
- Literal: (node) => {
27
- checker(node, node.value)
28
- },
29
- TemplateElement: (node) => {
30
- checker(node, node.value.cooked)
31
- },
22
+ 'Literal[value=/\\[\\]\\[/]': checker,
23
+ 'TemplateElement[value.cooked=/\\[\\]\\[/]': checker,
32
24
  }
33
25
  },
34
26
  }
@@ -1,6 +1,3 @@
1
- const REGEX_REMOTE_TRIGGER_DIALOG = /RemoteTrigger(Action|Form|Message|Modeless)Dialog$/
2
- const REGEX_REMOTE_DIALOG_TRIGGER = /RemoteDialogTrigger$/
3
-
4
1
  /**
5
2
  * @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
6
3
  */
@@ -10,27 +7,17 @@ module.exports = {
10
7
  schema: [],
11
8
  },
12
9
  create(context) {
13
- return {
14
- JSXOpeningElement: (node) => {
15
- const nodeName = node.name.name || '';
16
-
17
- const regexRemoteTriggerDialog = REGEX_REMOTE_TRIGGER_DIALOG.test(nodeName)
18
-
19
- if (regexRemoteTriggerDialog || REGEX_REMOTE_DIALOG_TRIGGER.test(nodeName)) {
20
- const attrName = regexRemoteTriggerDialog ? 'id' : 'targetId'
21
- const id = node.attributes.find((a) => a.name?.name === attrName)
10
+ const checker = (node) => {
11
+ context.report({
12
+ node,
13
+ message: `${node.parent.name.name}の${node.name.name}属性には直接文字列を指定してください。
14
+ - 変数などは利用できません(これは関連するTriggerとDialogを検索しやすくするためです)`,
15
+ })
16
+ }
22
17
 
23
- if (id && id.value.type !== 'Literal') {
24
- context.report({
25
- node: id,
26
- message: `${nodeName}の${attrName}属性には直接文字列を指定してください。
27
- - 変数などは利用できません(これは関連するTriggerとDialogを検索しやすくするためです)
28
- - RemoteTriggerActionDialogはループやDropdown内にTriggerが存在する場合に利用してください
29
- - ループやDropdown以外にTriggerが設定されている場合、TriggerAndActionDialogを利用してください`,
30
- })
31
- }
32
- }
33
- }
18
+ return {
19
+ 'JSXOpeningElement[name.name=/RemoteTrigger(Action|Form|Message|Modeless)Dialog$/] JSXAttribute[name.name="id"]:not([value.type="Literal"])': checker,
20
+ 'JSXOpeningElement[name.name=/RemoteDialogTrigger$/] JSXAttribute[name.name="targetId"]:not([value.type="Literal"])': checker,
34
21
  }
35
22
  },
36
23
  }
@@ -1,46 +1,4 @@
1
- const { AST_NODE_TYPES } = require('@typescript-eslint/utils')
2
-
3
1
  const SCHEMA = []
4
- const MARGIN_CLASS_PATTERNS = /shr-m[trbl]?-/ // mt-, mr-, mb-, ml-, m-
5
-
6
- const findClassNameAttr = (attr) => attr.type === AST_NODE_TYPES.JSXAttribute && attr.name.name === 'className'
7
-
8
- /**
9
- * コンポーネントのルート要素を渡し、該当の余白クラスが存在すればそれを、なければNULLを返す
10
- * @param {import('@typescript-eslint/utils').TSESTree.Node} node
11
- * @returns {import('@typescript-eslint/utils').TSESTree.Literal | null}
12
- */
13
- const findSpacingClassInRootElement = (node) => {
14
- // JSX でなければ対象外
15
- if (node.type !== AST_NODE_TYPES.JSXElement) return null
16
-
17
- const classNameAttr = node.openingElement.attributes.find(findClassNameAttr)
18
-
19
- if (
20
- classNameAttr &&
21
- // className属性の値がリテラル、かつ余白クラスの場合
22
- classNameAttr.value?.type === AST_NODE_TYPES.Literal && typeof classNameAttr.value.value === 'string' &&
23
- MARGIN_CLASS_PATTERNS.test(classNameAttr.value.value)
24
- ) {
25
- return classNameAttr.value
26
- }
27
-
28
- return null
29
- }
30
-
31
- /**
32
- * ブロックステートメント内から、JSX要素を返す ReturnStatement を返す
33
- * @param {import('@typescript-eslint/utils').TSESTree.BlockStatement} block
34
- * @returns {import('@typescript-eslint/utils').TSESTree.ReturnStatement | null}
35
- */
36
- const findJSXReturnStatement = (block) => {
37
- for (const statement of block.body) {
38
- if (statement.type === AST_NODE_TYPES.ReturnStatement && statement.argument?.type === AST_NODE_TYPES.JSXElement) {
39
- return statement
40
- }
41
- }
42
- return null
43
- }
44
2
 
45
3
  /**
46
4
  * @type {import('@typescript-eslint/utils').TSESLint.RuleModule}
@@ -49,61 +7,15 @@ module.exports = {
49
7
  meta: {
50
8
  type: 'problem',
51
9
  schema: SCHEMA,
52
- messages: {
53
- noRootSpacing:
54
- 'コンポーネントのルート要素に外側への余白(margin)を設定しないでください。外側の余白は使用する側で制御するべきです。',
55
- },
56
10
  },
57
11
  create(context) {
58
- /**
59
- * 関数本体をチェックし、ルート要素で余白クラスが設定されたJSXを返している場合、エラーを報告する
60
- * @param {import('@typescript-eslint/utils').TSESTree.Node} body
61
- */
62
- const checkFunctionBody = (n) => {
63
- const body = n.body
64
-
65
- switch (body.type) {
66
- // 関数がブロックを持たずに直接JSXを返すパターン
67
- case AST_NODE_TYPES.JSXElement: {
68
- const spacingClass = findSpacingClassInRootElement(body)
69
-
70
- if (spacingClass) {
71
- context.report({
72
- node: spacingClass,
73
- messageId: 'noRootSpacing',
74
- })
75
- }
76
-
77
- break
78
- }
79
- // 関数がブロック内で JSX を return するパターン
80
- case AST_NODE_TYPES.BlockStatement: {
81
- const returnStatement = findJSXReturnStatement(body)
82
- if (returnStatement?.argument) {
83
- const spacingClass = findSpacingClassInRootElement(returnStatement.argument)
84
- if (spacingClass) {
85
- context.report({
86
- node: spacingClass,
87
- messageId: 'noRootSpacing',
88
- })
89
- }
90
- }
91
-
92
- break
93
- }
94
- }
95
- }
96
-
97
12
  return {
98
- // アロー関数式のコンポーネントをチェック
99
- ArrowFunctionExpression: (node) => {
100
- if (node.parent?.type === AST_NODE_TYPES.VariableDeclarator) {
101
- checkFunctionBody(node)
102
- }
13
+ ':matches(ArrowFunctionExpression,FunctionDeclaration,ReturnStatement)>JSXElement>JSXOpeningElement JSXAttribute[name.name="className"][value.value=/( |^)shr-m[trbl]?-/]': (node) => {
14
+ context.report({
15
+ node,
16
+ message: 'コンポーネントのルート要素に外側への余白(margin)を設定しないでください。外側の余白は使用する側で制御するべきです。',
17
+ })
103
18
  },
104
-
105
- // function宣言のコンポーネントをチェック
106
- FunctionDeclaration: checkFunctionBody,
107
19
  }
108
20
  },
109
21
  }