eslint-plugin-smarthr 6.2.2 → 6.3.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 CHANGED
@@ -2,6 +2,25 @@
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
+ ## [6.3.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.2.3...eslint-plugin-smarthr-v6.3.0) (2026-02-06)
6
+
7
+
8
+ ### Features
9
+
10
+ * **a11y-heading-in-sectioning-content:** tag属性がunrecommendedTagにrenameされたためチェック対象を調整 ([#1066](https://github.com/kufu/tamatebako/issues/1066)) ([5b524f3](https://github.com/kufu/tamatebako/commit/5b524f3a712e48953ec6aad18028f3f19d6b86ca))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * selectorでhas(>...)をやめ、直接属性で絞り込むように調整 ([#1069](https://github.com/kufu/tamatebako/issues/1069)) ([540426f](https://github.com/kufu/tamatebako/commit/540426f0ea90ba9b8cd67242067dcc8e4c855b5f))
16
+
17
+ ## [6.2.3](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.2.2...eslint-plugin-smarthr-v6.2.3) (2026-02-05)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * eslintのセレクタエンジンのアップデートにより、>の前後のスペースが必須になったため修正 ([#1055](https://github.com/kufu/tamatebako/issues/1055)) ([d885f3c](https://github.com/kufu/tamatebako/commit/d885f3cde328abc119c0e4798c399cbdd1b4907a))
23
+
5
24
  ## [6.2.2](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.2.1...eslint-plugin-smarthr-v6.2.2) (2026-02-05)
6
25
 
7
26
 
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "6.2.2",
3
+ "version": "6.3.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.21.1"
9
+ "node": ">=22.22.0"
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.51.0"
29
+ "typescript-eslint": "^8.54.0"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "eslint": "^9"
@@ -37,5 +37,5 @@
37
37
  "eslintplugin",
38
38
  "smarthr"
39
39
  ],
40
- "gitHead": "71497e58907b3eb5e52681f3e6473b3e30902ebf"
40
+ "gitHead": "b68d1a046be224dadd15a4e266df1ef1ccb49b23"
41
41
  }
@@ -46,10 +46,10 @@ const OPTION = (() => {
46
46
  const ANCHOR_ELEMENT = 'JSXOpeningElement[name.name=/(Anchor|Link|^a)$/]'
47
47
  const HREF_ATTRIBUTE = `JSXAttribute[name.name=${OPTION.react_router ? '/^(href|to)$/' : '"href"'}]`
48
48
  const NULL_HREF_ATTRIBUTE_VALUES = `${HREF_ATTRIBUTE}:matches(${['#', ''].reduce((prev, v) => {
49
- return `${prev},:has(>Literal[value="${v}"]),:has(>JSXExpressionContainer[expression.value="${v}"])`
49
+ return `${prev},[value.type="Literal"][value.value="${v}"],[value.type="JSXExpressionContainer"][value.expression.value="${v}"]`
50
50
  }, '[value=null]')})`
51
51
  const NEXT_LINK_REGEX = /Link$/
52
- // HINT: next/link で `Link>a` という構造がありえるので直上のJSXElementを調べる
52
+ // HINT: next/link で `Link > a` という構造がありえるので直上のJSXElementを調べる
53
53
  const nextCheck = (node) => ((node.parent.parent.openingElement.name.name || '').test(NEXT_LINK_REGEX))
54
54
 
55
55
  const MESSAGE_SUFFIX = ` に href${OPTION.react_router ? '、もしくはto' : ''} 属性を正しく設定してください
@@ -11,6 +11,7 @@ const noHeadingTagNamesRegex = /^(span|legend)$/
11
11
  const ignoreHeadingCheckParentTypeRegex = /^(Program|ExportNamedDeclaration)$/
12
12
  const headingAttributeRegex = /^(heading|title)$/
13
13
  const ariaLabelRegex = /^aria-label(ledby)?$/
14
+ const tagAttrRegex = /^(tag|unrecommendedTag)$/
14
15
 
15
16
  const includeSectioningAsAttr = (a) => asRegex.test(a.name?.name) && bareTagRegex.test(a.value.value)
16
17
 
@@ -23,9 +24,9 @@ const pageHeadingMessage = `smarthr-ui/PageHeading が同一ファイル内に
23
24
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-heading-in-sectioning-content`
24
25
  const pageHeadingInSectionMessage = `smarthr-ui/PageHeadingはsmarthr-uiのArticle, Aside, Nav, Sectionで囲まないでください。囲んでしまうとページ全体の見出しではなくなってしまいます。
25
26
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-heading-in-sectioning-content`
26
- const noTagAttrMessage = `tag属性を指定せず、smarthr-uiのArticle, Aside, Nav, Sectionのいずれかの自動レベル計算に任せるよう、tag属性を削除してください。
27
+ const noTagAttrMessage = `tag属性、unrecommendedTag属性を指定せず、smarthr-uiのArticle, Aside, Nav, Sectionのいずれかの自動レベル計算に任せるよう、tag属性、unrecommendedTag属性を削除してください。
27
28
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-heading-in-sectioning-content
28
- - tag属性を指定することで意図しないレベルに固定されてしまう可能性があります。`
29
+ - tag属性、unrecommendedTag属性を指定することで意図しないレベルに固定されてしまう可能性があります。`
29
30
 
30
31
  const VariableDeclaratorBareToSHR = (context, node) => {
31
32
  if (!node.init) {
@@ -177,7 +178,7 @@ const forInSearchChildren = (ary) => {
177
178
  return r
178
179
  }
179
180
 
180
- const findTagAttr = (a) => a.name?.name == 'tag'
181
+ const findTagAttr = (a) => tagAttrRegex.test(a.name?.name)
181
182
 
182
183
  /**
183
184
  * @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
@@ -70,14 +70,14 @@ module.exports = {
70
70
  const targetNameProp = `[name.name=${interactiveComponentRegex}]`
71
71
 
72
72
  return {
73
- [`JSXOpeningElement${targetNameProp}>JSXAttribute[name.name="role"]${NOT_ARROW_ROLE_ATTRIBUTES}`]: (node) => {
73
+ [`JSXOpeningElement${targetNameProp} > JSXAttribute[name.name="role"]${NOT_ARROW_ROLE_ATTRIBUTES}`]: (node) => {
74
74
  context.report({
75
75
  node: node.parent,
76
76
  message: `${node.parent.name.name}にrole属性は指定しないでください。
77
77
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-interactive-element`,
78
78
  });
79
79
  },
80
- [`JSXOpeningElement>${AS_FORM_PART_ATTRIBUTE}`]: (node) => {
80
+ [`JSXOpeningElement > ${AS_FORM_PART_ATTRIBUTE}`]: (node) => {
81
81
  if (node.parent.attributes.some((a) => a.type === 'JSXAttribute' && a.name?.name === 'role')) {
82
82
  context.report({
83
83
  node: node.parent,
@@ -86,7 +86,7 @@ module.exports = {
86
86
  });
87
87
  }
88
88
  },
89
- [`JSXOpeningElement:not(${targetNameProp}):not(:has(${AS_FORM_PART_ATTRIBUTE}))>JSXAttribute[name.name=${INTERACTIVE_ON_REGEX}]:not([value.expression.name=${DELEGATE_REGEX}])`]: (node) => {
89
+ [`JSXOpeningElement:not(${targetNameProp}):not(:has(${AS_FORM_PART_ATTRIBUTE})) > JSXAttribute[name.name=${INTERACTIVE_ON_REGEX}]:not([value.expression.name=${DELEGATE_REGEX}])`]: (node) => {
90
90
  switch (node.value.expression.type) {
91
91
  case 'MemberExpression':
92
92
  if (DELEGATE_REGEX.test(context.sourceCode.getText(node.expression))) {
@@ -185,7 +185,7 @@ module.exports = {
185
185
  message: `Fieldsetのlegend属性にアイコンを設定する場合 <Fieldset legend={{ text: 'テキスト', icon: <XxxIcon /> }} /> のようにlegend.icon属性を利用してください${DETAIL_LINK_MESSAGE}`,
186
186
  })
187
187
  },
188
- [`JSXElement:has(>JSXOpeningElement[name.name=/RadioButton(Panel)?$/]) ${LAYOUT_ELEMENT_NOT_SPAN}`]: (node) => {
188
+ [`JSXElement[openingElement.name.name=/RadioButton(Panel)?$/] ${LAYOUT_ELEMENT_NOT_SPAN}`]: (node) => {
189
189
  const component = node.name.name.match(LAYOUT_COMPONENT_REGEX)[1]
190
190
 
191
191
  context.report({
@@ -193,7 +193,7 @@ module.exports = {
193
193
  message: `RadioButton, RadioButtonPanelの子孫に${component}を置く場合、as属性、もしくはforwardedAs属性に \`span\` を指定してください${DETAIL_LINK_MESSAGE}`,
194
194
  })
195
195
  },
196
- [`JSXElement:has(>JSXOpeningElement[name.name=/Checkbox?$/]) ${LAYOUT_ELEMENT_NOT_SPAN}`]: (node) => {
196
+ [`JSXElement[openingElement.name.name=/Checkbox?$/] ${LAYOUT_ELEMENT_NOT_SPAN}`]: (node) => {
197
197
  const component = node.name.name.match(LAYOUT_COMPONENT_REGEX)[1]
198
198
 
199
199
  context.report({
@@ -47,10 +47,10 @@ module.exports = {
47
47
  message: `残余引数には ${REST_REGEX} とマッチする名称を指定してください${DETAIL_LINK}`,
48
48
  })
49
49
  },
50
- [`:not(:matches(RestElement,JSXSpreadAttribute,JSXSpreadAttribute>TSAsExpression,SpreadElement,SpreadElement>TSAsExpression,MemberExpression,VariableDeclarator,ArrayExpression,CallExpression,ObjectPattern>Property,ObjectExpression>Property,ReturnStatement,ArrowFunctionExpression))>Identifier[name=${REST_REGEX}]`]: actionNotRest,
51
- [`:matches(VariableDeclarator[id.name=${REST_REGEX}],ObjectPattern>Property[value.name=${REST_REGEX}],ObjectExpression>Property[key.name=${REST_REGEX}])`]: actionNotRest,
50
+ [`:not(:matches(RestElement,JSXSpreadAttribute,JSXSpreadAttribute > TSAsExpression,SpreadElement,SpreadElement > TSAsExpression,MemberExpression,VariableDeclarator,ArrayExpression,CallExpression,ObjectPattern > Property,ObjectExpression > Property,ReturnStatement,ArrowFunctionExpression)) > Identifier[name=${REST_REGEX}]`]: actionNotRest,
51
+ [`:matches(VariableDeclarator[id.name=${REST_REGEX}],ObjectPattern > Property[value.name=${REST_REGEX}],ObjectExpression > Property[key.name=${REST_REGEX}])`]: actionNotRest,
52
52
  [`MemberExpression[object.name=${REST_REGEX}]`]: actionMemberExpressionName,
53
- [`ArrowFunctionExpression>Identifier[name=${REST_REGEX}]`]: (node) => {
53
+ [`ArrowFunctionExpression > Identifier[name=${REST_REGEX}]`]: (node) => {
54
54
  if (node !== node.parent.body) {
55
55
  actionNotRest(node)
56
56
  }
@@ -10,7 +10,7 @@ module.exports = {
10
10
  },
11
11
  create(context) {
12
12
  return {
13
- ':matches(ArrowFunctionExpression,FunctionDeclaration,ReturnStatement)>JSXElement>JSXOpeningElement JSXAttribute[name.name="className"][value.value=/( |^)shr-m[trbl]?-/]': (node) => {
13
+ ':matches(ArrowFunctionExpression,FunctionDeclaration,ReturnStatement) > JSXElement > JSXOpeningElement JSXAttribute[name.name="className"][value.value=/( |^)shr-m[trbl]?-/]': (node) => {
14
14
  context.report({
15
15
  node,
16
16
  message: `コンポーネントのルート要素に外側への余白(margin)を設定しないでください。外側の余白は使用する側で制御するべきです。
@@ -1,6 +1,6 @@
1
1
  const SCHEMA = []
2
2
 
3
- const EARLY_RETURN_IF_STATEMENT = `:matches(ArrowFunctionExpression,FunctionExpression,FunctionDeclaration)>BlockStatement>IfStatement[alternate=null]:matches([consequent.type='ReturnStatement'],[consequent.body.length=1])>`
3
+ const EARLY_RETURN_IF_STATEMENT = `:matches(ArrowFunctionExpression,FunctionExpression,FunctionDeclaration) > BlockStatement > IfStatement[alternate=null]:matches([consequent.type='ReturnStatement'],[consequent.body.length=1]) > `
4
4
  const NULL_RETURN_STATEMENT = 'ReturnStatement[argument=null]'
5
5
 
6
6
  const FUNCTION_REGEX = /^(Arrow)?Function(Expression|Declaration)$/
@@ -84,7 +84,7 @@ module.exports = {
84
84
  }
85
85
 
86
86
  return {
87
- [`${EARLY_RETURN_IF_STATEMENT}BlockStatement>${NULL_RETURN_STATEMENT}`]: action,
87
+ [`${EARLY_RETURN_IF_STATEMENT}BlockStatement > ${NULL_RETURN_STATEMENT}`]: action,
88
88
  [`${EARLY_RETURN_IF_STATEMENT}${NULL_RETURN_STATEMENT}`]: action,
89
89
  }
90
90
  },
@@ -1,16 +1,5 @@
1
1
  const SCHEMA = []
2
2
 
3
- const searchBubbleUp = (node) => {
4
- switch (node.type) {
5
- case 'Program':
6
- case 'JSXAttribute':
7
- return null
8
- case 'TemplateLiteral':
9
- return node
10
- }
11
-
12
- return searchBubbleUp(node.parent)
13
- }
14
3
 
15
4
  /**
16
5
  * @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
@@ -23,20 +12,20 @@ module.exports = {
23
12
  },
24
13
  create(context) {
25
14
  const checker = (node) => {
26
- // HINT: TemplateLiteralがネストしている場合、親側だけチェックする
27
- if (!searchBubbleUp(node.parent)) {
28
- return context.report({
29
- node,
30
- message: `属性に設定している文字列から先頭、末尾の空白文字を削除してください
15
+ return context.report({
16
+ node,
17
+ message: `属性に設定している文字列から先頭、末尾の空白文字を削除してください
31
18
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/trim-props`,
32
- fix: (fixer) => fixer.replaceText(node, context.sourceCode.getText(node).replace(/^('|"|`)\s+/, '$1').replace(/\s+('|"|`)$/, '$1')),
33
- })
34
- }
19
+ fix: (fixer) => fixer.replaceText(node, context.sourceCode.getText(node).replace(/^('|"|`)\s+/, '$1').replace(/\s+('|"|`)$/, '$1')),
20
+ })
35
21
  }
36
22
 
37
23
  return {
38
- 'JSXAttribute Literal[value=/(^ | $)/]': checker,
39
- 'JSXAttribute TemplateLiteral:has(>TemplateElement:matches(:first-child[value.raw=/^ /],:last-child[value.raw=/ $/]))': checker,
24
+ 'JSXAttribute > Literal[value=/(^ | $)/]': checker,
25
+ 'JSXAttribute > JSXExpressionContainer>Literal[value=/(^ | $)/]': checker,
26
+ 'JSXAttribute > JSXExpressionContainer > TemplateLiteral > TemplateElement:matches(:first-child[value.raw=/^ /],:last-child[value.raw=/ $/])': (node) => {
27
+ checker(node.parent)
28
+ },
40
29
  }
41
30
  },
42
31
  }
@@ -20,9 +20,9 @@ const pageMessage = `smarthr-ui/PageHeading が同一ファイル内に複数存
20
20
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-heading-in-sectioning-content`
21
21
  const pageInSectionMessage = `smarthr-ui/PageHeadingはsmarthr-uiのArticle, Aside, Nav, Sectionで囲まないでください。囲んでしまうとページ全体の見出しではなくなってしまいます。
22
22
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-heading-in-sectioning-content`
23
- const noTagAttrMessage = `tag属性を指定せず、smarthr-uiのArticle, Aside, Nav, Sectionのいずれかの自動レベル計算に任せるよう、tag属性を削除してください。
23
+ const noTagAttrMessage = `tag属性、unrecommendedTag属性を指定せず、smarthr-uiのArticle, Aside, Nav, Sectionのいずれかの自動レベル計算に任せるよう、tag属性、unrecommendedTag属性を削除してください。
24
24
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-heading-in-sectioning-content
25
- - tag属性を指定することで意図しないレベルに固定されてしまう可能性があります。`
25
+ - tag属性、unrecommendedTag属性を指定することで意図しないレベルに固定されてしまう可能性があります。`
26
26
  const notHaveHeadingMessage = (elementName, isNav) => `${elementName} はHeading要素を含んでいません。
27
27
  - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-heading-in-sectioning-content
28
28
  - SectioningContentはHeadingを含むようにマークアップする必要があります
@@ -69,6 +69,7 @@ ruleTester.run('a11y-heading-in-sectioning-content', rule, {
69
69
  { code: '<Section><Heading>hoge</Heading><Heading>fuga</Heading></Section>', errors: [ { message: lowerMessage } ] },
70
70
  { code: '<Section><PageHeading>hoge</PageHeading></Section>', errors: [ { message: pageInSectionMessage } ] },
71
71
  { code: '<Section><Heading tag="h2">hoge</Heading></Section>', errors: [ { message: noTagAttrMessage } ] },
72
+ { code: '<Section><Heading unrecommendedTag="h2">hoge</Heading></Section>', errors: [ { message: noTagAttrMessage } ] },
72
73
  { code: '<Section></Section>', errors: [ { message: notHaveHeadingMessage('Section') } ] },
73
74
  { code: '<Aside><HogeSection></HogeSection></Aside>', errors: [ { message: notHaveHeadingMessage('Aside') }, { message: notHaveHeadingMessage('HogeSection') } ] },
74
75
  { code: '<Aside any="hoge"><HogeSection><Heading /></HogeSection></Aside>', errors: [ { message: notHaveHeadingMessage('Aside') } ] },
@@ -89,12 +89,12 @@ ruleTester.run('trim-props', rule, {
89
89
  {
90
90
  code: '<div data-spec={` a${b} c `}>....</div>',
91
91
  output: '<div data-spec={`a${b} c`}>....</div>',
92
- errors: [{ message: ERROR_MESSAGE }],
92
+ errors: [{ message: ERROR_MESSAGE }, { message: ERROR_MESSAGE }],
93
93
  },
94
94
  {
95
95
  code: '<div data-spec={` a${b ? ` ${c} ` : " "} d `}>....</div>',
96
96
  output: '<div data-spec={`a${b ? ` ${c} ` : " "} d`}>....</div>',
97
- errors: [{ message: ERROR_MESSAGE }],
97
+ errors: [{ message: ERROR_MESSAGE }, { message: ERROR_MESSAGE }],
98
98
  },
99
99
  ],
100
100
  })