eslint-plugin-smarthr 0.5.2 → 0.5.4

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,27 @@
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.5.4](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.3...v0.5.4) (2024-03-29)
6
+
7
+
8
+ ### Features
9
+
10
+ * a11y-heading-in-sectioning-contentで title, もしくはheading属性がSectioningContent系コンポーネントに設定されている場合、headingがあると判定するように修正 ([#129](https://github.com/kufu/eslint-plugin-smarthr/issues/129)) ([93bbd19](https://github.com/kufu/eslint-plugin-smarthr/commit/93bbd1907cd7f28510dde1ddc93a16d0c92a2c9b))
11
+
12
+ ### [0.5.3](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.2...v0.5.3) (2024-03-26)
13
+
14
+
15
+ ### Features
16
+
17
+ * a11y-heading-in-sectioning-contentでHeadingを持たないSectioning Contentを検知するように修正 ([#124](https://github.com/kufu/eslint-plugin-smarthr/issues/124)) ([8b96de0](https://github.com/kufu/eslint-plugin-smarthr/commit/8b96de0c9a474b0c8d72a4b9c3b3b351d2cfb4e5))
18
+ * smarthr-ui/Layouts系コンポーネントの利用方法をチェックする best-practice-for-layouts ルールを追加する ([#126](https://github.com/kufu/eslint-plugin-smarthr/issues/126)) ([e0324e4](https://github.com/kufu/eslint-plugin-smarthr/commit/e0324e4ffab0413e61811cf7cf7f129c0602e0f0))
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * a11y-input-in-form-control で Clusterを拡張する際のチェックを追加 ([#125](https://github.com/kufu/eslint-plugin-smarthr/issues/125)) ([f723e24](https://github.com/kufu/eslint-plugin-smarthr/commit/f723e24db86c1095178bb1f28636147e7b619bf2))
24
+ * a11y-numbered-text-within-olのチェックで、属性の値でstyleに数値を指定しているような値の場合、無視する ([#123](https://github.com/kufu/eslint-plugin-smarthr/issues/123)) ([77a6278](https://github.com/kufu/eslint-plugin-smarthr/commit/77a6278ff8ec58c99843746442e1f3c0e54574c5))
25
+
5
26
  ### [0.5.2](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.1...v0.5.2) (2024-03-17)
6
27
 
7
28
  ### [0.5.1](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.5.0...v0.5.1) (2024-03-17)
package/README.md CHANGED
@@ -12,7 +12,9 @@
12
12
  - [a11y-prohibit-input-placeholder](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-input-placeholder)
13
13
  - [a11y-prohibit-useless-sectioning-fragment](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-prohibit-useless-sectioning-fragment)
14
14
  - [a11y-trigger-has-button](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/a11y-trigger-has-button)
15
+ - [best-practice-for-button-element](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-button-element)
15
16
  - [best-practice-for-date](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-date)
17
+ - [best-practice-for-layouts](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-layouts)
16
18
  - [best-practice-for-remote-trigger-dialog](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/best-practice-for-remote-trigger-dialog)
17
19
  - [format-import-path](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/format-import-path)
18
20
  - [format-translate-component](https://github.com/kufu/eslint-plugin-smarthr/tree/main/rules/format-translate-component)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -52,11 +52,13 @@ const bareTagRegex = /^(article|aside|nav|section)$/
52
52
  const modelessDialogRegex = /ModelessDialog$/
53
53
  const layoutComponentRegex = /((C(ent|lust)er)|Reel|Sidebar|Stack)$/
54
54
  const asRegex = /^(as|forwardedAs)$/
55
+ const ignoreCheckParentTypeRegex = /^(Program|ExportNamedDeclaration)$/
56
+ const noHeadingTagNamesRegex = /^(span|legend)$/
57
+ const ignoreHeadingCheckParentTypeRegex = /^(Program|ExportNamedDeclaration)$/
58
+ const headingAttributeRegex = /^(heading|title)$/
55
59
 
56
- const includeSectioningAsAttr = (a) => a.name?.name.match(asRegex) && a.value.value.match(bareTagRegex)
57
-
58
- const noHeadingTagNames = ['span', 'legend']
59
- const ignoreHeadingCheckParentType = ['Program', 'ExportNamedDeclaration']
60
+ const includeSectioningAsAttr = (a) => a.name?.name.match(asRegex) && bareTagRegex.test(a.value.value)
61
+ const findHeadingAttribute = (a) => headingAttributeRegex.test(a.name?.name || '')
60
62
 
61
63
  const headingMessage = `smarthr-ui/Headingと紐づく内容の範囲(アウトライン)が曖昧になっています。
62
64
  - smarthr-uiのArticle, Aside, Nav, SectionのいずれかでHeadingコンポーネントと内容をラップしてHeadingに対応する範囲を明確に指定してください。
@@ -102,8 +104,8 @@ const searchBubbleUp = (node) => {
102
104
  if (
103
105
  node.type === 'Program' ||
104
106
  node.type === 'JSXElement' && node.openingElement.name.name && (
105
- node.openingElement.name.name.match(sectioningRegex) ||
106
- node.openingElement.name.name.match(layoutComponentRegex) && node.openingElement.attributes.some(includeSectioningAsAttr)
107
+ sectioningRegex.test(node.openingElement.name.name) ||
108
+ layoutComponentRegex.test(node.openingElement.name.name) && node.openingElement.attributes.some(includeSectioningAsAttr)
107
109
  )
108
110
  ) {
109
111
  return node
@@ -111,16 +113,113 @@ const searchBubbleUp = (node) => {
111
113
 
112
114
  if (
113
115
  // Headingコンポーネントの拡張なので対象外
114
- node.type === 'VariableDeclarator' && ignoreHeadingCheckParentType.includes(node.parent.parent?.type) && node.id.name.match(declaratorHeadingRegex) ||
115
- node.type === 'FunctionDeclaration' && ignoreHeadingCheckParentType.includes(node.parent.type) && node.id.name.match(declaratorHeadingRegex) ||
116
+ node.type === 'VariableDeclarator' && node.parent.parent?.type.match(ignoreHeadingCheckParentTypeRegex) && declaratorHeadingRegex.test(node.id.name) ||
117
+ node.type === 'FunctionDeclaration' && ignoreHeadingCheckParentTypeRegex.test(node.parent.type) && declaratorHeadingRegex.test(node.id.name) ||
116
118
  // ModelessDialogのheaderにHeadingを設定している場合も対象外
117
- node.type === 'JSXAttribute' && node.name.name === 'header' && node.parent.name.name.match(modelessDialogRegex)
119
+ node.type === 'JSXAttribute' && node.name.name === 'header' && modelessDialogRegex.test(node.parent.name.name)
118
120
  ) {
119
121
  return null
120
122
  }
121
123
 
122
124
  return searchBubbleUp(node.parent)
123
125
  }
126
+ const searchBubbleUpSections = (node) => {
127
+ switch (node.type) {
128
+ case 'Program':
129
+ // rootまで検索した場合は確定でエラーにする
130
+ return null
131
+ case 'VariableDeclarator':
132
+ // SectioningContent系コンポーネントの拡張の場合は対象外
133
+ if (ignoreCheckParentTypeRegex.test(node.parent.parent?.type) && sectioningRegex.test(node.id.name)) {
134
+ return node
135
+ }
136
+
137
+ break
138
+ case 'FunctionDeclaration':
139
+ case 'ClassDeclaration':
140
+ // SectioningContent系コンポーネントの拡張の場合は対象外
141
+ if (ignoreCheckParentTypeRegex.test(node.parent.type) && sectioningRegex.test(node.id.name)) {
142
+ return node
143
+ }
144
+
145
+ break
146
+ }
147
+
148
+ return searchBubbleUpSections(node.parent)
149
+ }
150
+ const searchChildren = (n) => {
151
+ switch (n.type) {
152
+ case 'BinaryExpression':
153
+ case 'Identifier':
154
+ case 'JSXEmptyExpression':
155
+ case 'JSXText':
156
+ case 'Literal':
157
+ case 'VariableDeclaration':
158
+ // これ以上childrenが存在しないため終了
159
+ return false
160
+ case 'JSXAttribute':
161
+ return n.value ? searchChildren(n.value) : false
162
+ case 'LogicalExpression':
163
+ return searchChildren(n.right)
164
+ case 'ArrowFunctionExpression':
165
+ return searchChildren(n.body)
166
+ case 'MemberExpression':
167
+ return searchChildren(n.property)
168
+ case 'ReturnStatement':
169
+ case 'UnaryExpression':
170
+ return searchChildren(n.argument)
171
+ case 'ChainExpression':
172
+ case 'JSXExpressionContainer':
173
+ return searchChildren(n.expression)
174
+ case 'BlockStatement': {
175
+ return forInSearchChildren(n.body)
176
+ }
177
+ case 'ConditionalExpression': {
178
+ return searchChildren(n.consequent) || searchChildren(n.alternate)
179
+ }
180
+ case 'CallExpression': {
181
+ return forInSearchChildren(n.arguments)
182
+ }
183
+ case 'JSXFragment':
184
+ break
185
+ case 'JSXElement': {
186
+ const name = n.openingElement.name.name || ''
187
+
188
+ if (
189
+ sectioningRegex.test(name) ||
190
+ layoutComponentRegex.test(name) && n.openingElement.attributes.some(includeSectioningAsAttr)
191
+ ) {
192
+ return false
193
+ } else if (
194
+ (
195
+ headingRegex.test(name) &&
196
+ !n.openingElement.attributes.find(findTagAttr)?.value.value.match(noHeadingTagNamesRegex)
197
+ ) ||
198
+ forInSearchChildren(n.openingElement.attributes)
199
+ ) {
200
+ return true
201
+ }
202
+
203
+ break
204
+ }
205
+ }
206
+
207
+ return n.children ? forInSearchChildren(n.children) : false
208
+ }
209
+
210
+ const forInSearchChildren = (ary) => {
211
+ let r = false
212
+
213
+ for (const i in ary) {
214
+ r = searchChildren(ary[i])
215
+
216
+ if (r) {
217
+ break
218
+ }
219
+ }
220
+
221
+ return r
222
+ }
124
223
 
125
224
  const findTagAttr = (a) => a.name?.name == 'tag'
126
225
 
@@ -151,15 +250,15 @@ module.exports = {
151
250
  message,
152
251
  })
153
252
  // Headingに明示的にtag属性が設定されており、それらが span or legend の場合はHeading扱いしない
154
- } else if (elementName.match(headingRegex)) {
253
+ } else if (headingRegex.test(elementName)) {
155
254
  const tagAttr = node.attributes.find(findTagAttr)
156
255
 
157
- if (!noHeadingTagNames.includes(tagAttr?.value.value)) {
256
+ if (!tagAttr?.value.value.match(noHeadingTagNamesRegex)) {
158
257
  const result = searchBubbleUp(node.parent)
159
258
  let hit = false
160
259
 
161
260
  if (result) {
162
- if (elementName.match(pageHeadingRegex)) {
261
+ if (pageHeadingRegex.test(elementName)) {
163
262
  h1s.push(node)
164
263
 
165
264
  if (h1s.length > 1) {
@@ -181,7 +280,7 @@ module.exports = {
181
280
  node,
182
281
  message: rootHeadingMessage,
183
282
  })
184
- } else if (sections.find((s) => s === result)) {
283
+ } else if (sections.includes(result)) {
185
284
  hit = true
186
285
  context.report({
187
286
  node,
@@ -199,6 +298,26 @@ module.exports = {
199
298
  })
200
299
  }
201
300
  }
301
+ } else if (!node.selfClosing) {
302
+ const isSection = sectioningRegex.test(elementName)
303
+
304
+ // HINT: SectioningContent系コンポーネントの拡張の場合、title, heading属性などにHeadingのテキストが仕込まれている場合がある
305
+ // 対象属性を持っている場合はcorrectとして扱う
306
+ if (isSection && node.attributes.some(findHeadingAttribute)) {
307
+ return
308
+ }
309
+
310
+ const layoutSectionAsAttr = !isSection && layoutComponentRegex.test(elementName) ? node.attributes.find(includeSectioningAsAttr) : null
311
+
312
+ if ((isSection || layoutSectionAsAttr) && !searchBubbleUpSections(node.parent.parent) && !forInSearchChildren(node.parent.children)) {
313
+ context.report({
314
+ node,
315
+ message: `${isSection ? elementName : `<${elementName} ${layoutSectionAsAttr.name.name}="${layoutSectionAsAttr.value.value}">`} はHeading要素を含んでいません。
316
+ - SectioningContentはHeadingを含むようにマークアップする必要があります
317
+ - ${elementName}に設定しているいずれかの属性がHeading,もしくはHeadingのテキストに該当する場合、その属性の名称を ${headingAttributeRegex.toString()} にマッチする名称に変更してください
318
+ - Headingにするべき適切な文字列が存在しない場合、 ${isSection ? `${elementName} は削除するか、SectioningContentではない要素に差し替えてください` : `${layoutSectionAsAttr.name.name}="${layoutSectionAsAttr.value.value}"を削除、もしくは別の要素に変更してください`}`,
319
+ })
320
+ }
202
321
  }
203
322
  },
204
323
  }
@@ -32,6 +32,7 @@ const EXPECTED_NAMES = {
32
32
  '(A|^a)side$': '(Aside)$',
33
33
  '(N|^n)av$': '(Nav)$',
34
34
  '(S|^s)ection$': '(Section)$',
35
+ 'Cluster$': '(Cluster)$',
35
36
  'Center$': '(Center)$',
36
37
  'Reel$': '(Reel)$',
37
38
  'Sidebar$': '(Sidebar)$',
@@ -9,7 +9,7 @@ const UNEXPECTED_NAMES = EXPECTED_NAMES
9
9
  const NUMBERED_TEXT_REGEX = /^[\s\n]*(([0-9])([^0-9]{2})[^\s\n]*)/
10
10
  const ORDERED_LIST_REGEX = /(Ordered(.*)List|^ol)$/
11
11
  const SELECT_REGEX = /(S|s)elect$/
12
- const IGNORE_ATTRIBUTE_REGEX = /((w|W)idth|(h|H)eight)$/
12
+ const IGNORE_ATTRIBUTE_VALUE_REGEX = /^[0-9]+(px|em|ex|ch|rem|lh|rlh|vw|vh|vmin|vmax|vb|vi|svw|svh|lvw|lvh|dvw|dvh|%)?$/
13
13
  const AS_ATTRIBUTE_REGEX = /^(as|forwardedAs)$/
14
14
 
15
15
  const findAsOlAttr = (a) => a.type === 'JSXAttribute' && AS_ATTRIBUTE_REGEX.test(a.name?.name) && a.value?.value === 'ol'
@@ -128,7 +128,7 @@ module.exports = {
128
128
  return {
129
129
  ...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
130
130
  JSXAttribute: (node) => {
131
- if (node.value?.value && !IGNORE_ATTRIBUTE_REGEX.test(node.name?.name)) {
131
+ if (node.value?.value && !IGNORE_ATTRIBUTE_VALUE_REGEX.test(node.value.value)) {
132
132
  checker(node, node.value.value.match(NUMBERED_TEXT_REGEX))
133
133
  }
134
134
  },
@@ -0,0 +1,53 @@
1
+ # smarthr/best-practice-for-layouts
2
+
3
+ - smarthr-ui/Layoutsに属するコンポーネントの利用方法をチェックするルールです
4
+
5
+ ## rules
6
+
7
+ ```js
8
+ {
9
+ rules: {
10
+ 'smarthr/best-practice-for-layouts': 'error', // 'warn', 'off',
11
+ },
12
+ }
13
+ ```
14
+
15
+ ## ❌ Incorrect
16
+
17
+ ```jsx
18
+ // 子が複数無いためエラー
19
+ <Cluster>
20
+ <Any />
21
+ </Cluster>
22
+ <StyledStack>
23
+ {flg ? 'a' : 'b'}
24
+ </StyledStack>
25
+ ```
26
+
27
+ ## ✅ Correct
28
+
29
+ ```jsx
30
+ // 子が複数あるのでOK
31
+ <Cluster>
32
+ <Any />
33
+ <Any />
34
+ </Cluster>
35
+
36
+ <StyledStack>
37
+ {flg ? 'a' : (
38
+ <>
39
+ <Any />
40
+ <Any />
41
+ </>
42
+ )}
43
+ </StyledStack>
44
+
45
+ // Cluster、かつ右寄せをしている場合は子一つでもOK
46
+ <Cluster justify="end">
47
+ <Any />
48
+ </Cluster>
49
+ <Cluster justify="flex-end">
50
+ <Any />
51
+ <Any />
52
+ </Cluster>
53
+ ```
@@ -0,0 +1,100 @@
1
+ const { generateTagFormatter } = require('../../libs/format_styled_components')
2
+
3
+ const MULTI_CHILDREN_EXPECTED_NAMES = {
4
+ 'Cluster$': '(Cluster)$',
5
+ 'Stack$': '(Stack)$',
6
+ }
7
+ const EXPECTED_NAMES = {
8
+ ...MULTI_CHILDREN_EXPECTED_NAMES,
9
+ 'Center$': '(Center)$',
10
+
11
+ }
12
+
13
+ const UNEXPECTED_NAMES = EXPECTED_NAMES
14
+
15
+ const MULTI_CHILDREN_REGEX = new RegExp(`(${Object.keys(MULTI_CHILDREN_EXPECTED_NAMES).join('|')})`)
16
+ const REGEX_NLSP = /^\s*\n+\s*$/
17
+ const FLEX_END_REGEX = /^(flex-)?end$/
18
+
19
+ const filterFalsyJSXText = (cs) => cs.filter(checkFalsyJSXText)
20
+ const checkFalsyJSXText = (c) => (
21
+ !(
22
+ c.type === 'JSXText' && c.value.match(REGEX_NLSP) ||
23
+ c.type === 'JSXEmptyExpression'
24
+ )
25
+ )
26
+
27
+ const findJustifyAttr = (a) => a.name?.name === 'justify'
28
+
29
+ const searchChildren = (node) => {
30
+ if (
31
+ node.type === 'JSXFragment' ||
32
+ node.type === 'JSXElement' && node.openingElement.name?.name === 'SectioningFragment'
33
+ ) {
34
+ const children = node.children.filter(checkFalsyJSXText)
35
+
36
+ if (children.length > 1) {
37
+ return false
38
+ }
39
+ }
40
+
41
+ switch(node.type) {
42
+ case 'JSXExpressionContainer':
43
+ return searchChildren(node.expression)
44
+ case 'CallExpression':
45
+ return node.callee.property?.name !== 'map'
46
+ case 'ConditionalExpression':
47
+ return searchChildren(node.consequent) && searchChildren(node.alternate)
48
+ case 'LogicalExpression':
49
+ return searchChildren(node.right)
50
+ }
51
+
52
+ return true
53
+ }
54
+
55
+ const SCHEMA = []
56
+
57
+ module.exports = {
58
+ meta: {
59
+ type: 'problem',
60
+ schema: SCHEMA,
61
+ },
62
+ create(context) {
63
+ return {
64
+ ...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
65
+ JSXOpeningElement: (node) => {
66
+ const nodeName = node.name.name;
67
+
68
+ if (nodeName && !node.selfClosing) {
69
+ const matcher = nodeName.match(MULTI_CHILDREN_REGEX)
70
+
71
+ if (matcher) {
72
+ const children = node.parent.children.filter(checkFalsyJSXText)
73
+
74
+ if (children.length === 1) {
75
+ const layoutType = matcher[1]
76
+ const justifyAttr = layoutType === 'Cluster' ? node.attributes.find(findJustifyAttr) : null
77
+
78
+ if (justifyAttr && FLEX_END_REGEX.test(justifyAttr.value.value)) {
79
+ return
80
+ }
81
+
82
+ if (searchChildren(children[0])) {
83
+ context.report({
84
+ node,
85
+ message:
86
+ justifyAttr && justifyAttr.value.value === 'center'
87
+ ? `${nodeName} は smarthr-ui/${layoutType} ではなく smarthr-ui/Center でマークアップしてください`
88
+ : `${nodeName}には子要素が一つしか無いため、${layoutType}でマークアップする意味がありません。
89
+ - styleを確認し、div・spanなど、別要素でマークアップし直すか、${nodeName}を削除してください
90
+ - as, forwardedAsなどでSectioningContent系要素に変更している場合、対応するsmarthr-ui/Section, Aside, Nav, Article のいずれかに差し替えてください`
91
+ })
92
+ }
93
+ }
94
+ }
95
+ }
96
+ },
97
+ }
98
+ },
99
+ }
100
+ module.exports.schema = SCHEMA
@@ -22,6 +22,10 @@ const pageMessage = 'smarthr-ui/PageHeading が同一ファイル内に複数存
22
22
  const pageInSectionMessage = 'smarthr-ui/PageHeadingはsmarthr-uiのArticle, Aside, Nav, Sectionで囲まないでください。囲んでしまうとページ全体の見出しではなくなってしまいます。'
23
23
  const noTagAttrMessage = `tag属性を指定せず、smarthr-uiのArticle, Aside, Nav, Section, SectioningFragmentのいずれかの自動レベル計算に任せるよう、tag属性を削除してください。
24
24
  - tag属性を指定することで意図しないレベルに固定されてしまう可能性があります。`
25
+ const notHaveHeadingMessage = (elementName) => `${elementName} はHeading要素を含んでいません。
26
+ - SectioningContentはHeadingを含むようにマークアップする必要があります
27
+ - ${elementName}に設定しているいずれかの属性がHeading,もしくはHeadingのテキストに該当する場合、その属性の名称を /^(heading|title)$/ にマッチする名称に変更してください
28
+ - Headingにするべき適切な文字列が存在しない場合、 ${elementName} は削除するか、SectioningContentではない要素に差し替えてください`
25
29
 
26
30
  ruleTester.run('a11y-heading-in-sectioning-content', rule, {
27
31
  valid: [
@@ -52,6 +56,9 @@ ruleTester.run('a11y-heading-in-sectioning-content', rule, {
52
56
  { code: 'const FugaStack = styled(HogeStack)``' },
53
57
  { code: '<PageHeading>hoge</PageHeading>' },
54
58
  { code: '<Section><Heading>hoge</Heading></Section>' },
59
+ { code: '<FugaSection heading={<Heading>hoge</Heading>}>abc</FugaSection>' },
60
+ { code: '<FugaSection heading="hoge">abc</FugaSection>' },
61
+ { code: '<FugaSection title="hoge">abc</FugaSection>' },
55
62
  { code: '<><Section><Heading>hoge</Heading></Section><Section><Heading>fuga</Heading></Section></>' },
56
63
  { code: 'const HogeHeading = () => <FugaHeading anyArg={abc}>hoge</FugaHeading>' },
57
64
  { code: 'export const HogeHeading = () => <FugaHeading anyArg={abc}>hoge</FugaHeading>' },
@@ -137,5 +144,8 @@ ruleTester.run('a11y-heading-in-sectioning-content', rule, {
137
144
  { code: '<Section><Heading>hoge</Heading><Heading>fuga</Heading></Section>', errors: [ { message: lowerMessage } ] },
138
145
  { code: '<Section><PageHeading>hoge</PageHeading></Section>', errors: [ { message: pageInSectionMessage } ] },
139
146
  { code: '<Section><Heading tag="h2">hoge</Heading></Section>', errors: [ { message: noTagAttrMessage } ] },
147
+ { code: '<Section></Section>', errors: [ { message: notHaveHeadingMessage('Section') } ] },
148
+ { code: '<Aside><HogeSection></HogeSection></Aside>', errors: [ { message: notHaveHeadingMessage('Aside') }, { message: notHaveHeadingMessage('HogeSection') } ] },
149
+ { code: '<Aside any="hoge"><HogeSection><Heading /></HogeSection></Aside>', errors: [ { message: notHaveHeadingMessage('Aside') } ] },
140
150
  ],
141
151
  });
@@ -0,0 +1,84 @@
1
+ const rule = require('../rules/best-practice-for-layouts')
2
+ const RuleTester = require('eslint').RuleTester
3
+
4
+ const ruleTester = new RuleTester({
5
+ parserOptions: {
6
+ ecmaVersion: 2018,
7
+ ecmaFeatures: {
8
+ experimentalObjectRestSpread: true,
9
+ jsx: true,
10
+ },
11
+ sourceType: 'module',
12
+ },
13
+ })
14
+
15
+ const errorMessage = (type, name) => `${name}には子要素が一つしか無いため、${type}でマークアップする意味がありません。
16
+ - styleを確認し、div・spanなど、別要素でマークアップし直すか、${name}を削除してください
17
+ - as, forwardedAsなどでSectioningContent系要素に変更している場合、対応するsmarthr-ui/Section, Aside, Nav, Article のいずれかに差し替えてください`
18
+
19
+ ruleTester.run('best-practice-for-button-element', rule, {
20
+ valid: [
21
+ { code: `<Center />` },
22
+ { code: `<Cluster />` },
23
+ { code: `<Stack />` },
24
+ { code: `<HogeCenter />` },
25
+ { code: `<HogeCluster />` },
26
+ { code: `<HogeStack />` },
27
+ { code: `<Center><Hoge /></Center>` },
28
+ { code: `<Center><Hoge /><Hoge /></Center>` },
29
+ { code: `<Stack><Hoge /><Hoge /></Stack>` },
30
+ { code: `<Stack>{a}<Hoge /></Stack>` },
31
+ { code: `<AnyStack>{a.map(action)}</AnyStack>` },
32
+ { code: `<AnyStack>{a && <><Hoge /><Hoge /></>}</AnyStack>` },
33
+ { code: `<AnyStack>{a && a.map(action)}</AnyStack>` },
34
+ { code: `<AnyStack>{a && a.b.map(action)}</AnyStack>` },
35
+ { code: `<AnyStack>{a || <><Hoge /><Hoge /></>}</AnyStack>` },
36
+ { code: `<AnyStack>{a || a.map(action)}</AnyStack>` },
37
+ { code: `<AnyStack>{a || a.b.map(action)}</AnyStack>` },
38
+ { code: `<AnyStack>{a ? a.b.map(action) : <Hoge />}</AnyStack>` },
39
+ { code: `<AnyStack>{a ? <Hoge /> : a.b.map(action)}</AnyStack>` },
40
+ { code: `<AnyStack>{a ? <Hoge /> : a ? <Hoge /> : a.b.map(action)}</AnyStack>` },
41
+ { code: `<Cluster><Hoge /><Hoge /></Cluster>` },
42
+ { code: `<Cluster>{a}<Hoge /></Cluster>` },
43
+ { code: `<AnyCluster>{a.map(action)}</AnyCluster>` },
44
+ { code: `<AnyCluster>{a && <><Hoge /><Hoge /></>}</AnyCluster>` },
45
+ { code: `<AnyCluster>{a && a.map(action)}</AnyCluster>` },
46
+ { code: `<AnyCluster>{a && a.b.map(action)}</AnyCluster>` },
47
+ { code: `<AnyCluster>{a || <><Hoge /><Hoge /></>}</AnyCluster>` },
48
+ { code: `<AnyCluster>{a || a.map(action)}</AnyCluster>` },
49
+ { code: `<AnyCluster>{a || a.b.map(action)}</AnyCluster>` },
50
+ { code: `<AnyCluster>{a ? a.b.map(action) : <Hoge />}</AnyCluster>` },
51
+ { code: `<AnyCluster>{a ? <Hoge /> : a.b.map(action)}</AnyCluster>` },
52
+ { code: `<AnyCluster>{a ? <Hoge /> : a ? <Hoge /> : a.b.map(action)}</AnyCluster>` },
53
+ { code: `<Cluster justify="flex-end">{a}</Cluster>` },
54
+ { code: `<HogeCluster justify="end">{a}</HogeCluster>` },
55
+ ],
56
+ invalid: [
57
+ { code: `<Stack><Hoge /></Stack>`, errors: [ { message: errorMessage('Stack', 'Stack') } ] },
58
+ { code: `<Stack>{a}</Stack>`, errors: [ { message: errorMessage('Stack', 'Stack') } ] },
59
+ { code: `<AnyStack>{a.hoge(action)}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
60
+ { code: `<AnyStack>{a && <><Hoge /></>}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
61
+ { code: `<AnyStack>{a && a.hoge(action)}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
62
+ { code: `<AnyStack>{a && a.b.hoge(action)}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
63
+ { code: `<AnyStack>{a || <><Hoge /></>}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
64
+ { code: `<AnyStack>{a || a.hoge(action)}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
65
+ { code: `<AnyStack>{a || a.b.hoge(action)}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
66
+ { code: `<AnyStack>{a ? a.b.hoge(action) : <Hoge />}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
67
+ { code: `<AnyStack>{a ? <Hoge /> : a.b.hoge(action)}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
68
+ { code: `<AnyStack>{a ? <Hoge /> : a ? <Hoge /> : a.b.hoge(action)}</AnyStack>`, errors: [ { message: errorMessage('Stack', 'AnyStack') } ] },
69
+ { code: `<Cluster><Hoge /></Cluster>`, errors: [ { message: errorMessage('Cluster', 'Cluster') } ] },
70
+ { code: `<Cluster>{a}</Cluster>`, errors: [ { message: errorMessage('Cluster', 'Cluster') } ] },
71
+ { code: `<AnyCluster>{a.hoge(action)}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
72
+ { code: `<AnyCluster>{a && <><Hoge /></>}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
73
+ { code: `<AnyCluster>{a && a.hoge(action)}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
74
+ { code: `<AnyCluster>{a && a.b.hoge(action)}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
75
+ { code: `<AnyCluster>{a || <><Hoge /></>}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
76
+ { code: `<AnyCluster>{a || a.hoge(action)}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
77
+ { code: `<AnyCluster>{a || a.b.hoge(action)}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
78
+ { code: `<AnyCluster>{a ? a.b.hoge(action) : <Hoge />}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
79
+ { code: `<AnyCluster>{a ? <Hoge /> : a.b.hoge(action)}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
80
+ { code: `<AnyCluster>{a ? <Hoge /> : a ? <Hoge /> : a.b.hoge(action)}</AnyCluster>`, errors: [ { message: errorMessage('Cluster', 'AnyCluster') } ] },
81
+ { code: `<HogeCluster justify="center">{a}</HogeCluster>`, errors: [ { message: 'HogeCluster は smarthr-ui/Cluster ではなく smarthr-ui/Center でマークアップしてください' } ] },
82
+ ]
83
+ })
84
+