eslint-plugin-smarthr 0.3.9 → 0.3.10

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 CHANGED
@@ -2,6 +2,15 @@
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
+ ### [0.3.10](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.9...v0.3.10) (2023-09-20)
6
+
7
+
8
+ ### Features
9
+
10
+ * a11y-heading-in-sectioning-contentでSectioningContentと予想される名前だがそれらの拡張ではないコンポーネントはエラーとする ([#77](https://github.com/kufu/eslint-plugin-smarthr/issues/77)) ([f7248d5](https://github.com/kufu/eslint-plugin-smarthr/commit/f7248d597cb06ba0bab1d3f0d51956efefd04aac))
11
+ * a11y-xxx-has-yyy-attributeにcheckTypeオプションを追加 ([#81](https://github.com/kufu/eslint-plugin-smarthr/issues/81)) ([94a511a](https://github.com/kufu/eslint-plugin-smarthr/commit/94a511a412e62431282eb1980941862313dfb777))
12
+ * a11y系ruleにstyled-componentsで既存のコンポーネントなどを拡張する際、誤検知が発生しそうな名称が設定されている場合はエラーにする機能を追加 ([#80](https://github.com/kufu/eslint-plugin-smarthr/issues/80)) ([727ff3f](https://github.com/kufu/eslint-plugin-smarthr/commit/727ff3fc6116fca017f8c3a3e62af569b76863da))
13
+
5
14
  ### [0.3.9](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.3.8...v0.3.9) (2023-09-04)
6
15
 
7
16
 
@@ -3,8 +3,13 @@ const STYLED_COMPONENTS = `${STYLED_COMPONENTS_METHOD}-components`
3
3
 
4
4
  const findInvalidImportNameNode = (s) => s.type === 'ImportDefaultSpecifier' && s.local.name !== STYLED_COMPONENTS_METHOD
5
5
 
6
- const generateTagFormatter = ({ context, EXPECTED_NAMES }) => {
6
+ const generateTagFormatter = ({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }) => {
7
7
  const entriesesTagNames = Object.entries(EXPECTED_NAMES).map(([b, e]) => [ new RegExp(b), new RegExp(e) ])
8
+ const entriesesUnTagNames = UNEXPECTED_NAMES ? Object.entries(UNEXPECTED_NAMES).map(([b, e]) => {
9
+ const [ auctualE, messageTemplate ] = Array.isArray(e) ? e : [e, '']
10
+
11
+ return [ new RegExp(b), new RegExp(auctualE), messageTemplate ]
12
+ }) : []
8
13
 
9
14
  return {
10
15
  ImportDeclaration: (node) => {
@@ -63,6 +68,28 @@ const generateTagFormatter = ({ context, EXPECTED_NAMES }) => {
63
68
  });
64
69
  }
65
70
  })
71
+
72
+ entriesesUnTagNames.forEach(([b, e, m]) => {
73
+ const matcher = extended.match(e)
74
+
75
+ if (matcher && !base.match(b)) {
76
+ const expected = matcher[1]
77
+ const isBareTag = base === base.toLowerCase()
78
+ const sampleFixBase = `styled${isBareTag ? `.${base}` : `(${base})`}`
79
+
80
+ context.report({
81
+ node,
82
+ message: m ? m
83
+ .replaceAll('{{extended}}', extended)
84
+ .replaceAll('{{expected}}', expected) : `${extended} は ${b.toString()} にmatchする名前のコンポーネントを拡張することを期待している名称になっています
85
+ - ${extended} の名称の末尾が"${expected}" という文字列ではない状態にしつつ、"${base}"を継承していることをわかる名称に変更してください
86
+ - もしくは"${base}"を"${extended}"の継承元であることがわかるような${isBareTag ? '適切なタグや別コンポーネントに差し替えてください' : '名称に変更するか、適切な別コンポーネントに差し替えてください'}
87
+ - 修正例1: const ${extended.replace(expected, '')}Xxxx = ${sampleFixBase}
88
+ - 修正例2: const ${extended}Xxxx = ${sampleFixBase}
89
+ - 修正例3: const ${extended} = styled(Xxxx${expected})`
90
+ })
91
+ }
92
+ })
66
93
  }
67
94
  },
68
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -6,13 +6,17 @@
6
6
  - URL遷移を行う場合、hrefが設定されていないとキーボード操作やコンテキストメニューからの遷移ができなくなります
7
7
  - これらの操作は href属性を参照します
8
8
  - 無効化されたリンクであることを表したい場合 `href={undefined}` を設定してください
9
+ - checkTypeオプションに 'smart' を指定することで spread attributeが設定されている場合はcorrectに出来ます。
9
10
 
10
11
  ## rules
11
12
 
12
13
  ```js
13
14
  {
14
15
  rules: {
15
- 'smarthr/a11y-anchor-has-href-attribute': 'error', // 'warn', 'off'
16
+ 'smarthr/a11y-anchor-has-href-attribute': [
17
+ 'error', // 'warn', 'off'
18
+ // { checkType: 'always' } /* 'always' || 'smart' */
19
+ ]
16
20
  },
17
21
  }
18
22
  ```
@@ -24,6 +28,10 @@
24
28
  <XxxAnchor>any</XxxAnchor>
25
29
  <XxxLink>any</XxxLink>
26
30
  <XxxLink href>any</XxxLink>
31
+
32
+ // checkType: 'always'
33
+ <XxxAnchor {...args} />
34
+ <XxxLink {...args} any="any" />
27
35
  ```
28
36
 
29
37
  ## ✅ Correct
@@ -38,4 +46,8 @@
38
46
 
39
47
  // react-router-domを利用している場合
40
48
  <Link to={hoge}>any</Link>
49
+
50
+ // checkType: 'smart'
51
+ <XxxAnchor {...args} />
52
+ <XxxLink {...args} any="any" />
41
53
  ```
@@ -28,22 +28,35 @@ const EXPECTED_NAMES = {
28
28
  '^a$': '(Anchor|Link)$',
29
29
  }
30
30
 
31
+ const UNEXPECTED_NAMES = {
32
+ '(Anchor|^a)$': '(Anchor)$',
33
+ '(Link|^a)$': '(Link)$',
34
+ }
35
+
31
36
  const REGEX_TARGET = /(Anchor|Link|^a)$/
32
- const check = (node) => {
33
- const result = baseCheck(node)
37
+ const check = (node, checkType) => {
38
+ const result = baseCheck(node, checkType)
34
39
 
35
- return result && ((OPTION.nextjs && !nextCheck(node)) || (OPTION.react_router && !reactRouterCheck(node))) ? null : result
40
+ return result && ((OPTION.nextjs && !nextCheck(node, checkType)) || (OPTION.react_router && !reactRouterCheck(node))) ? null : result
36
41
  }
37
- const baseCheck = (node) => {
42
+ const baseCheck = (node, checkType) => {
38
43
  const nodeName = node.name.name || ''
39
44
 
40
- return nodeName.match(REGEX_TARGET) && checkExistAttribute(node, findHrefAttribute) ? nodeName : false
45
+ if (
46
+ nodeName.match(REGEX_TARGET) &&
47
+ checkExistAttribute(node, findHrefAttribute) &&
48
+ (checkType !== 'smart' || !node.attributes.some(findSpreadAttr))
49
+ ) {
50
+ return nodeName
51
+ }
52
+
53
+ return false
41
54
  }
42
- const nextCheck = (node) => {
55
+ const nextCheck = (node, checkType) => {
43
56
  // HINT: next/link で `Link>a` という構造がありえるので直上のJSXElementを調べる
44
57
  const target = node.parent.parent.openingElement
45
58
 
46
- return target ? baseCheck(target) : false
59
+ return target ? baseCheck(target, checkType) : false
47
60
  }
48
61
  const reactRouterCheck = (node) => checkExistAttribute(node, findToAttribute)
49
62
 
@@ -57,6 +70,7 @@ const checkExistAttribute = (node, find) => {
57
70
  )
58
71
  }
59
72
  const isNullTextHref = (attr) => attr.type === 'Literal' && (attr.value === '' || attr.value === '#')
73
+ const findSpreadAttr = (a) => a.type === 'JSXSpreadAttribute'
60
74
 
61
75
  const findHrefAttribute = (a) => a.name?.name == 'href'
62
76
  const findToAttribute = (a) => a.name?.name == 'to'
@@ -69,7 +83,15 @@ const MESSAGE_SUFFIX = ` に href 属性を正しく設定してください
69
83
  - リンクが存在せず無効化されていることを表したい場合、href属性に undefined を設定してください
70
84
  - button要素のdisabled属性が設定された場合に相当します`
71
85
 
72
- const SCHEMA = []
86
+ const SCHEMA = [
87
+ {
88
+ type: 'object',
89
+ properties: {
90
+ checkType: { type: 'string', enum: ['always', 'smart'], default: 'always' },
91
+ },
92
+ additionalProperties: false,
93
+ }
94
+ ]
73
95
 
74
96
  module.exports = {
75
97
  meta: {
@@ -77,10 +99,13 @@ module.exports = {
77
99
  schema: SCHEMA,
78
100
  },
79
101
  create(context) {
102
+ const option = context.options[0] || {}
103
+ const checkType = option.checkType || 'always'
104
+
80
105
  return {
81
- ...generateTagFormatter({ context, EXPECTED_NAMES }),
106
+ ...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
82
107
  JSXOpeningElement: (node) => {
83
- const nodeName = check(node)
108
+ const nodeName = check(node, checkType)
84
109
 
85
110
  if (nodeName) {
86
111
  context.report({
@@ -20,6 +20,12 @@ const EXPECTED_NAMES = {
20
20
  '^a$': '(Anchor|Link)$',
21
21
  }
22
22
 
23
+ const UNEXPECTED_NAMES = {
24
+ '(B|^b)utton$': '(Button)$',
25
+ '(Anchor|^a)$': '(Anchor)$',
26
+ '(Link|^a)$': '(Link)$',
27
+ }
28
+
23
29
  const REGEX_NLSP = /^\s*\n+\s*$/
24
30
  const REGEX_CLICKABLE_ELEMENT = /^(a|(.*?)Anchor(Button)?|(.*?)Link|(b|B)utton)$/
25
31
  const REGEX_SMARTHR_LOGO = /SmartHRLogo$/
@@ -47,7 +53,7 @@ module.exports = {
47
53
  const componentsWithText = option.componentsWithText || []
48
54
 
49
55
  return {
50
- ...generateTagFormatter({ context, EXPECTED_NAMES }),
56
+ ...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
51
57
  JSXElement: (parentNode) => {
52
58
  // HINT: 閉じタグが存在しない === テキストノードが存在しない
53
59
  if (!parentNode.closingElement) {
@@ -12,6 +12,30 @@ const EXPECTED_NAMES = {
12
12
  'ModelessDialog$': 'ModelessDialog$',
13
13
  }
14
14
 
15
+ const unexpectedMessageTemplate = `{{extended}} は smarthr-ui/{{expected}} をextendすることを期待する名称になっています
16
+ - childrenにHeadingを含まない場合、コンポーネントの名称から"{{expected}}"を取り除いてください
17
+ - childrenにHeadingを含み、アウトラインの範囲を指定するためのコンポーネントならば、smarthr-ui/{{expected}}をexendしてください
18
+ - "styled(Xxxx)" 形式の場合、拡張元であるXxxxコンポーネントの名称の末尾に"{{expected}}"を設定し、そのコンポーネント内でsmarthr-ui/{{expected}}を利用してください`
19
+ const UNEXPECTED_NAMES = {
20
+ '(Heading|^h(1|2|3|4|5|6))$': '(Heading)$',
21
+ '(A|^a)rticle$': [
22
+ '(Article)$',
23
+ unexpectedMessageTemplate,
24
+ ],
25
+ '(A|^a)side$': [
26
+ '(Aside)$',
27
+ unexpectedMessageTemplate,
28
+ ],
29
+ '(N|^n)av$': [
30
+ '(Nav)$',
31
+ unexpectedMessageTemplate,
32
+ ],
33
+ '(S|^s)ection$': [
34
+ '(Section)$',
35
+ unexpectedMessageTemplate,
36
+ ],
37
+ }
38
+
15
39
  const headingRegex = /((^h(1|2|3|4|5|6))|Heading)$/
16
40
  const declaratorHeadingRegex = /Heading$/
17
41
  const sectioningRegex = /((A(rticle|side))|Nav|Section|^SectioningFragment)$/
@@ -28,6 +52,24 @@ const rootHeadingMessage = `${headingMessage}
28
52
  const pageHeadingMessage = 'smarthr-ui/PageHeading が同一ファイル内に複数存在しています。PageHeadingはh1タグを出力するため最も重要な見出しにのみ利用してください。'
29
53
  const pageHeadingInSectionMessage = 'smarthr-ui/PageHeadingはsmarthr-uiのArticle, Aside, Nav, Sectionで囲まないでください。囲んでしまうとページ全体の見出しではなくなってしまいます。'
30
54
 
55
+ const VariableDeclaratorBareToSHR = (context, node) => {
56
+ if (!node.init) {
57
+ return
58
+ }
59
+
60
+ const tag = node.init.tag || node.init
61
+
62
+ if (tag.object?.name === 'styled') {
63
+ const message = reportMessageBareToSHR(tag.property.name, true)
64
+
65
+ if (message) {
66
+ context.report({
67
+ node,
68
+ message,
69
+ });
70
+ }
71
+ }
72
+ }
31
73
  const reportMessageBareToSHR = (tagName, visibleExample) => {
32
74
  const matcher = tagName.match(bareTagRegex)
33
75
 
@@ -67,26 +109,11 @@ module.exports = {
67
109
  create(context) {
68
110
  let h1s = []
69
111
  let sections = []
70
- let { VariableDeclarator, ...formatter } = generateTagFormatter({ context, EXPECTED_NAMES })
112
+ let { VariableDeclarator, ...formatter } = generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES, unexpectedMessageTemplate })
71
113
 
72
114
  formatter.VariableDeclarator = (node) => {
73
115
  VariableDeclarator(node)
74
- if (!node.init) {
75
- return
76
- }
77
-
78
- const tag = node.init.tag || node.init
79
-
80
- if (tag.object?.name === 'styled') {
81
- const message = reportMessageBareToSHR(tag.property.name, true)
82
-
83
- if (message) {
84
- context.report({
85
- node,
86
- message,
87
- });
88
- }
89
- }
116
+ VariableDeclaratorBareToSHR(context, node)
90
117
  }
91
118
 
92
119
  return {
@@ -1,13 +1,17 @@
1
1
  # smarthr/a11y-image-has-alt-attribute
2
2
 
3
3
  - 画像やアイコンにalt属性を設定することを強制するルールです
4
+ - checkTypeオプションに 'smart' を指定することで spread attributeが設定されている場合はcorrectに出来ます。
4
5
 
5
6
  ## rules
6
7
 
7
8
  ```js
8
9
  {
9
10
  rules: {
10
- 'smarthr/a11y-image-has-alt-attribute': 'error', // 'warn', 'off'
11
+ 'smarthr/a11y-image-has-alt-attribute': [
12
+ 'error', // 'warn', 'off'
13
+ // { checkType: 'always' } /* 'always' || 'smart' */
14
+ ]
11
15
  },
12
16
  }
13
17
  ```
@@ -26,6 +30,11 @@
26
30
  <Icon />
27
31
  ```
28
32
  ```jsx
33
+ // checkType: 'always'
34
+ <XxxImage {...args} />
35
+ <YyyIcon {...args} any="any" />
36
+ ```
37
+ ```jsx
29
38
  import styled from 'styled-components'
30
39
 
31
40
  const StyledHoge = styled.img``
@@ -47,6 +56,11 @@ const StyledPiyo = styled(Icon)``
47
56
  <Icon alt="message" />
48
57
  ```
49
58
  ```jsx
59
+ // checkType: 'smart'
60
+ <XxxImage {...args} />
61
+ <YyyIcon {...args} any="any" />
62
+ ```
63
+ ```jsx
50
64
  import styled from 'styled-components'
51
65
 
52
66
  const StyledImage = styled.img``
@@ -7,9 +7,16 @@ const EXPECTED_NAMES = {
7
7
  '^(img|svg)$': '(Img|Image|Icon)$',
8
8
  }
9
9
 
10
+ const UNEXPECTED_NAMES = {
11
+ '(Img|^(img|svg))$': '(Img)$',
12
+ '(Image|^(img|svg))$': '(Image)$',
13
+ '(Icon|^(img|svg))$': '(Icon)$',
14
+ }
15
+
10
16
  const REGEX_IMG = /(img|image)$/i // HINT: Iconは別途テキストが存在する場合が多いためチェックの対象外とする
11
17
 
12
18
  const findAltAttr = (a) => a.name?.name === 'alt'
19
+ const findSpreadAttr = (a) => a.type === 'JSXSpreadAttribute'
13
20
  const isWithinSvgJsxElement = (node) => {
14
21
  if (
15
22
  node.type === 'JSXElement' &&
@@ -28,14 +35,27 @@ const MESSAGE_NOT_EXIST_ALT = `画像にはalt属性を指定してください
28
35
  const MESSAGE_NULL_ALT = `画像の情報をテキストにした代替テキスト('alt')を設定してください。
29
36
  - 装飾目的の画像など、alt属性に指定すべき文字がない場合は背景画像にすることを検討してください。`
30
37
 
38
+ const SCHEMA = [
39
+ {
40
+ type: 'object',
41
+ properties: {
42
+ checkType: { type: 'string', enum: ['always', 'smart'], default: 'always' },
43
+ },
44
+ additionalProperties: false,
45
+ }
46
+ ]
47
+
31
48
  module.exports = {
32
49
  meta: {
33
50
  type: 'problem',
34
- schema: [],
51
+ schema: SCHEMA,
35
52
  },
36
53
  create(context) {
54
+ const option = context.options[0] || {}
55
+ const checkType = option.checkType || 'always'
56
+
37
57
  return {
38
- ...generateTagFormatter({ context, EXPECTED_NAMES }),
58
+ ...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
39
59
  JSXOpeningElement: (node) => {
40
60
  if (node.name.name) {
41
61
  const matcher = node.name.name.match(REGEX_IMG)
@@ -46,7 +66,16 @@ module.exports = {
46
66
  let message = ''
47
67
 
48
68
  if (!alt) {
49
- if (matcher.input !== 'image' || !isWithinSvgJsxElement(node.parent)) {
69
+ if (
70
+ (
71
+ matcher.input !== 'image' ||
72
+ !isWithinSvgJsxElement(node.parent)
73
+ ) &&
74
+ (
75
+ checkType !== 'smart' ||
76
+ !node.attributes.some(findSpreadAttr)
77
+ )
78
+ ) {
50
79
  message = MESSAGE_NOT_EXIST_ALT
51
80
  }
52
81
  } else if (alt.value.value === '') {
@@ -65,4 +94,4 @@ module.exports = {
65
94
  }
66
95
  },
67
96
  }
68
- module.exports.schema = []
97
+ module.exports.schema = SCHEMA
@@ -4,13 +4,17 @@
4
4
  - input は name を設定することでブラウザの補完機能が有効になる可能性が高まります。
5
5
  - 補完機能はブラウザによって異なるため、補完される可能性が上がるよう、name には半角英数の小文字・大文字と一部記号(`_ , [, ]`)のみ利用可能です。
6
6
  - input[type="radio"] は name を適切に設定することでラジオグループが確立され、キーボード操作しやすくなる等のメリットがあります。
7
+ - checkTypeオプションに 'smart' を指定することで spread attributeが設定されている場合はcorrectに出来ます。
7
8
 
8
9
  ## rules
9
10
 
10
11
  ```js
11
12
  {
12
13
  rules: {
13
- 'smarthr/a11y-input-has-name-attribute': 'error', // 'warn', 'off'
14
+ 'smarthr/a11y-input-has-name-attribute': [
15
+ 'error', // 'warn', 'off'
16
+ // { checkType: 'always' } /* 'always' || 'smart' */
17
+ ]
14
18
  },
15
19
  }
16
20
  ```
@@ -23,6 +27,10 @@
23
27
  <input type="text" />
24
28
  <Textarea />
25
29
  <Select />
30
+
31
+ // checkType: 'always'
32
+ <AnyInput {...args} />
33
+ <AnyInput {...args} any="any" />
26
34
  ```
27
35
 
28
36
 
@@ -42,6 +50,10 @@ const StyledPiyo = styled(RadioButton)``;
42
50
  <input type="text" name="any" />
43
51
  <Textarea name="some" />
44
52
  <Select name="piyo" />
53
+
54
+ // checkType: 'smart'
55
+ <AnyInput {...args} />
56
+ <AnyInput {...args} any="any" />
45
57
  ```
46
58
 
47
59
  ```jsx
@@ -14,8 +14,10 @@ const EXPECTED_NAMES = {
14
14
  const TARGET_TAG_NAME_REGEX = new RegExp(`(${Object.keys(EXPECTED_NAMES).join('|')})`)
15
15
  const INPUT_NAME_REGEX = /^[a-zA-Z0-9_\[\]]+$/
16
16
  const INPUT_TAG_REGEX = /(i|I)nput$/
17
+ const RADIO_BUTTON_REGEX = /RadioButton$/
17
18
 
18
19
  const findNameAttr = (a) => a?.name?.name === 'name'
20
+ const findSpreadAttr = (a) => a.type === 'JSXSpreadAttribute'
19
21
  const findRadioInput = (a) => a.name?.name === 'type' && a.value.value === 'radio'
20
22
 
21
23
  const MESSAGE_PART_FORMAT = `"${INPUT_NAME_REGEX.toString()}"にmatchするフォーマットで命名してください`
@@ -27,7 +29,15 @@ const MESSAGE_UNDEFINED_FOR_RADIO = `にグループとなる他のinput[radio]
27
29
  const MESSAGE_UNDEFINED_FOR_NOT_RADIO = `にname属性を指定してください${MESSAGE_UNDEFINED_NAME_PART}`
28
30
  const MESSAGE_NAME_FORMAT_SUFFIX = `はブラウザの自動補完が適切に行えない可能性があるため${MESSAGE_PART_FORMAT}`
29
31
 
30
- const SCHEMA = []
32
+ const SCHEMA = [
33
+ {
34
+ type: 'object',
35
+ properties: {
36
+ checkType: { type: 'string', enum: ['always', 'smart'], default: 'always' },
37
+ },
38
+ additionalProperties: false,
39
+ }
40
+ ]
31
41
 
32
42
  module.exports = {
33
43
  meta: {
@@ -35,6 +45,9 @@ module.exports = {
35
45
  schema: SCHEMA,
36
46
  },
37
47
  create(context) {
48
+ const option = context.options[0] || {}
49
+ const checkType = option.checkType || 'always'
50
+
38
51
  return {
39
52
  ...generateTagFormatter({ context, EXPECTED_NAMES }),
40
53
  JSXOpeningElement: (node) => {
@@ -44,14 +57,20 @@ module.exports = {
44
57
  const nameAttr = node.attributes.find(findNameAttr)
45
58
 
46
59
  if (!nameAttr) {
47
- const isRadio =
48
- nodeName.match(/RadioButton$/) ||
49
- (nodeName.match(INPUT_TAG_REGEX) && node.attributes.some(findRadioInput));
60
+ if (
61
+ node.attributes.length === 0 ||
62
+ checkType !== 'smart' ||
63
+ !node.attributes.some(findSpreadAttr)
64
+ ) {
65
+ const isRadio =
66
+ nodeName.match(RADIO_BUTTON_REGEX) ||
67
+ (nodeName.match(INPUT_TAG_REGEX) && node.attributes.some(findRadioInput));
50
68
 
51
- context.report({
52
- node,
53
- message: `${nodeName} ${isRadio ? MESSAGE_UNDEFINED_FOR_RADIO : MESSAGE_UNDEFINED_FOR_NOT_RADIO}`,
54
- });
69
+ context.report({
70
+ node,
71
+ message: `${nodeName} ${isRadio ? MESSAGE_UNDEFINED_FOR_RADIO : MESSAGE_UNDEFINED_FOR_NOT_RADIO}`,
72
+ });
73
+ }
55
74
  } else {
56
75
  const nameValue = nameAttr.value?.value || ''
57
76
 
@@ -1,19 +1,55 @@
1
1
  const path = require('path')
2
2
  const fs = require('fs')
3
3
  const { replacePaths, rootPath } = require('../../libs/common')
4
+
5
+ const SCHEMA = [
6
+ {
7
+ type: 'object',
8
+ properties: {
9
+ allowedImports: {
10
+ type: 'object',
11
+ patternProperties: {
12
+ '.+': {
13
+ type: 'object',
14
+ patternProperties: {
15
+ '.+': {
16
+ type: ['boolean', 'array' ],
17
+ items: {
18
+ type: 'string',
19
+ },
20
+ additionalProperties: false
21
+ }
22
+ }
23
+ },
24
+ },
25
+ additionalProperties: true,
26
+ },
27
+ ignores: { type: 'array', items: { type: 'string' }, default: [] },
28
+ },
29
+ additionalProperties: false,
30
+ }
31
+ ]
32
+
33
+ const entriedReplacePaths = Object.entries(replacePaths)
34
+ const CWD = process.cwd()
35
+ const REGEX_UNNECESSARY_SLASH = /(\/)+/g
36
+ const REGEX_ROOT_PATH = new RegExp(`^${rootPath}/index\.`)
37
+ const REGEX_INDEX_FILE = /\/index\.(ts|js)x?$/
38
+ const TARGET_EXTS = ['ts', 'tsx', 'js', 'jsx']
39
+
4
40
  const calculateAbsoluteImportPath = (source) => {
5
41
  if (source[0] === '/') {
6
42
  return source
7
43
  }
8
44
 
9
- return Object.entries(replacePaths).reduce((prev, [key, values]) => {
45
+ return entriedReplacePaths.reduce((prev, [key, values]) => {
10
46
  if (source === prev) {
47
+ const regexp = new RegExp(`^${key}(.+)$`)
48
+
11
49
  return values.reduce((p, v) => {
12
50
  if (prev === p) {
13
- const regexp = new RegExp(`^${key}(.+)$`)
14
-
15
51
  if (prev.match(regexp)) {
16
- return p.replace(regexp, `${path.resolve(`${process.cwd()}/${v}`)}/$1`)
52
+ return p.replace(regexp, `${path.resolve(`${CWD}/${v}`)}/$1`)
17
53
  }
18
54
  }
19
55
 
@@ -25,14 +61,14 @@ const calculateAbsoluteImportPath = (source) => {
25
61
  }, source)
26
62
  }
27
63
  const calculateReplacedImportPath = (source) => {
28
- return Object.entries(replacePaths).reduce((prev, [key, values]) => {
64
+ return entriedReplacePaths.reduce((prev, [key, values]) => {
29
65
  if (source === prev) {
30
66
  return values.reduce((p, v) => {
31
67
  if (prev === p) {
32
- const regexp = new RegExp(`^${path.resolve(`${process.cwd()}/${v}`)}(.+)$`)
68
+ const regexp = new RegExp(`^${path.resolve(`${CWD}/${v}`)}(.+)$`)
33
69
 
34
70
  if (prev.match(regexp)) {
35
- return p.replace(regexp, `${key}/$1`).replace(/(\/)+/g, '/')
71
+ return p.replace(regexp, `${key}/$1`).replace(REGEX_UNNECESSARY_SLASH, '/')
36
72
  }
37
73
  }
38
74
 
@@ -43,34 +79,9 @@ const calculateReplacedImportPath = (source) => {
43
79
  return prev
44
80
  }, source)
45
81
  }
46
- const TARGET_EXTS = ['ts', 'tsx', 'js', 'jsx']
47
- const SCHEMA = [
48
- {
49
- type: 'object',
50
- properties: {
51
- allowedImports: {
52
- type: 'object',
53
- patternProperties: {
54
- '.+': {
55
- type: 'object',
56
- patternProperties: {
57
- '.+': {
58
- type: ['boolean', 'array' ],
59
- items: {
60
- type: 'string',
61
- },
62
- additionalProperties: false
63
- }
64
- }
65
- },
66
- },
67
- additionalProperties: true,
68
- },
69
- ignores: { type: 'array', items: { type: 'string' }, default: [] },
70
- },
71
- additionalProperties: false,
72
- }
73
- ]
82
+
83
+ const pickImportedName = (s) => s.imported?.name
84
+ const findExistsSync = (p) => fs.existsSync(p)
74
85
 
75
86
  module.exports = {
76
87
  meta: {
@@ -81,16 +92,13 @@ module.exports = {
81
92
  const option = context.options[0] || {}
82
93
  const filename = context.getFilename()
83
94
 
84
- if ((option.ignores || []).some((i) => !!filename.match(new RegExp(i)))) {
95
+ if (option.ignores && option.ignores.some((i) => !!filename.match(new RegExp(i)))) {
85
96
  return {}
86
97
  }
87
98
 
88
- const dir = (() => {
89
- const d = filename.split('/')
90
- d.pop()
91
-
92
- return d.join('/')
93
- })()
99
+ let d = filename.split('/')
100
+ d.pop()
101
+ const dir = d.join('/')
94
102
  const targetPathRegexs = Object.keys(option?.allowedImports || {})
95
103
  const targetAllowedImports = targetPathRegexs.filter((regex) => !!filename.match(new RegExp(regex)))
96
104
 
@@ -105,7 +113,7 @@ module.exports = {
105
113
 
106
114
  targetModules.forEach((targetModule) => {
107
115
  const allowedModules = allowedOption[targetModule] || true
108
- const actualTarget = targetModule[0] !== '.' ? targetModule : path.resolve(`${process.cwd()}/${targetModule}`)
116
+ const actualTarget = targetModule[0] !== '.' ? targetModule : path.resolve(`${CWD}/${targetModule}`)
109
117
  let sourceValue = node.source.value
110
118
 
111
119
  if (actualTarget[0] === '/') {
@@ -116,21 +124,19 @@ module.exports = {
116
124
  return
117
125
  }
118
126
 
119
-
120
127
  if (!Array.isArray(allowedModules)) {
121
128
  isDenyPath = true
122
129
  deniedModules.push(true)
123
130
  } else {
124
- deniedModules.push(node.specifiers.map((s) => s.imported?.name).filter(i => allowedModules.indexOf(i) == -1))
131
+ deniedModules.push(node.specifiers.map(pickImportedName).filter(i => allowedModules.indexOf(i) == -1))
125
132
  }
126
133
  })
127
134
  })
128
135
 
129
- if (isDenyPath && deniedModules[0] === true) {
130
- return
131
- }
132
-
133
- if (!isDenyPath && deniedModules.length === 1 && deniedModules[0].length === 0) {
136
+ if (
137
+ isDenyPath && deniedModules[0] === true ||
138
+ !isDenyPath && deniedModules.length === 1 && deniedModules[0].length === 0
139
+ ) {
134
140
  return
135
141
  }
136
142
 
@@ -167,15 +173,15 @@ module.exports = {
167
173
  break
168
174
  }
169
175
 
170
- barrel = TARGET_EXTS.map((e) => `${sourceValue}/index.${e}`).find((p) => fs.existsSync(p)) || barrel
176
+ barrel = TARGET_EXTS.map((e) => `${sourceValue}/index.${e}`).find(findExistsSync) || barrel
171
177
 
172
178
  sources.pop()
173
179
  sourceValue = sources.join('/')
174
180
  }
175
181
 
176
- if (barrel && !barrel.match(new RegExp(`^${rootPath}/index\.`))) {
182
+ if (barrel && !barrel.match(REGEX_ROOT_PATH)) {
177
183
  barrel = calculateReplacedImportPath(barrel)
178
- const noExt = barrel.replace(/\/index\.(ts|js)x?$/, '')
184
+ const noExt = barrel.replace(REGEX_INDEX_FILE, '')
179
185
  deniedModules = [...new Set(deniedModules.flat())]
180
186
 
181
187
  context.report({
@@ -29,30 +29,31 @@ ruleTester.run('a11y-anchor-has-href-attribute', rule, {
29
29
  { code: 'const HogeLink = styled.a``' },
30
30
  { code: 'const HogeAnchor = styled(Anchor)``' },
31
31
  { code: 'const HogeLink = styled(Link)``' },
32
- {
33
- code: `<a href="hoge">ほげ</a>`,
34
- },
35
- {
36
- code: `<a href={hoge}>ほげ</a>`,
37
- },
38
- {
39
- code: `<a href={undefined}>ほげ</a>`,
40
- },
41
- {
42
- code: `<HogeAnchor href={hoge}>ほげ</HogeAnchor>`,
43
- },
44
- {
45
- code: `<Link href="hoge">ほげ</Link>`,
46
- },
47
- {
48
- code: `<Link href="#fuga">ほげ</Link>`,
49
- },
32
+ { code: `<a href="hoge">ほげ</a>` },
33
+ { code: `<a href={hoge}>ほげ</a>` },
34
+ { code: `<a href={undefined}>ほげ</a>` },
35
+ { code: `<HogeAnchor href={hoge}>ほげ</HogeAnchor>` },
36
+ { code: `<Link href="hoge">ほげ</Link>` },
37
+ { code: `<Link href="#fuga">ほげ</Link>` },
38
+ { code: '<AnyAnchor {...args1} />', options: [{ checkType: 'smart' }] },
50
39
  ],
51
40
  invalid: [
52
41
  { code: `import hoge from 'styled-components'`, errors: [ { message: `styled-components をimportする際は、名称が"styled" となるようにしてください。例: "import styled from 'styled-components'"` } ] },
53
42
  { code: 'const Hoge = styled.a``', errors: [ { message: `Hogeを正規表現 "/(Anchor|Link)$/" がmatchする名称に変更してください` } ] },
54
43
  { code: 'const Hoge = styled(Anchor)``', errors: [ { message: `Hogeを正規表現 "/Anchor$/" がmatchする名称に変更してください` } ] },
55
44
  { code: 'const Hoge = styled(Link)``', errors: [ { message: `Hogeを正規表現 "/Link$/" がmatchする名称に変更してください` } ] },
45
+ { code: 'const FugaAnchor = styled.div``', errors: [ { message: `FugaAnchor は /(Anchor|^a)$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
46
+ - FugaAnchor の名称の末尾が"Anchor" という文字列ではない状態にしつつ、"div"を継承していることをわかる名称に変更してください
47
+ - もしくは"div"を"FugaAnchor"の継承元であることがわかるような適切なタグや別コンポーネントに差し替えてください
48
+ - 修正例1: const FugaXxxx = styled.div
49
+ - 修正例2: const FugaAnchorXxxx = styled.div
50
+ - 修正例3: const FugaAnchor = styled(XxxxAnchor)` } ] },
51
+ { code: 'const FugaLink = styled.p``', errors: [ { message: `FugaLink は /(Link|^a)$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
52
+ - FugaLink の名称の末尾が"Link" という文字列ではない状態にしつつ、"p"を継承していることをわかる名称に変更してください
53
+ - もしくは"p"を"FugaLink"の継承元であることがわかるような適切なタグや別コンポーネントに差し替えてください
54
+ - 修正例1: const FugaXxxx = styled.p
55
+ - 修正例2: const FugaLinkXxxx = styled.p
56
+ - 修正例3: const FugaLink = styled(XxxxLink)` } ] },
56
57
  { code: `<a></a>`, errors: [{ message: generateErrorText('a') }] },
57
58
  { code: `<a>hoge</a>`, errors: [{ message: generateErrorText('a') }] },
58
59
  { code: `<Anchor>hoge</Anchor>`, errors: [{ message: generateErrorText('Anchor') }] },
@@ -65,5 +66,7 @@ ruleTester.run('a11y-anchor-has-href-attribute', rule, {
65
66
  { code: `<HogeLink href={''}>hoge</HogeLink>`, errors: [{ message: generateErrorText('HogeLink') }] },
66
67
  { code: `<HogeLink href="#">hoge</HogeLink>`, errors: [{ message: generateErrorText('HogeLink') }] },
67
68
  { code: `<HogeLink href={'#'}>hoge</HogeLink>`, errors: [{ message: generateErrorText('HogeLink') }] },
69
+ { code: '<AnyAnchor {...args1} />', errors: [{ message: generateErrorText('AnyAnchor') }] },
70
+ { code: '<AnyAnchor {...args1} />', options: [{ checkType: 'always' }], errors: [{ message: generateErrorText('AnyAnchor') }] },
68
71
  ]
69
72
  })
@@ -141,6 +141,24 @@ ruleTester.run('a11y-clickable-element-has-text', rule, {
141
141
  { code: 'const Piyo = styled(Anchor)(() => ``)', errors: [ { message: `Piyoを正規表現 "/Anchor$/" がmatchする名称に変更してください` } ] },
142
142
  { code: 'const Hoge = styled(Text)``', errors: [ { message: `Hogeを正規表現 "/Text$/" がmatchする名称に変更してください` } ] },
143
143
  { code: 'const Hoge = styled(HogeMessage)``', errors: [ { message: `Hogeを正規表現 "/Message$/" がmatchする名称に変更してください` } ] },
144
+ { code: 'const StyledButton = styled.div``', errors: [ { message: `StyledButton は /(B|^b)utton$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
145
+ - StyledButton の名称の末尾が"Button" という文字列ではない状態にしつつ、"div"を継承していることをわかる名称に変更してください
146
+ - もしくは"div"を"StyledButton"の継承元であることがわかるような適切なタグや別コンポーネントに差し替えてください
147
+ - 修正例1: const StyledXxxx = styled.div
148
+ - 修正例2: const StyledButtonXxxx = styled.div
149
+ - 修正例3: const StyledButton = styled(XxxxButton)` } ] },
150
+ { code: 'const HogeAnchor = styled(Fuga)``', errors: [ { message: `HogeAnchor は /(Anchor|^a)$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
151
+ - HogeAnchor の名称の末尾が"Anchor" という文字列ではない状態にしつつ、"Fuga"を継承していることをわかる名称に変更してください
152
+ - もしくは"Fuga"を"HogeAnchor"の継承元であることがわかるような名称に変更するか、適切な別コンポーネントに差し替えてください
153
+ - 修正例1: const HogeXxxx = styled(Fuga)
154
+ - 修正例2: const HogeAnchorXxxx = styled(Fuga)
155
+ - 修正例3: const HogeAnchor = styled(XxxxAnchor)` } ] },
156
+ { code: 'const HogeLink = styled.p``', errors: [ { message: `HogeLink は /(Link|^a)$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
157
+ - HogeLink の名称の末尾が"Link" という文字列ではない状態にしつつ、"p"を継承していることをわかる名称に変更してください
158
+ - もしくは"p"を"HogeLink"の継承元であることがわかるような適切なタグや別コンポーネントに差し替えてください
159
+ - 修正例1: const HogeXxxx = styled.p
160
+ - 修正例2: const HogeLinkXxxx = styled.p
161
+ - 修正例3: const HogeLink = styled(XxxxLink)` } ] },
144
162
  {
145
163
  code: `<a><img src="hoge.jpg" /></a>`,
146
164
  errors: [{ message: defaultErrorMessage }]
@@ -58,6 +58,30 @@ ruleTester.run('a11y-heading-in-sectioning-content', rule, {
58
58
  { code: 'const StyledAside = styled.aside``', errors: [ { message: `"aside"を利用せず、smarthr-ui/Asideを拡張してください。Headingのレベルが自動計算されるようになります。(例: "styled.aside" -> "styled(Aside)")` } ] },
59
59
  { code: 'const StyledNav = styled.nav``', errors: [ { message: `"nav"を利用せず、smarthr-ui/Navを拡張してください。Headingのレベルが自動計算されるようになります。(例: "styled.nav" -> "styled(Nav)")` } ] },
60
60
  { code: 'const StyledSection = styled.section``', errors: [ { message: `"section"を利用せず、smarthr-ui/Sectionを拡張してください。Headingのレベルが自動計算されるようになります。(例: "styled.section" -> "styled(Section)")` } ] },
61
+ { code: 'const StyledSection = styled.div``', errors: [ { message: `StyledSection は smarthr-ui/Section をextendすることを期待する名称になっています
62
+ - childrenにHeadingを含まない場合、コンポーネントの名称から"Section"を取り除いてください
63
+ - childrenにHeadingを含み、アウトラインの範囲を指定するためのコンポーネントならば、smarthr-ui/Sectionをexendしてください
64
+ - "styled(Xxxx)" 形式の場合、拡張元であるXxxxコンポーネントの名称の末尾に"Section"を設定し、そのコンポーネント内でsmarthr-ui/Sectionを利用してください` } ] },
65
+ { code: 'const StyledArticle = styled(Hoge)``', errors: [ { message: `StyledArticle は smarthr-ui/Article をextendすることを期待する名称になっています
66
+ - childrenにHeadingを含まない場合、コンポーネントの名称から"Article"を取り除いてください
67
+ - childrenにHeadingを含み、アウトラインの範囲を指定するためのコンポーネントならば、smarthr-ui/Articleをexendしてください
68
+ - "styled(Xxxx)" 形式の場合、拡張元であるXxxxコンポーネントの名称の末尾に"Article"を設定し、そのコンポーネント内でsmarthr-ui/Articleを利用してください` } ] },
69
+ { code: 'const StyledAside = styled(AsideXxxx)``', errors: [ { message: `StyledAside は smarthr-ui/Aside をextendすることを期待する名称になっています
70
+ - childrenにHeadingを含まない場合、コンポーネントの名称から"Aside"を取り除いてください
71
+ - childrenにHeadingを含み、アウトラインの範囲を指定するためのコンポーネントならば、smarthr-ui/Asideをexendしてください
72
+ - "styled(Xxxx)" 形式の場合、拡張元であるXxxxコンポーネントの名称の末尾に"Aside"を設定し、そのコンポーネント内でsmarthr-ui/Asideを利用してください` } ] },
73
+ { code: 'const StyledHeading = styled(Hoge)``', errors: [ { message: `StyledHeading は /(Heading|^h(1|2|3|4|5|6))$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
74
+ - StyledHeading の名称の末尾が"Heading" という文字列ではない状態にしつつ、"Hoge"を継承していることをわかる名称に変更してください
75
+ - もしくは"Hoge"を"StyledHeading"の継承元であることがわかるような名称に変更するか、適切な別コンポーネントに差し替えてください
76
+ - 修正例1: const StyledXxxx = styled(Hoge)
77
+ - 修正例2: const StyledHeadingXxxx = styled(Hoge)
78
+ - 修正例3: const StyledHeading = styled(XxxxHeading)` } ] },
79
+ { code: 'const StyledHeading = styled.div``', errors: [ { message: `StyledHeading は /(Heading|^h(1|2|3|4|5|6))$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
80
+ - StyledHeading の名称の末尾が"Heading" という文字列ではない状態にしつつ、"div"を継承していることをわかる名称に変更してください
81
+ - もしくは"div"を"StyledHeading"の継承元であることがわかるような適切なタグや別コンポーネントに差し替えてください
82
+ - 修正例1: const StyledXxxx = styled.div
83
+ - 修正例2: const StyledHeadingXxxx = styled.div
84
+ - 修正例3: const StyledHeading = styled(XxxxHeading)` } ] },
61
85
  { code: '<><PageHeading>hoge</PageHeading><PageHeading>fuga</PageHeading></>', errors: [ { message: pageMessage } ] },
62
86
  { code: '<Heading>hoge</Heading>', errors: [ { message } ] },
63
87
  { code: '<><Heading>hoge</Heading><Heading>fuga</Heading></>', errors: [ { message }, { message } ] },
@@ -32,12 +32,13 @@ ruleTester.run('a11y-image-has-alt-attribute', rule, {
32
32
  { code: 'const HogeIcon = styled.svg``' },
33
33
  { code: 'const HogeImg = styled(Img)``' },
34
34
  { code: 'const HogeImage = styled(Image)``' },
35
- { code: 'const HogeIcon = styled(ICon)``' },
35
+ { code: 'const HogeIcon = styled(Icon)``' },
36
36
  { code: '<img alt="hoge" />' },
37
37
  { code: '<HogeImg alt="hoge" />' },
38
38
  { code: '<HogeImage alt="hoge" />' },
39
39
  { code: '<HogeIcon />' },
40
40
  { code: '<svg><image /></svg>' },
41
+ { code: '<AnyImg {...hoge} />', options: [{ checkType: 'smart' }] },
41
42
  ],
42
43
  invalid: [
43
44
  { code: `import hoge from 'styled-components'`, errors: [ { message: `styled-components をimportする際は、名称が"styled" となるようにしてください。例: "import styled from 'styled-components'"` } ] },
@@ -46,8 +47,28 @@ ruleTester.run('a11y-image-has-alt-attribute', rule, {
46
47
  { code: 'const Hoge = styled(Icon)``', errors: [ { message: `Hogeを正規表現 "/Icon$/" がmatchする名称に変更してください` } ] },
47
48
  { code: 'const Hoge = styled(Img)``', errors: [ { message: `Hogeを正規表現 "/Img$/" がmatchする名称に変更してください` } ] },
48
49
  { code: 'const Hoge = styled(Image)``', errors: [ { message: `Hogeを正規表現 "/Image$/" がmatchする名称に変更してください` } ] },
50
+ { code: 'const StyledImage = styled.span``', errors: [ { message: `StyledImage は /(Image|^(img|svg))$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
51
+ - StyledImage の名称の末尾が"Image" という文字列ではない状態にしつつ、"span"を継承していることをわかる名称に変更してください
52
+ - もしくは"span"を"StyledImage"の継承元であることがわかるような適切なタグや別コンポーネントに差し替えてください
53
+ - 修正例1: const StyledXxxx = styled.span
54
+ - 修正例2: const StyledImageXxxx = styled.span
55
+ - 修正例3: const StyledImage = styled(XxxxImage)` } ] },
56
+ { code: 'const StyledImg = styled(Hoge)``', errors: [ { message: `StyledImg は /(Img|^(img|svg))$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
57
+ - StyledImg の名称の末尾が"Img" という文字列ではない状態にしつつ、"Hoge"を継承していることをわかる名称に変更してください
58
+ - もしくは"Hoge"を"StyledImg"の継承元であることがわかるような名称に変更するか、適切な別コンポーネントに差し替えてください
59
+ - 修正例1: const StyledXxxx = styled(Hoge)
60
+ - 修正例2: const StyledImgXxxx = styled(Hoge)
61
+ - 修正例3: const StyledImg = styled(XxxxImg)` } ] },
62
+ { code: 'const FugaIcon = styled(Fuga)``', errors: [ { message: `FugaIcon は /(Icon|^(img|svg))$/ にmatchする名前のコンポーネントを拡張することを期待している名称になっています
63
+ - FugaIcon の名称の末尾が"Icon" という文字列ではない状態にしつつ、"Fuga"を継承していることをわかる名称に変更してください
64
+ - もしくは"Fuga"を"FugaIcon"の継承元であることがわかるような名称に変更するか、適切な別コンポーネントに差し替えてください
65
+ - 修正例1: const FugaXxxx = styled(Fuga)
66
+ - 修正例2: const FugaIconXxxx = styled(Fuga)
67
+ - 修正例3: const FugaIcon = styled(XxxxIcon)` } ] },
49
68
  { code: '<img />', errors: [ { message: messageNotExistAlt } ] },
50
69
  { code: '<HogeImage alt="" />', errors: [ { message: messageNullAlt } ] },
51
70
  { code: '<hoge><image /></hoge>', errors: [ { message: messageNotExistAlt } ] },
71
+ { code: '<AnyImg {...hoge} />', errors: [ { message: messageNotExistAlt } ] },
72
+ { code: '<AnyImg {...hoge} />', options: [{ checkType: 'always' }], errors: [ { message: messageNotExistAlt } ] },
52
73
  ]
53
74
  })
@@ -37,6 +37,9 @@ ruleTester.run('a11y-input-has-name-attribute', rule, {
37
37
  { code: '<HogeTextarea name="hoge" />' },
38
38
  { code: '<select name="hoge" />' },
39
39
  { code: '<Select name="hoge[0][Fuga]" />' },
40
+ { code: '<Input {...hoge} />', options: [{ checkType: 'smart' }] },
41
+ { code: '<Input {...args1} {...args2} />', options: [{ checkType: 'smart' }] },
42
+ { code: '<Input {...args} hoge="fuga" />', options: [{ checkType: 'smart' }] },
40
43
  ],
41
44
  invalid: [
42
45
  { code: `import hoge from 'styled-components'`, errors: [ { message: `styled-components をimportする際は、名称が"styled" となるようにしてください。例: "import styled from 'styled-components'"` } ] },
@@ -56,5 +59,11 @@ ruleTester.run('a11y-input-has-name-attribute', rule, {
56
59
  { code: '<HogeTextarea />', errors: [ { message: `HogeTextarea にname属性を指定してください${MESSAGE_SUFFIX}` } ] },
57
60
  { code: '<input type="radio" name="ほげ" />', errors: [ { message: 'input のname属性の値(ほげ)はブラウザの自動補完が適切に行えない可能性があるため"/^[a-zA-Z0-9_\\[\\]]+$/"にmatchするフォーマットで命名してください' } ] },
58
61
  { code: '<select name="hoge[fuga][0][あいうえお]" />', errors: [ { message: 'select のname属性の値(hoge[fuga][0][あいうえお])はブラウザの自動補完が適切に行えない可能性があるため"/^[a-zA-Z0-9_\\[\\]]+$/"にmatchするフォーマットで命名してください' } ] },
62
+ { code: '<Input {...hoge} />', errors: [ { message: `Input にname属性を指定してください
63
+ - ブラウザの自動補完が有効化されるなどのメリットがあります
64
+ - より多くのブラウザが自動補完を行える可能性を上げるため、\"/^[a-zA-Z0-9_\\[\\]]+$/\"にmatchするフォーマットで命名してください` } ] },
65
+ { code: '<Input {...hoge} hoge="fuga" />', options: [{ checkType: 'always' }], errors: [ { message: `Input にname属性を指定してください
66
+ - ブラウザの自動補完が有効化されるなどのメリットがあります
67
+ - より多くのブラウザが自動補完を行える可能性を上げるため、\"/^[a-zA-Z0-9_\\[\\]]+$/\"にmatchするフォーマットで命名してください` } ] },
59
68
  ],
60
69
  });
@@ -27,9 +27,9 @@ ruleTester.run('best-practice-for-date', rule, {
27
27
  { code: `const year = 2022; const month = 11; const date = 31; new Date(year, month, date)` },
28
28
  ],
29
29
  invalid: [
30
- { code: 'new Date("2022/12/31")', errors: [ { message: errorNewDate } ] },
30
+ { code: 'new Date("2022/12/31")', errors: [ { message: errorNewDate } ], output: 'new Date(2022, 12 - 1, 31)' },
31
31
  { code: 'const arg = "2022/12/31"; new Date(arg)', errors: [ { message: errorNewDate } ] },
32
- { code: 'Date.parse("2022/12/31")', errors: [ { message: errorDateParse } ] },
32
+ { code: 'Date.parse("2022/12/31")', errors: [ { message: errorDateParse } ], output: 'new Date(2022, 12 - 1, 31).getTime()' },
33
33
  { code: 'const arg = "2022/12/31"; Date.parse(arg)', errors: [ { message: errorDateParse } ] },
34
34
  ]
35
35
  })