eslint-plugin-smarthr 2.3.0 → 2.4.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 +13 -0
- package/package.json +2 -2
- package/rules/a11y-prohibit-checkbox-or-radio-in-table-cell/index.js +3 -22
- package/rules/component-name/index.js +130 -2
- package/rules/trim-props/index.js +22 -11
- package/test/a11y-prohibit-checkbox-or-radio-in-table-cell.js +14 -4
- package/test/component-name.js +4 -0
- package/test/trim-props.js +6 -0
- package/libs/format_styled_components.js +0 -126
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [2.4.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v2.3.0...eslint-plugin-smarthr-v2.4.0) (2025-12-02)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* Modalというコンポーネントの作成を禁止し、Dialogという名称に統一させる ([#915](https://github.com/kufu/tamatebako/issues/915)) ([208c2a0](https://github.com/kufu/tamatebako/commit/208c2a06034bcf24bfe1a3dd0d8633b0c546db11))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* a11y-prohibit-checkbox-or-radio-in-table-cellのautofixを削除 & childrenを持つ場合に警告されないよう修正 ([#914](https://github.com/kufu/tamatebako/issues/914)) ([3962fda](https://github.com/kufu/tamatebako/commit/3962fda01a1966c9624d5cd7056d807f86ccb5bc))
|
|
16
|
+
* trim-props でTemplateLiteralがネストしている場合のチェックを修正 ([#912](https://github.com/kufu/tamatebako/issues/912)) ([2d090c4](https://github.com/kufu/tamatebako/commit/2d090c4ef6d1ac4c9537fdb13a0198552456c10c))
|
|
17
|
+
|
|
5
18
|
## [2.3.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v2.2.1...eslint-plugin-smarthr-v2.3.0) (2025-12-01)
|
|
6
19
|
|
|
7
20
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-smarthr",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.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": "6b27e405cda722121437718a67b5886cb13ed534"
|
|
41
41
|
}
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
const findClosestThFromAncestor = (node) => {
|
|
2
|
-
if (node.type === 'JSXElement' && node.openingElement.name.name === 'Th') {
|
|
3
|
-
return node
|
|
4
|
-
}
|
|
5
|
-
if (node.parent) {
|
|
6
|
-
return findClosestThFromAncestor(node.parent)
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
1
|
/**
|
|
11
2
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
12
3
|
*/
|
|
@@ -20,10 +11,8 @@ module.exports = {
|
|
|
20
11
|
},
|
|
21
12
|
},
|
|
22
13
|
create(context) {
|
|
23
|
-
const sourceCode = context.sourceCode
|
|
24
|
-
|
|
25
14
|
return {
|
|
26
|
-
'JSXElement[openingElement.name.name=/Td$/] JSXElement[openingElement.name.name=/Check(b|B)ox$/]': (node) => {
|
|
15
|
+
'JSXElement[openingElement.name.name=/Td$/] JSXElement[openingElement.name.name=/Check(b|B)ox$/][children.length=0]': (node) => {
|
|
27
16
|
context.report({
|
|
28
17
|
node,
|
|
29
18
|
messageId: 'default',
|
|
@@ -34,7 +23,7 @@ module.exports = {
|
|
|
34
23
|
},
|
|
35
24
|
})
|
|
36
25
|
},
|
|
37
|
-
'JSXElement[openingElement.name.name=/Td$/] JSXElement[openingElement.name.name=/RadioButton$/]': (node) => {
|
|
26
|
+
'JSXElement[openingElement.name.name=/Td$/] JSXElement[openingElement.name.name=/RadioButton$/][children.length=0]': (node) => {
|
|
38
27
|
context.report({
|
|
39
28
|
node,
|
|
40
29
|
messageId: 'default',
|
|
@@ -45,7 +34,7 @@ module.exports = {
|
|
|
45
34
|
},
|
|
46
35
|
})
|
|
47
36
|
},
|
|
48
|
-
'JSXElement[openingElement.name.name=/Th$/] JSXElement[openingElement.name.name=/Check(b|B)ox$/]': (node) => {
|
|
37
|
+
'JSXElement[openingElement.name.name=/Th$/] JSXElement[openingElement.name.name=/Check(b|B)ox$/][children.length=0]': (node) => {
|
|
49
38
|
context.report({
|
|
50
39
|
node,
|
|
51
40
|
messageId: 'default',
|
|
@@ -54,14 +43,6 @@ module.exports = {
|
|
|
54
43
|
component: 'Checkbox',
|
|
55
44
|
preferred: 'ThCheckbox',
|
|
56
45
|
},
|
|
57
|
-
*fix(fixer) {
|
|
58
|
-
const th = findClosestThFromAncestor(node)
|
|
59
|
-
if (th) {
|
|
60
|
-
const thCheckbox = sourceCode.getText(node).replace(/<Check(b|B)ox/, '<ThCheckbox')
|
|
61
|
-
yield fixer.insertTextAfter(th, thCheckbox)
|
|
62
|
-
yield fixer.remove(th)
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
46
|
})
|
|
66
47
|
},
|
|
67
48
|
}
|
|
@@ -1,4 +1,60 @@
|
|
|
1
|
-
const
|
|
1
|
+
const STYLED_COMPONENTS_METHOD = 'styled'
|
|
2
|
+
const STYLED_COMPONENTS = `${STYLED_COMPONENTS_METHOD}-components`
|
|
3
|
+
|
|
4
|
+
const findInvalidImportNameNode = (s) => s.type === 'ImportDefaultSpecifier' && s.local.name !== STYLED_COMPONENTS_METHOD
|
|
5
|
+
|
|
6
|
+
const checkImportStyledComponents = (node, context) => {
|
|
7
|
+
if (node.source.value !== STYLED_COMPONENTS) {
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const invalidNameNode = node.specifiers.find(findInvalidImportNameNode)
|
|
12
|
+
|
|
13
|
+
if (invalidNameNode) {
|
|
14
|
+
context.report({
|
|
15
|
+
node: invalidNameNode,
|
|
16
|
+
message: `${STYLED_COMPONENTS} をimportする際は、名称が"${STYLED_COMPONENTS_METHOD}" となるようにしてください。例: "import ${STYLED_COMPONENTS_METHOD} from '${STYLED_COMPONENTS}'"`,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const getStyledComponentBaseName = (node) => {
|
|
22
|
+
let base = null
|
|
23
|
+
|
|
24
|
+
if (!node.init) {
|
|
25
|
+
return base
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const tag = node.init.tag || node.init
|
|
29
|
+
|
|
30
|
+
if (tag.object?.name === STYLED_COMPONENTS_METHOD) {
|
|
31
|
+
base = tag.property.name
|
|
32
|
+
} else if (tag.callee) {
|
|
33
|
+
const callee = tag.callee
|
|
34
|
+
|
|
35
|
+
switch (STYLED_COMPONENTS_METHOD) {
|
|
36
|
+
case callee.name: {
|
|
37
|
+
const arg = tag.arguments[0]
|
|
38
|
+
base = arg.name || arg.value
|
|
39
|
+
break
|
|
40
|
+
}
|
|
41
|
+
case callee.callee?.name: {
|
|
42
|
+
const arg = callee.arguments[0]
|
|
43
|
+
base = arg.name || arg.value
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
case callee.object?.name:
|
|
47
|
+
base = callee.property.name
|
|
48
|
+
break
|
|
49
|
+
case callee.object?.callee?.name:
|
|
50
|
+
const arg = callee.object.arguments[0]
|
|
51
|
+
base = arg.name || arg.value
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return base
|
|
57
|
+
}
|
|
2
58
|
|
|
3
59
|
const EXPECTED_NAMES = {
|
|
4
60
|
'(A|^a)rticle$': 'Article$',
|
|
@@ -134,7 +190,79 @@ module.exports = {
|
|
|
134
190
|
schema: SCHEMA,
|
|
135
191
|
},
|
|
136
192
|
create(context) {
|
|
137
|
-
|
|
193
|
+
const entriesesTagNames = Object.entries(EXPECTED_NAMES).map(([b, e]) => [ new RegExp(b), new RegExp(e) ])
|
|
194
|
+
const entriesesUnTagNames = UNEXPECTED_NAMES ? Object.entries(UNEXPECTED_NAMES).map(([b, e]) => {
|
|
195
|
+
const [ auctualE, messageTemplate ] = Array.isArray(e) ? e : [e, '']
|
|
196
|
+
|
|
197
|
+
return [ new RegExp(b), new RegExp(auctualE), messageTemplate ]
|
|
198
|
+
}) : []
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
const checkImportedNameToLocalName = (node, base, extended, isImport) => {
|
|
202
|
+
entriesesTagNames.forEach(([b, e]) => {
|
|
203
|
+
if (base.match(b) && !extended.match(e)) {
|
|
204
|
+
context.report({
|
|
205
|
+
node,
|
|
206
|
+
message: `${extended}を正規表現 "${e.toString()}" がmatchする名称に変更してください。${isImport ? `
|
|
207
|
+
- ${base}が型の場合、'import type { ${base} as ${extended} }' もしくは 'import { type ${base} as ${extended} }' のように明示的に型であることを宣言してください。名称変更が不要になります` : ''}`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
ImportDeclaration: (node) => {
|
|
215
|
+
checkImportStyledComponents(node, context)
|
|
216
|
+
|
|
217
|
+
if (node.importKind !== 'type') {
|
|
218
|
+
node.specifiers.forEach((s) => {
|
|
219
|
+
if (s.importKind !== 'type' && s.imported && s.imported.name !== s.local.name) {
|
|
220
|
+
checkImportedNameToLocalName(node, s.imported.name, s.local.name, true)
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
VariableDeclarator: (node) => {
|
|
226
|
+
const base = getStyledComponentBaseName(node)
|
|
227
|
+
|
|
228
|
+
if (base) {
|
|
229
|
+
const extended = node.id.name
|
|
230
|
+
|
|
231
|
+
checkImportedNameToLocalName(node, base, extended)
|
|
232
|
+
|
|
233
|
+
entriesesUnTagNames.forEach(([b, e, m]) => {
|
|
234
|
+
const matcher = extended.match(e)
|
|
235
|
+
|
|
236
|
+
if (matcher && !base.match(b)) {
|
|
237
|
+
const expected = matcher[1]
|
|
238
|
+
const isBareTag = base === base.toLowerCase()
|
|
239
|
+
const sampleFixBase = `styled${isBareTag ? `.${base}` : `(${base})`}`
|
|
240
|
+
|
|
241
|
+
context.report({
|
|
242
|
+
node,
|
|
243
|
+
message: m ? m
|
|
244
|
+
.replaceAll('{{extended}}', extended)
|
|
245
|
+
.replaceAll('{{expected}}', expected) : `${extended} は ${b.toString()} にmatchする名前のコンポーネントを拡張することを期待している名称になっています
|
|
246
|
+
- ${extended} の名称の末尾が"${expected}" という文字列ではない状態にしつつ、"${base}"を継承していることをわかる名称に変更してください
|
|
247
|
+
- もしくは"${base}"を"${extended}"の継承元であることがわかるような${isBareTag ? '適切なタグや別コンポーネントに差し替えてください' : '名称に変更するか、適切な別コンポーネントに差し替えてください'}
|
|
248
|
+
- 修正例1: const ${extended.replace(expected, '')}Xxxx = ${sampleFixBase}
|
|
249
|
+
- 修正例2: const ${extended}Xxxx = ${sampleFixBase}
|
|
250
|
+
- 修正例3: const ${extended} = styled(Xxxx${expected})`
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
'VariableDeclarator[id.name=/Modal/]': (node) => {
|
|
257
|
+
context.report({
|
|
258
|
+
node,
|
|
259
|
+
message: `コンポーネント名や変数名に"Modal"という名称は使わず、"Dialog"に統一してください
|
|
260
|
+
- Modalとは形容詞であり、かつ"現在の操作から切り離して専用の操作を行わせる" という意味合いを持ちます
|
|
261
|
+
- そのためDialogでなければ正しくない場合がありえます(smarthr-ui/ModelessDialogのように元々の操作も行えるDialogなどが該当)
|
|
262
|
+
- DialogはModalなダイアログ、Modelessなダイアログすべてを含有した名称のため、統一することを推奨しています`
|
|
263
|
+
})
|
|
264
|
+
},
|
|
265
|
+
}
|
|
138
266
|
},
|
|
139
267
|
}
|
|
140
268
|
module.exports.schema = SCHEMA
|
|
@@ -1,5 +1,17 @@
|
|
|
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
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
5
17
|
*/
|
|
@@ -10,21 +22,20 @@ module.exports = {
|
|
|
10
22
|
fixable: 'whitespace',
|
|
11
23
|
},
|
|
12
24
|
create(context) {
|
|
13
|
-
|
|
14
|
-
|
|
25
|
+
const checker = (node) => {
|
|
26
|
+
// HINT: TemplateLiteralがネストしている場合、親側だけチェックする
|
|
27
|
+
if (!searchBubbleUp(node.parent)) {
|
|
15
28
|
return context.report({
|
|
16
29
|
node,
|
|
17
30
|
message: '属性に設定している文字列から先頭、末尾の空白文字を削除してください',
|
|
18
|
-
fix: (fixer) => fixer.replaceText(node, context.sourceCode.getText(node).replace(/^('|")\s+/, '$1').replace(/\s+('|")$/, '$1')),
|
|
31
|
+
fix: (fixer) => fixer.replaceText(node, context.sourceCode.getText(node).replace(/^('|"|`)\s+/, '$1').replace(/\s+('|"|`)$/, '$1')),
|
|
19
32
|
})
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
})
|
|
27
|
-
},
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
'JSXAttribute Literal[value=/(^ | $)/]': checker,
|
|
38
|
+
'JSXAttribute TemplateLiteral:has(>TemplateElement:matches(:first-child[value.raw=/^ /],:last-child[value.raw=/ $/]))': checker,
|
|
28
39
|
}
|
|
29
40
|
},
|
|
30
41
|
}
|
|
@@ -13,7 +13,20 @@ const ruleTester = new RuleTester({
|
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
ruleTester.run('a11y-prohibit-checkbox-or-radio-in-table-cell', rule, {
|
|
16
|
-
valid: [
|
|
16
|
+
valid: [
|
|
17
|
+
'<TdCheckbox />',
|
|
18
|
+
'<ThCheckbox />',
|
|
19
|
+
'<TdRadioButton />',
|
|
20
|
+
'<Td>hello</Td>',
|
|
21
|
+
'<Th>hello</Th>',
|
|
22
|
+
`
|
|
23
|
+
<Td>
|
|
24
|
+
<Checkbox>
|
|
25
|
+
可視ラベル
|
|
26
|
+
</Checkbox>
|
|
27
|
+
</Td>
|
|
28
|
+
`,
|
|
29
|
+
],
|
|
17
30
|
invalid: [
|
|
18
31
|
{
|
|
19
32
|
code: `<Td><Checkbox /></Td>`,
|
|
@@ -21,12 +34,10 @@ ruleTester.run('a11y-prohibit-checkbox-or-radio-in-table-cell', rule, {
|
|
|
21
34
|
},
|
|
22
35
|
{
|
|
23
36
|
code: `<Th><Checkbox /></Th>`,
|
|
24
|
-
output: `<ThCheckbox />`,
|
|
25
37
|
errors: [{ message: 'Th の子孫に Checkbox を置くことはできません。代わりに ThCheckbox を使用してください。' }],
|
|
26
38
|
},
|
|
27
39
|
{
|
|
28
40
|
code: `<Th><Checkbox id="my-checkbox" name="agree" error /></Th>`,
|
|
29
|
-
output: `<ThCheckbox id="my-checkbox" name="agree" error />`,
|
|
30
41
|
errors: [{ message: 'Th の子孫に Checkbox を置くことはできません。代わりに ThCheckbox を使用してください。' }],
|
|
31
42
|
},
|
|
32
43
|
{
|
|
@@ -49,7 +60,6 @@ ruleTester.run('a11y-prohibit-checkbox-or-radio-in-table-cell', rule, {
|
|
|
49
60
|
},
|
|
50
61
|
{
|
|
51
62
|
code: `<CustomTh><CustomCheckbox /></CustomTh>`,
|
|
52
|
-
output: null,
|
|
53
63
|
errors: [{ message: 'Th の子孫に Checkbox を置くことはできません。代わりに ThCheckbox を使用してください。' }],
|
|
54
64
|
},
|
|
55
65
|
{
|
package/test/component-name.js
CHANGED
|
@@ -243,5 +243,9 @@ ruleTester.run('component-name', rule, {
|
|
|
243
243
|
|
|
244
244
|
{ code: 'const Hoge = styled(RemoteDialogTrigger)``', errors: [ { message: messageInheritance({ extended: 'Hoge', matcher: /DialogTrigger$/ }) }, { message: messageInheritance({ extended: 'Hoge', matcher: /RemoteDialogTrigger$/ }) } ] },
|
|
245
245
|
{ code: 'const Fuga = styled(RemoteTriggerActionDialog)``', errors: [ { message: messageInheritance({ extended: 'Fuga', matcher: /RemoteTrigger(.+)Dialog$/ }) } ] },
|
|
246
|
+
{ code: 'const HogeModalFuga = any', errors: [ { message: `コンポーネント名や変数名に"Modal"という名称は使わず、"Dialog"に統一してください
|
|
247
|
+
- Modalとは形容詞であり、かつ"現在の操作から切り離して専用の操作を行わせる" という意味合いを持ちます
|
|
248
|
+
- そのためDialogでなければ正しくない場合がありえます(smarthr-ui/ModelessDialogのように元々の操作も行えるDialogなどが該当)
|
|
249
|
+
- DialogはModalなダイアログ、Modelessなダイアログすべてを含有した名称のため、統一することを推奨しています` } ] },
|
|
246
250
|
]
|
|
247
251
|
})
|
package/test/trim-props.js
CHANGED
|
@@ -19,6 +19,7 @@ ruleTester.run('trim-props', rule, {
|
|
|
19
19
|
{ code: `<img src={'/sample.jpg'} alt={'sample'} />` },
|
|
20
20
|
{ code: '<div data-spec="info-area">....</div>' },
|
|
21
21
|
{ code: '<div data-spec={`a${b} c`}>....</div>' },
|
|
22
|
+
{ code: '<div data-spec={`a${b ? ` ${c} ` : " "} d`}>....</div>' },
|
|
22
23
|
],
|
|
23
24
|
invalid: [
|
|
24
25
|
{
|
|
@@ -87,5 +88,10 @@ ruleTester.run('trim-props', rule, {
|
|
|
87
88
|
output: '<div data-spec={`a${b} c`}>....</div>',
|
|
88
89
|
errors: [{ message: '属性に設定している文字列から先頭、末尾の空白文字を削除してください' }],
|
|
89
90
|
},
|
|
91
|
+
{
|
|
92
|
+
code: '<div data-spec={` a${b ? ` ${c} ` : " "} d `}>....</div>',
|
|
93
|
+
output: '<div data-spec={`a${b ? ` ${c} ` : " "} d`}>....</div>',
|
|
94
|
+
errors: [{ message: '属性に設定している文字列から先頭、末尾の空白文字を削除してください' }],
|
|
95
|
+
},
|
|
90
96
|
],
|
|
91
97
|
})
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
const STYLED_COMPONENTS_METHOD = 'styled'
|
|
2
|
-
const STYLED_COMPONENTS = `${STYLED_COMPONENTS_METHOD}-components`
|
|
3
|
-
|
|
4
|
-
const findInvalidImportNameNode = (s) => s.type === 'ImportDefaultSpecifier' && s.local.name !== STYLED_COMPONENTS_METHOD
|
|
5
|
-
|
|
6
|
-
const checkImportStyledComponents = (node, context) => {
|
|
7
|
-
if (node.source.value !== STYLED_COMPONENTS) {
|
|
8
|
-
return
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const invalidNameNode = node.specifiers.find(findInvalidImportNameNode)
|
|
12
|
-
|
|
13
|
-
if (invalidNameNode) {
|
|
14
|
-
context.report({
|
|
15
|
-
node: invalidNameNode,
|
|
16
|
-
message: `${STYLED_COMPONENTS} をimportする際は、名称が"${STYLED_COMPONENTS_METHOD}" となるようにしてください。例: "import ${STYLED_COMPONENTS_METHOD} from '${STYLED_COMPONENTS}'"`,
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const getStyledComponentBaseName = (node) => {
|
|
22
|
-
let base = null
|
|
23
|
-
|
|
24
|
-
if (!node.init) {
|
|
25
|
-
return base
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const tag = node.init.tag || node.init
|
|
29
|
-
|
|
30
|
-
if (tag.object?.name === STYLED_COMPONENTS_METHOD) {
|
|
31
|
-
base = tag.property.name
|
|
32
|
-
} else if (tag.callee) {
|
|
33
|
-
const callee = tag.callee
|
|
34
|
-
|
|
35
|
-
switch (STYLED_COMPONENTS_METHOD) {
|
|
36
|
-
case callee.name: {
|
|
37
|
-
const arg = tag.arguments[0]
|
|
38
|
-
base = arg.name || arg.value
|
|
39
|
-
break
|
|
40
|
-
}
|
|
41
|
-
case callee.callee?.name: {
|
|
42
|
-
const arg = callee.arguments[0]
|
|
43
|
-
base = arg.name || arg.value
|
|
44
|
-
break
|
|
45
|
-
}
|
|
46
|
-
case callee.object?.name:
|
|
47
|
-
base = callee.property.name
|
|
48
|
-
break
|
|
49
|
-
case callee.object?.callee?.name:
|
|
50
|
-
const arg = callee.object.arguments[0]
|
|
51
|
-
base = arg.name || arg.value
|
|
52
|
-
break
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return base
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const generateTagFormatter = ({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }) => {
|
|
60
|
-
const entriesesTagNames = Object.entries(EXPECTED_NAMES).map(([b, e]) => [ new RegExp(b), new RegExp(e) ])
|
|
61
|
-
const entriesesUnTagNames = UNEXPECTED_NAMES ? Object.entries(UNEXPECTED_NAMES).map(([b, e]) => {
|
|
62
|
-
const [ auctualE, messageTemplate ] = Array.isArray(e) ? e : [e, '']
|
|
63
|
-
|
|
64
|
-
return [ new RegExp(b), new RegExp(auctualE), messageTemplate ]
|
|
65
|
-
}) : []
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const checkImportedNameToLocalName = (node, base, extended, isImport) => {
|
|
69
|
-
entriesesTagNames.forEach(([b, e]) => {
|
|
70
|
-
if (base.match(b) && !extended.match(e)) {
|
|
71
|
-
context.report({
|
|
72
|
-
node,
|
|
73
|
-
message: `${extended}を正規表現 "${e.toString()}" がmatchする名称に変更してください。${isImport ? `
|
|
74
|
-
- ${base}が型の場合、'import type { ${base} as ${extended} }' もしくは 'import { type ${base} as ${extended} }' のように明示的に型であることを宣言してください。名称変更が不要になります` : ''}`,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
ImportDeclaration: (node) => {
|
|
82
|
-
checkImportStyledComponents(node, context)
|
|
83
|
-
|
|
84
|
-
if (node.importKind !== 'type') {
|
|
85
|
-
node.specifiers.forEach((s) => {
|
|
86
|
-
if (s.importKind !== 'type' && s.imported && s.imported.name !== s.local.name) {
|
|
87
|
-
checkImportedNameToLocalName(node, s.imported.name, s.local.name, true)
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
VariableDeclarator: (node) => {
|
|
93
|
-
const base = getStyledComponentBaseName(node)
|
|
94
|
-
|
|
95
|
-
if (base) {
|
|
96
|
-
const extended = node.id.name
|
|
97
|
-
|
|
98
|
-
checkImportedNameToLocalName(node, base, extended)
|
|
99
|
-
|
|
100
|
-
entriesesUnTagNames.forEach(([b, e, m]) => {
|
|
101
|
-
const matcher = extended.match(e)
|
|
102
|
-
|
|
103
|
-
if (matcher && !base.match(b)) {
|
|
104
|
-
const expected = matcher[1]
|
|
105
|
-
const isBareTag = base === base.toLowerCase()
|
|
106
|
-
const sampleFixBase = `styled${isBareTag ? `.${base}` : `(${base})`}`
|
|
107
|
-
|
|
108
|
-
context.report({
|
|
109
|
-
node,
|
|
110
|
-
message: m ? m
|
|
111
|
-
.replaceAll('{{extended}}', extended)
|
|
112
|
-
.replaceAll('{{expected}}', expected) : `${extended} は ${b.toString()} にmatchする名前のコンポーネントを拡張することを期待している名称になっています
|
|
113
|
-
- ${extended} の名称の末尾が"${expected}" という文字列ではない状態にしつつ、"${base}"を継承していることをわかる名称に変更してください
|
|
114
|
-
- もしくは"${base}"を"${extended}"の継承元であることがわかるような${isBareTag ? '適切なタグや別コンポーネントに差し替えてください' : '名称に変更するか、適切な別コンポーネントに差し替えてください'}
|
|
115
|
-
- 修正例1: const ${extended.replace(expected, '')}Xxxx = ${sampleFixBase}
|
|
116
|
-
- 修正例2: const ${extended}Xxxx = ${sampleFixBase}
|
|
117
|
-
- 修正例3: const ${extended} = styled(Xxxx${expected})`
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
|
-
})
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
module.exports = { generateTagFormatter }
|