eslint-plugin-smarthr 2.5.0 → 3.0.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.
- package/CHANGELOG.md +16 -0
- package/README.md +0 -1
- package/package.json +2 -2
- package/rules/require-i18n-text/index.js +26 -20
- package/test/require-i18n-text.js +16 -0
- package/rules/a11y-required-layout-as-attribute/README.md +0 -54
- package/rules/a11y-required-layout-as-attribute/index.js +0 -65
- package/test/a11y-required-layout-as-attribute.js +0 -34
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
|
+
## [3.0.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v2.5.0...eslint-plugin-smarthr-v3.0.0) (2025-12-08)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* a11y-required-layout-as-attributeを削除する ([#919](https://github.com/kufu/tamatebako/issues/919))
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* a11y-required-layout-as-attributeを削除する ([#919](https://github.com/kufu/tamatebako/issues/919)) ([fbf6897](https://github.com/kufu/tamatebako/commit/fbf68978c6b2590cf031cdea6badb9edf7faab96))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* require-i18n-textで特定の文字列の場合、修正対象にしないように調整 ([#924](https://github.com/kufu/tamatebako/issues/924)) ([ddfb24b](https://github.com/kufu/tamatebako/commit/ddfb24b82a784312eb4750e87fbcd7c1374d6fcf))
|
|
20
|
+
|
|
5
21
|
## [2.5.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v2.4.0...eslint-plugin-smarthr-v2.5.0) (2025-12-04)
|
|
6
22
|
|
|
7
23
|
|
package/README.md
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
- [a11y-prohibit-input-placeholder](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-prohibit-input-placeholder)
|
|
15
15
|
- [a11y-prohibit-sectioning-content-in-form](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-prohibit-sectioning-content-in-form)
|
|
16
16
|
- [a11y-prohibit-useless-sectioning-fragment](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-prohibit-useless-sectioning-fragment)
|
|
17
|
-
- [a11y-required-layout-as-attribute](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-required-layout-as-attribute)
|
|
18
17
|
- [a11y-trigger-has-button](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-trigger-has-button)
|
|
19
18
|
- [best-practice-for-async-current-target](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-async-current-target)
|
|
20
19
|
- [best-practice-for-button-element](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-button-element)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-smarthr",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"author": "SmartHR",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A sharable ESLint plugin for SmartHR",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"eslintplugin",
|
|
38
38
|
"smarthr"
|
|
39
39
|
],
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "81723cdb62ca80730409d872043a72f7844abc46"
|
|
41
41
|
}
|
|
@@ -35,6 +35,9 @@ const STRING_LITERAL_CONDITION =
|
|
|
35
35
|
const generateAttributeSelector = (attributes) =>
|
|
36
36
|
`JSXAttribute[name.name=/^(${attributes.join('|')})$/]${STRING_LITERAL_CONDITION}`
|
|
37
37
|
|
|
38
|
+
const REGEX_IGNORE_TEXT = /^ *(\.|\+|\-|\*|\/|[0-9]+) *$/
|
|
39
|
+
const checkIgnoreText = (text) => !REGEX_IGNORE_TEXT.test(text)
|
|
40
|
+
|
|
38
41
|
/**
|
|
39
42
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
40
43
|
*/
|
|
@@ -44,49 +47,52 @@ module.exports = {
|
|
|
44
47
|
schema: SCHEMA,
|
|
45
48
|
},
|
|
46
49
|
create(context) {
|
|
47
|
-
const
|
|
48
|
-
const elementsObj = options.elements || {}
|
|
49
|
-
|
|
50
|
+
const elementsObj = (context.options[0] || {}).elements || {}
|
|
50
51
|
// ユーザーが'*'を設定していない場合のみデフォルトを適用
|
|
51
52
|
const wildcardAttributes = elementsObj['*'] || DEFAULT_WILDCARD_ATTRIBUTES
|
|
52
53
|
const specificElements = Object.keys(elementsObj).filter((k) => k !== '*')
|
|
53
54
|
const handlers = {}
|
|
54
55
|
|
|
55
56
|
const reportAttributeError = (node) => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
if (checkIgnoreText(node.value.value)) {
|
|
58
|
+
context.report({
|
|
59
|
+
node,
|
|
60
|
+
message: `${node.parent.name.name}の${node.name.name}属性に文字列リテラルが指定されています。多言語化対応のため、翻訳関数を使用してください
|
|
59
61
|
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-i18n-text`,
|
|
60
|
-
|
|
62
|
+
})
|
|
63
|
+
}
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
// 個別要素の設定
|
|
64
67
|
for (const elementName of specificElements) {
|
|
65
68
|
const attributes = elementsObj[elementName]
|
|
66
|
-
if (attributes.length === 0) continue
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
if (attributes.length !== 0) {
|
|
71
|
+
handlers[`JSXOpeningElement[name.name="${elementName}"] > ${generateAttributeSelector(attributes)}`] = reportAttributeError
|
|
72
|
+
}
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
// ワイルドカード設定
|
|
72
76
|
if (wildcardAttributes && wildcardAttributes.length > 0) {
|
|
73
77
|
const attributeSelector = generateAttributeSelector(wildcardAttributes)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
|
|
79
|
+
handlers[
|
|
80
|
+
specificElements.length > 0
|
|
81
|
+
// 個別設定要素を除外
|
|
82
|
+
? `JSXOpeningElement:not([name.name=/^(${specificElements.join('|')})$/]) > ${attributeSelector}`
|
|
83
|
+
: attributeSelector
|
|
84
|
+
] = reportAttributeError
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
// 子要素の文字列リテラルチェック(空白のみのテキストは除外)
|
|
84
88
|
handlers['JSXText[value=/\\S/]'] = (node) => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
if (checkIgnoreText(node.value)) {
|
|
90
|
+
context.report({
|
|
91
|
+
node,
|
|
92
|
+
message: `子要素に文字列リテラルが指定されています。多言語化対応のため、翻訳関数を使用してください
|
|
88
93
|
- 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-i18n-text`,
|
|
89
|
-
|
|
94
|
+
})
|
|
95
|
+
}
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
return handlers
|
|
@@ -54,6 +54,14 @@ ruleTester.run('require-i18n-text', rule, {
|
|
|
54
54
|
{ code: `<img src="image.png" />` },
|
|
55
55
|
{ code: `<div data-testid="test" />` },
|
|
56
56
|
|
|
57
|
+
// 数値のみ、.と演算記号のみの場合は許容
|
|
58
|
+
{ code: `<Any aria-label="1234" />` },
|
|
59
|
+
{ code: `<div>.</div>` },
|
|
60
|
+
{ code: `<a> +</a>` },
|
|
61
|
+
{ code: `<img alt="-" />` },
|
|
62
|
+
{ code: `<i>*</i>` },
|
|
63
|
+
{ code: `<i>/</i>` },
|
|
64
|
+
|
|
57
65
|
// ワイルドカード - 空配列で除外
|
|
58
66
|
{
|
|
59
67
|
code: `<Icon label="Icon text" />`,
|
|
@@ -98,6 +106,14 @@ ruleTester.run('require-i18n-text', rule, {
|
|
|
98
106
|
errors: [{ message: attributeError('button', 'title') }],
|
|
99
107
|
},
|
|
100
108
|
|
|
109
|
+
// 数値、.と演算記号の場合でも他の文字列が含まれていればエラー
|
|
110
|
+
{ code: `<Any aria-label="1234 あ" />`, errors: [{ message: attributeError('Any', 'aria-label') }] },
|
|
111
|
+
{ code: `<div>a.</div>`, errors: [{ message: childTextError }] },
|
|
112
|
+
{ code: `<a> + b</a>`, errors: [{ message: childTextError }] },
|
|
113
|
+
{ code: `<img alt="-zod" />`, errors: [{ message: attributeError('img', 'alt') }] },
|
|
114
|
+
{ code: `<i>*1</i>`, errors: [{ message: childTextError }] },
|
|
115
|
+
{ code: `<i>a/</i>`, errors: [{ message: childTextError }] },
|
|
116
|
+
|
|
101
117
|
// 属性エラー: カスタムオプション
|
|
102
118
|
{
|
|
103
119
|
code: `<img alt="Profile picture" />`,
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# smarthr/a11y-required-layout-as-attribute
|
|
2
|
-
|
|
3
|
-
- smarthr-ui/Layoutに属するコンポーネントはデフォルトではdiv要素を出力します
|
|
4
|
-
- そのため他の一部コンポーネントとの組み合わせによってはinvalidなマークアップになる場合が起こり得ます
|
|
5
|
-
- 例: FormControlのtitle属性内でClusterを使うと `label > div` の構造になるためinvalid
|
|
6
|
-
- 対象コンポーネントの使用方法をチェックし、適切なマークアップになるよう、as・forwardedAs属性の利用を促します
|
|
7
|
-
|
|
8
|
-
## rules
|
|
9
|
-
|
|
10
|
-
```js
|
|
11
|
-
{
|
|
12
|
-
rules: {
|
|
13
|
-
'smarthr/a11y-required-layout-as-attribute': [
|
|
14
|
-
'error', // 'warn', 'off'
|
|
15
|
-
]
|
|
16
|
-
},
|
|
17
|
-
}
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## ❌ Incorrect
|
|
21
|
-
|
|
22
|
-
```jsx
|
|
23
|
-
<Heading>
|
|
24
|
-
<Cluster>any</Cluster>
|
|
25
|
-
</Heading>
|
|
26
|
-
|
|
27
|
-
<HogeFormControl title={
|
|
28
|
-
<FugaCluster>any</FugaCluster>
|
|
29
|
-
} />
|
|
30
|
-
|
|
31
|
-
<StyledFieldset title={
|
|
32
|
-
<Cluster>any</Cluster>
|
|
33
|
-
}>
|
|
34
|
-
// any
|
|
35
|
-
</StyledFieldset>
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## ✅ Correct
|
|
39
|
-
|
|
40
|
-
```jsx
|
|
41
|
-
<Heading>
|
|
42
|
-
<Cluster as="span">any</Cluster>
|
|
43
|
-
</Heading>
|
|
44
|
-
|
|
45
|
-
<HogeFormControl title={
|
|
46
|
-
<FugaCluster forwardedAs="span">any</FugaCluster>
|
|
47
|
-
} />
|
|
48
|
-
|
|
49
|
-
<StyledFieldset title={
|
|
50
|
-
<Cluster as="strong">any</Cluster>
|
|
51
|
-
}>
|
|
52
|
-
// any
|
|
53
|
-
</StyledFieldset>
|
|
54
|
-
```
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
const layoutRegex = /((C(ent|lust)er)|Reel|Sidebar|Stack|Base(Column)?)$/
|
|
2
|
-
const headingRegex = /((^h(1|2|3|4|5|6))|Heading)$/
|
|
3
|
-
const asRegex = /^(as|forwardedAs)$/
|
|
4
|
-
const formControlRegex = /(FormControl|Fieldset)$/
|
|
5
|
-
|
|
6
|
-
const findAsAttr = (a) => a.name?.name.match(asRegex)
|
|
7
|
-
|
|
8
|
-
const searchBubbleUp = (node) => {
|
|
9
|
-
switch (node.type) {
|
|
10
|
-
case 'Program':
|
|
11
|
-
// rootまで検索した場合は確定でエラーにする
|
|
12
|
-
return null
|
|
13
|
-
case 'JSXElement': {
|
|
14
|
-
const name = node.openingElement.name.name || ''
|
|
15
|
-
|
|
16
|
-
if (headingRegex.test(name)) {
|
|
17
|
-
return name
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
break
|
|
21
|
-
}
|
|
22
|
-
case 'JSXAttribute': {
|
|
23
|
-
const name = node.name.name || ''
|
|
24
|
-
|
|
25
|
-
if (name === 'title' && formControlRegex.test(node.parent.name.name)) {
|
|
26
|
-
return `${node.parent.name.name}のtitle属性`
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return searchBubbleUp(node.parent)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
36
|
-
*/
|
|
37
|
-
module.exports = {
|
|
38
|
-
meta: {
|
|
39
|
-
type: 'problem',
|
|
40
|
-
schema: [],
|
|
41
|
-
},
|
|
42
|
-
create(context) {
|
|
43
|
-
return {
|
|
44
|
-
JSXOpeningElement: (node) => {
|
|
45
|
-
const name = node.name.name || ''
|
|
46
|
-
|
|
47
|
-
if (layoutRegex.test(name) && !node.attributes.some(findAsAttr)) {
|
|
48
|
-
const parentName = searchBubbleUp(node.parent.parent)
|
|
49
|
-
|
|
50
|
-
if (parentName) {
|
|
51
|
-
context.report({
|
|
52
|
-
node,
|
|
53
|
-
message: `${name}は${parentName}内に存在するため、as、もしくはforwardedAs属性を指定し、div以外の要素にする必要があります
|
|
54
|
-
- smarthr-ui/Layoutに属するコンポーネントはデフォルトでdiv要素を出力するため${parentName}内で利用すると、マークアップの仕様に違反します
|
|
55
|
-
- ほぼすべての場合、spanを指定することで適切なマークアップに変更出来ます
|
|
56
|
-
- span以外を指定したい場合、記述コンテンツに属する要素かどうかを確認してください (https://developer.mozilla.org/ja/docs/Web/HTML/Content_categories#%E8%A8%98%E8%BF%B0%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84)`,
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
module.exports.schema = []
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
const rule = require('../rules/a11y-required-layout-as-attribute')
|
|
2
|
-
const RuleTester = require('eslint').RuleTester
|
|
3
|
-
|
|
4
|
-
const ruleTester = new RuleTester({
|
|
5
|
-
languageOptions: {
|
|
6
|
-
parserOptions: {
|
|
7
|
-
ecmaFeatures: {
|
|
8
|
-
jsx: true,
|
|
9
|
-
},
|
|
10
|
-
},
|
|
11
|
-
},
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
const generateErrorText = (parentName, name) => `${name}は${parentName}内に存在するため、as、もしくはforwardedAs属性を指定し、div以外の要素にする必要があります
|
|
15
|
-
- smarthr-ui/Layoutに属するコンポーネントはデフォルトでdiv要素を出力するため${parentName}内で利用すると、マークアップの仕様に違反します
|
|
16
|
-
- ほぼすべての場合、spanを指定することで適切なマークアップに変更出来ます
|
|
17
|
-
- span以外を指定したい場合、記述コンテンツに属する要素かどうかを確認してください (https://developer.mozilla.org/ja/docs/Web/HTML/Content_categories#%E8%A8%98%E8%BF%B0%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84)`
|
|
18
|
-
|
|
19
|
-
ruleTester.run('a11y-anchor-has-href-attribute', rule, {
|
|
20
|
-
valid: [
|
|
21
|
-
{ code: `<h1><Cluster as="span">ほげ</Cluster></h1>` },
|
|
22
|
-
{ code: `<Heading><Cluster as="strong" /></Heading>` },
|
|
23
|
-
{ code: `<StyledHeading><AnyCluster forwardedAs="span" /></StyledHeading>` },
|
|
24
|
-
{ code: `<FormControl title={<Cluster as="span" />} />` },
|
|
25
|
-
{ code: `<StyledFieldset title={<AnyCluster forwardedAs="strong" />} />` },
|
|
26
|
-
],
|
|
27
|
-
invalid: [
|
|
28
|
-
{ code: `<h1><Cluster>ほげ</Cluster></h1>`, errors: [{ message: generateErrorText('h1', 'Cluster') }] },
|
|
29
|
-
{ code: `<Heading><Cluster /></Heading>`, errors: [{ message: generateErrorText('Heading', 'Cluster') }] },
|
|
30
|
-
{ code: `<StyledHeading><AnyCluster /></StyledHeading>`, errors: [{ message: generateErrorText('StyledHeading', 'AnyCluster') }] },
|
|
31
|
-
{ code: `<FormControl title={<Cluster />} />`, errors: [{ message: generateErrorText('FormControlのtitle属性', 'Cluster') }] },
|
|
32
|
-
{ code: `<StyledFieldset title={<AnyCluster />} />`, errors: [{ message: generateErrorText('StyledFieldsetのtitle属性', 'AnyCluster') }] },
|
|
33
|
-
]
|
|
34
|
-
})
|