eslint-plugin-smarthr 1.4.2 → 1.5.1
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 +14 -0
- package/libs/common_domain.js +5 -8
- package/libs/util.js +4 -0
- package/package.json +4 -4
- package/rules/a11y-anchor-has-href-attribute/index.js +31 -20
- package/rules/a11y-clickable-element-has-text/index.js +4 -25
- package/rules/a11y-delegate-element-has-role-presentation/index.js +34 -55
- package/rules/a11y-form-control-in-form/index.js +5 -31
- package/rules/a11y-heading-in-sectioning-content/index.js +7 -63
- package/rules/a11y-image-has-alt-attribute/index.js +0 -16
- package/rules/a11y-input-has-name-attribute/index.js +15 -33
- package/rules/a11y-input-in-form-control/index.js +28 -74
- package/rules/a11y-numbered-text-within-ol/index.js +2 -11
- package/rules/a11y-prohibit-input-maxlength-attribute/index.js +8 -20
- package/rules/a11y-prohibit-input-placeholder/index.js +2 -14
- package/rules/a11y-prohibit-sectioning-content-in-form/index.js +5 -37
- package/rules/a11y-prohibit-useless-sectioning-fragment/index.js +5 -23
- package/rules/a11y-required-layout-as-attribute/index.js +0 -25
- package/rules/a11y-trigger-has-button/index.js +10 -18
- package/rules/best-practice-for-data-test-attribute/index.js +3 -6
- package/rules/best-practice-for-layouts/index.js +2 -16
- package/rules/best-practice-for-remote-trigger-dialog/index.js +3 -11
- package/rules/best-practice-for-tailwind-prohibit-root-margin/index.js +38 -30
- package/rules/best-practice-for-tailwind-variants/index.js +10 -18
- package/rules/component-name/README.md +44 -0
- package/rules/component-name/index.js +139 -0
- package/rules/design-system-guideline-prohibit-double-icons/index.js +1 -11
- package/rules/format-import-path/index.js +14 -6
- package/rules/format-translate-component/index.js +3 -1
- package/rules/no-import-other-domain/index.js +8 -8
- package/rules/prohibit-file-name/index.js +1 -1
- package/rules/prohibit-import/index.js +21 -23
- package/rules/prohibit-path-within-template-literal/index.js +1 -1
- package/rules/require-barrel-import/index.js +8 -11
- package/rules/require-declaration/index.js +5 -3
- package/rules/require-export/index.js +34 -30
- package/rules/require-import/index.js +10 -10
- package/rules/trim-props/index.js +9 -8
- package/test/a11y-anchor-has-href-attribute.js +0 -29
- package/test/a11y-clickable-element-has-text.js +0 -66
- package/test/{a11y-delegate-element-has-role-presantation.js → a11y-delegate-element-has-role-presentation.js} +3 -2
- package/test/a11y-form-control-in-form.js +1 -1
- package/test/a11y-heading-in-sectioning-content.js +0 -82
- package/test/a11y-image-has-alt-attribute.js +0 -45
- package/test/a11y-input-has-name-attribute.js +0 -44
- package/test/a11y-input-in-form-control.js +9 -33
- package/test/a11y-prohhibit-input-placeholder.js +0 -45
- package/test/a11y-trigger-has-button.js +0 -42
- package/test/best-practice-for-remote-trigger-dialog.js +0 -6
- package/test/component-name.js +247 -0
- package/test/prohibit-import.js +13 -13
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
const SCHEMA = []
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
"data-spec",
|
|
5
|
-
"data-testid"
|
|
6
|
-
]
|
|
3
|
+
const PROHIBIT_ATTR_REGEX = /^(data-(spec|testid))$/
|
|
7
4
|
|
|
8
5
|
/**
|
|
9
6
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
@@ -16,12 +13,12 @@ module.exports = {
|
|
|
16
13
|
create(context) {
|
|
17
14
|
return {
|
|
18
15
|
JSXAttribute: (node) => {
|
|
19
|
-
const hit =
|
|
16
|
+
const hit = node.name.name.match(PROHIBIT_ATTR_REGEX)
|
|
20
17
|
|
|
21
18
|
if (hit) {
|
|
22
19
|
context.report({
|
|
23
20
|
node,
|
|
24
|
-
message: `テストのために要素を指定するために、${hit} 属性を利用するのではなく、他の方法で要素を指定することを検討してください。
|
|
21
|
+
message: `テストのために要素を指定するために、${hit[1]} 属性を利用するのではなく、他の方法で要素を指定することを検討してください。
|
|
25
22
|
- 方法1: click_link, click_button等を利用したりすることで、利用しているテスト環境に準じた方法で要素を指定することを検討してください。
|
|
26
23
|
- 参考(Testing Library): https://testing-library.com/docs/queries/about
|
|
27
24
|
- 参考(Capybara): https://rubydoc.info/github/jnicklas/capybara/Capybara/Node/Finders
|
|
@@ -1,25 +1,12 @@
|
|
|
1
|
-
const
|
|
1
|
+
const MULTI_CHILDREN_REGEX = /(Cluster|Stack)$/
|
|
2
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
3
|
const REGEX_NLSP = /^\s*\n+\s*$/
|
|
17
4
|
const FLEX_END_REGEX = /^(flex-)?end$/
|
|
18
5
|
|
|
19
6
|
const filterFalsyJSXText = (cs) => cs.filter(checkFalsyJSXText)
|
|
20
7
|
const checkFalsyJSXText = (c) => (
|
|
21
8
|
!(
|
|
22
|
-
c.type === 'JSXText' && c.value
|
|
9
|
+
c.type === 'JSXText' && REGEX_NLSP.test(c.value) ||
|
|
23
10
|
c.type === 'JSXEmptyExpression'
|
|
24
11
|
)
|
|
25
12
|
)
|
|
@@ -63,7 +50,6 @@ module.exports = {
|
|
|
63
50
|
},
|
|
64
51
|
create(context) {
|
|
65
52
|
return {
|
|
66
|
-
...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
|
|
67
53
|
JSXOpeningElement: (node) => {
|
|
68
54
|
const nodeName = node.name.name;
|
|
69
55
|
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const EXPECTED_NAMES = {
|
|
4
|
-
'RemoteDialogTrigger$': 'RemoteDialogTrigger$',
|
|
5
|
-
'RemoteTrigger(.+)Dialog$': 'RemoteTrigger(.+)Dialog$',
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const REGEX_REMOTE_TRIGGER_DIALOG = /RemoteTrigger(Action|Message|Modeless)Dialog$/
|
|
1
|
+
const REGEX_REMOTE_TRIGGER_DIALOG = /RemoteTrigger(Action|Form|Message|Modeless)Dialog$/
|
|
9
2
|
const REGEX_REMOTE_DIALOG_TRIGGER = /RemoteDialogTrigger$/
|
|
10
3
|
|
|
11
4
|
/**
|
|
@@ -18,13 +11,12 @@ module.exports = {
|
|
|
18
11
|
},
|
|
19
12
|
create(context) {
|
|
20
13
|
return {
|
|
21
|
-
...generateTagFormatter({ context, EXPECTED_NAMES }),
|
|
22
14
|
JSXOpeningElement: (node) => {
|
|
23
15
|
const nodeName = node.name.name || '';
|
|
24
16
|
|
|
25
|
-
const regexRemoteTriggerDialog =
|
|
17
|
+
const regexRemoteTriggerDialog = REGEX_REMOTE_TRIGGER_DIALOG.test(nodeName)
|
|
26
18
|
|
|
27
|
-
if (regexRemoteTriggerDialog ||
|
|
19
|
+
if (regexRemoteTriggerDialog || REGEX_REMOTE_DIALOG_TRIGGER.test(nodeName)) {
|
|
28
20
|
const attrName = regexRemoteTriggerDialog ? 'id' : 'targetId'
|
|
29
21
|
const id = node.attributes.find((a) => a.name?.name === attrName)
|
|
30
22
|
|
|
@@ -3,6 +3,8 @@ const { AST_NODE_TYPES } = require('@typescript-eslint/utils')
|
|
|
3
3
|
const SCHEMA = []
|
|
4
4
|
const MARGIN_CLASS_PATTERNS = /shr-m[trbl]?-/ // mt-, mr-, mb-, ml-, m-
|
|
5
5
|
|
|
6
|
+
const findClassNameAttr = (attr) => attr.type === AST_NODE_TYPES.JSXAttribute && attr.name.name === 'className'
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* コンポーネントのルート要素を渡し、該当の余白クラスが存在すればそれを、なければNULLを返す
|
|
8
10
|
* @param {import('@typescript-eslint/utils').TSESTree.Node} node
|
|
@@ -12,17 +14,18 @@ const findSpacingClassInRootElement = (node) => {
|
|
|
12
14
|
// JSX でなければ対象外
|
|
13
15
|
if (node.type !== AST_NODE_TYPES.JSXElement) return null
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
const classNameAttr = node.openingElement.attributes.find(
|
|
17
|
-
(attr) => attr.type === AST_NODE_TYPES.JSXAttribute && attr.name.name === 'className',
|
|
18
|
-
)
|
|
19
|
-
if (!classNameAttr) return null
|
|
17
|
+
const classNameAttr = node.openingElement.attributes.find(findClassNameAttr)
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
if (
|
|
20
|
+
classNameAttr &&
|
|
21
|
+
// className属性の値がリテラル、かつ余白クラスの場合
|
|
22
|
+
classNameAttr.value?.type === AST_NODE_TYPES.Literal && typeof classNameAttr.value.value === 'string' &&
|
|
23
|
+
MARGIN_CLASS_PATTERNS.test(classNameAttr.value.value)
|
|
24
|
+
) {
|
|
25
|
+
return classNameAttr.value
|
|
26
|
+
}
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
return MARGIN_CLASS_PATTERNS.test(classNameAttr.value.value) ? classNameAttr.value : null
|
|
28
|
+
return null
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -56,30 +59,37 @@ module.exports = {
|
|
|
56
59
|
* 関数本体をチェックし、ルート要素で余白クラスが設定されたJSXを返している場合、エラーを報告する
|
|
57
60
|
* @param {import('@typescript-eslint/utils').TSESTree.Node} body
|
|
58
61
|
*/
|
|
59
|
-
const checkFunctionBody = (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
messageId: 'noRootSpacing',
|
|
67
|
-
})
|
|
68
|
-
}
|
|
69
|
-
return
|
|
70
|
-
}
|
|
62
|
+
const checkFunctionBody = (n) => {
|
|
63
|
+
const body = n.body
|
|
64
|
+
|
|
65
|
+
switch (body.type) {
|
|
66
|
+
// 関数がブロックを持たずに直接JSXを返すパターン
|
|
67
|
+
case AST_NODE_TYPES.JSXElement: {
|
|
68
|
+
const spacingClass = findSpacingClassInRootElement(body)
|
|
71
69
|
|
|
72
|
-
// 関数がブロック内で JSX を return するパターン
|
|
73
|
-
if (body.type === AST_NODE_TYPES.BlockStatement) {
|
|
74
|
-
const returnStatement = findJSXReturnStatement(body)
|
|
75
|
-
if (returnStatement?.argument) {
|
|
76
|
-
const spacingClass = findSpacingClassInRootElement(returnStatement.argument)
|
|
77
70
|
if (spacingClass) {
|
|
78
71
|
context.report({
|
|
79
72
|
node: spacingClass,
|
|
80
73
|
messageId: 'noRootSpacing',
|
|
81
74
|
})
|
|
82
75
|
}
|
|
76
|
+
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
// 関数がブロック内で JSX を return するパターン
|
|
80
|
+
case AST_NODE_TYPES.BlockStatement: {
|
|
81
|
+
const returnStatement = findJSXReturnStatement(body)
|
|
82
|
+
if (returnStatement?.argument) {
|
|
83
|
+
const spacingClass = findSpacingClassInRootElement(returnStatement.argument)
|
|
84
|
+
if (spacingClass) {
|
|
85
|
+
context.report({
|
|
86
|
+
node: spacingClass,
|
|
87
|
+
messageId: 'noRootSpacing',
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
break
|
|
83
93
|
}
|
|
84
94
|
}
|
|
85
95
|
}
|
|
@@ -88,14 +98,12 @@ module.exports = {
|
|
|
88
98
|
// アロー関数式のコンポーネントをチェック
|
|
89
99
|
ArrowFunctionExpression: (node) => {
|
|
90
100
|
if (node.parent?.type === AST_NODE_TYPES.VariableDeclarator) {
|
|
91
|
-
checkFunctionBody(node
|
|
101
|
+
checkFunctionBody(node)
|
|
92
102
|
}
|
|
93
103
|
},
|
|
94
104
|
|
|
95
105
|
// function宣言のコンポーネントをチェック
|
|
96
|
-
FunctionDeclaration:
|
|
97
|
-
checkFunctionBody(node.body)
|
|
98
|
-
},
|
|
106
|
+
FunctionDeclaration: checkFunctionBody,
|
|
99
107
|
}
|
|
100
108
|
},
|
|
101
109
|
}
|
|
@@ -6,8 +6,6 @@ const TV_RESULT_CONST_NAME_REGEX = /(C|c)lassNameGenerator$/
|
|
|
6
6
|
|
|
7
7
|
const findValidImportNameNode = (s) => s.type === 'ImportSpecifier' && s.imported.name === TV_COMPONENTS_METHOD && s.local.name !== TV_COMPONENTS_METHOD
|
|
8
8
|
|
|
9
|
-
const checkImportTailwindVariants = (node, context) => {
|
|
10
|
-
}
|
|
11
9
|
const findNodeHasId = (node) => {
|
|
12
10
|
if (node.id) {
|
|
13
11
|
return node
|
|
@@ -42,13 +40,11 @@ module.exports = {
|
|
|
42
40
|
create(context) {
|
|
43
41
|
return {
|
|
44
42
|
ImportDeclaration: (node) => {
|
|
45
|
-
if (node.source.value === TV_COMPONENTS) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
});
|
|
51
|
-
}
|
|
43
|
+
if (node.source.value === TV_COMPONENTS && node.specifiers.some(findValidImportNameNode)) {
|
|
44
|
+
context.report({
|
|
45
|
+
node,
|
|
46
|
+
message: `${TV_COMPONENTS} をimportする際は、名称が"${TV_COMPONENTS_METHOD}" となるようにしてください。例: "import { ${TV_COMPONENTS_METHOD} } from '${TV_COMPONENTS}'"`,
|
|
47
|
+
});
|
|
52
48
|
}
|
|
53
49
|
},
|
|
54
50
|
CallExpression: (node) => {
|
|
@@ -61,15 +57,11 @@ module.exports = {
|
|
|
61
57
|
message: `${TV_COMPONENTS_METHOD}の実行結果を格納する変数名は "${idNode.id.name}" ではなく "${TV_RESULT_CONST_NAME_REGEX}"にmatchする名称に統一してください。`,
|
|
62
58
|
});
|
|
63
59
|
}
|
|
64
|
-
} else if (TV_RESULT_CONST_NAME_REGEX.test(node.callee.name)) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
node,
|
|
70
|
-
message: `"${node.callee.name}" を実行する際、useMemoでラップし、メモ化してください`,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
60
|
+
} else if (TV_RESULT_CONST_NAME_REGEX.test(node.callee.name) && !findNodeUseMemo(node.parent)) {
|
|
61
|
+
context.report({
|
|
62
|
+
node,
|
|
63
|
+
message: `"${node.callee.name}" を実行する際、useMemoでラップし、メモ化してください`,
|
|
64
|
+
});
|
|
73
65
|
}
|
|
74
66
|
},
|
|
75
67
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# smarthr/component-name
|
|
2
|
+
|
|
3
|
+
- styled-componentなどでコンポーネントを作成する際の命名規則を設定するルールです
|
|
4
|
+
- 特定のタグ、smarthr-uiが提供するコンポーネントなどを拡張する際、元の要素がなんであるか?がわかる名称になるようにチェックします
|
|
5
|
+
- a11y系ルールなどの前提になるチェックのため、基本的にoffにすることは推奨されません
|
|
6
|
+
|
|
7
|
+
## rules
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
{
|
|
11
|
+
rules: {
|
|
12
|
+
'smarthr/component-name': 'error', // 'warn', 'off',
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## ❌ Incorrect
|
|
18
|
+
|
|
19
|
+
```jsx
|
|
20
|
+
// import 時のasをチェック
|
|
21
|
+
import { HogeSelect as Fuga } from 'any'
|
|
22
|
+
|
|
23
|
+
// selectと勘違いしてしまうような名称はNG
|
|
24
|
+
const HogeSelect = styled.div
|
|
25
|
+
const HogeSelect = styled(Hoge)
|
|
26
|
+
|
|
27
|
+
// selectとわからないような名称はNG
|
|
28
|
+
const Hoge = styled.select
|
|
29
|
+
const Hoge = styled(FugaSelect)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## ✅ Correct
|
|
33
|
+
|
|
34
|
+
```jsx
|
|
35
|
+
// import 時のasをチェック
|
|
36
|
+
import { HogeSelect as FugaSelect } from 'any'
|
|
37
|
+
// typeを設定すれば命名チェックから除外される
|
|
38
|
+
import { type HogeSelect as Fuga } from 'any'
|
|
39
|
+
import type { HogeSelect as Fuga } from 'any'
|
|
40
|
+
|
|
41
|
+
// 継承元がselectであることがわかるためOK
|
|
42
|
+
const HogeSelect = styled.select
|
|
43
|
+
const HogeSelect = styled(FugaSelect)
|
|
44
|
+
```
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const { generateTagFormatter } = require('../../libs/format_styled_components')
|
|
2
|
+
|
|
3
|
+
const EXPECTED_NAMES = {
|
|
4
|
+
'(A|^a)rticle$': 'Article$',
|
|
5
|
+
'(A|^a)side$': 'Aside$',
|
|
6
|
+
'(B|^b)utton$': 'Button$',
|
|
7
|
+
'(Date|Wareki)Picker$': '(Date|Wareki)Picker$',
|
|
8
|
+
'(F|^f)ieldset$': 'Fieldset$',
|
|
9
|
+
'(F|^f)orm$': 'Form$',
|
|
10
|
+
'(Heading|^h(2|3|4|5|6))$': 'Heading$',
|
|
11
|
+
'(I|^i)nput$': 'Input$',
|
|
12
|
+
'(L|^l)abel$': 'Label$',
|
|
13
|
+
'(N|^n)av$': 'Nav$',
|
|
14
|
+
'(Ordered(.*)List|^ol)$': 'Ordered(.*)List$',
|
|
15
|
+
'(PageHeading|^h1)$': 'PageHeading$',
|
|
16
|
+
'(S|^s)ection$': 'Section$',
|
|
17
|
+
'(S|^s)elect$': 'Select$',
|
|
18
|
+
'(T|^t)extarea$': 'Textarea$',
|
|
19
|
+
'AccordionPanel$': 'AccordionPanel$',
|
|
20
|
+
'ActionDialogWithTrigger$': 'ActionDialogWithTrigger$',
|
|
21
|
+
'Anchor$': 'Anchor$',
|
|
22
|
+
'AnchorButton$': 'AnchorButton$',
|
|
23
|
+
'Base$': 'Base$',
|
|
24
|
+
'BaseColumn$': 'BaseColumn$',
|
|
25
|
+
'Center$': 'Center$',
|
|
26
|
+
'Check(B|b)ox$': 'Checkbox$',
|
|
27
|
+
'Check(B|b)ox(e)?s$': 'Checkboxes$',
|
|
28
|
+
'Cluster$': 'Cluster$',
|
|
29
|
+
'Combo(B|b)ox$': 'Combobox$',
|
|
30
|
+
'DialogTrigger$': 'DialogTrigger$',
|
|
31
|
+
'DropZone$': 'DropZone$',
|
|
32
|
+
'DropdownTrigger$': 'DropdownTrigger$',
|
|
33
|
+
'FieldSet$': 'FieldSet$',
|
|
34
|
+
'Fieldsets$': 'Fieldsets$',
|
|
35
|
+
'FilterDropdown$': 'FilterDropdown$',
|
|
36
|
+
'FormControl$': 'FormControl$',
|
|
37
|
+
'FormControls$': 'FormControls$',
|
|
38
|
+
'FormDialog$': 'FormDialog$',
|
|
39
|
+
'FormGroup$': 'FormGroup$',
|
|
40
|
+
'Icon$': 'Icon$',
|
|
41
|
+
'Image$': 'Image$',
|
|
42
|
+
'Img$': 'Img$',
|
|
43
|
+
'IndexNav$': 'IndexNav$',
|
|
44
|
+
'InputFile$': 'InputFile$',
|
|
45
|
+
'Link$': 'Link$',
|
|
46
|
+
'Message$': 'Message$',
|
|
47
|
+
'ModelessDialog$': 'ModelessDialog$',
|
|
48
|
+
'Pagination$': 'Pagination$',
|
|
49
|
+
'RadioButton$': 'RadioButton$',
|
|
50
|
+
'RadioButtonPanel$': 'RadioButtonPanel$',
|
|
51
|
+
'RadioButtonPanels$': 'RadioButtonPanels$',
|
|
52
|
+
'RadioButtons$': 'RadioButtons$',
|
|
53
|
+
'Reel$': 'Reel$',
|
|
54
|
+
'RemoteDialogTrigger$': 'RemoteDialogTrigger$',
|
|
55
|
+
'RemoteTrigger(.*)FormDialog$': 'RemoteTrigger(.*)FormDialog$',
|
|
56
|
+
'RemoteTrigger(.+)Dialog$': 'RemoteTrigger(.+)Dialog$',
|
|
57
|
+
'RightFixedNote$': 'RightFixedNote$',
|
|
58
|
+
'SearchInput$': 'SearchInput$',
|
|
59
|
+
'SegmentedControl$': 'SegmentedControl$',
|
|
60
|
+
'SideNav$': 'SideNav$',
|
|
61
|
+
'Sidebar$': 'Sidebar$',
|
|
62
|
+
'SmartHRLogo$': 'SmartHRLogo$',
|
|
63
|
+
'Stack$': 'Stack$',
|
|
64
|
+
'Switch$': 'Switch$',
|
|
65
|
+
'TabItem$': 'TabItem$',
|
|
66
|
+
'Text$': 'Text$',
|
|
67
|
+
'TimePicker$': 'TimePicker$',
|
|
68
|
+
'^(img|svg)$': '(Img|Image|Icon)$',
|
|
69
|
+
'^a$': '(Anchor|Link)$',
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const unexpectedMessageTemplate = `{{extended}} は smarthr-ui/{{expected}} をextendすることを期待する名称になっています
|
|
73
|
+
- childrenにHeadingを含まない場合、コンポーネントの名称から"{{expected}}"を取り除いてください
|
|
74
|
+
- childrenにHeadingを含み、アウトラインの範囲を指定するためのコンポーネントならば、smarthr-ui/{{expected}}をexendしてください
|
|
75
|
+
- "styled(Xxxx)" 形式の場合、拡張元であるXxxxコンポーネントの名称の末尾に"{{expected}}"を設定し、そのコンポーネント内でsmarthr-ui/{{expected}}を利用してください`
|
|
76
|
+
const UNEXPECTED_NAMES = {
|
|
77
|
+
'(Anchor|^a)$': '(Anchor)$',
|
|
78
|
+
'(A|^a)rticle$': ['(Article)$', unexpectedMessageTemplate ],
|
|
79
|
+
'(A|^a)side$': ['(Aside)$', unexpectedMessageTemplate ],
|
|
80
|
+
'(B|^b)utton$': '(Button)$',
|
|
81
|
+
'(Date|Wareki)Picker$': '((Date|Wareki)Picker)$',
|
|
82
|
+
'(F|^f)ieldset$': '(Fieldset)$',
|
|
83
|
+
'(F|^f)orm$': '(Form)$',
|
|
84
|
+
'(Heading|^h(1|2|3|4|5|6))$': '(Heading)$',
|
|
85
|
+
'(Icon|^(img|svg))$': '(Icon)$',
|
|
86
|
+
'(Image|^(img|svg))$': '(Image)$',
|
|
87
|
+
'(Img|^(img|svg))$': '(Img)$',
|
|
88
|
+
'(I|^i)nput$': '(Input)$',
|
|
89
|
+
'(Link|^a)$': '(Link)$',
|
|
90
|
+
'(L|^l)abel$': '(Label)$',
|
|
91
|
+
'(N|^n)av$': ['(Nav)$', unexpectedMessageTemplate ],
|
|
92
|
+
'(Ordered(.*)List|^ol)$': '(Ordered(.*)List)$',
|
|
93
|
+
'(S|^s)ection$': ['(Section)$', unexpectedMessageTemplate ],
|
|
94
|
+
'(S|^s)elect$': '(Select)$',
|
|
95
|
+
'(T|^t)extarea$': '(Textarea)$',
|
|
96
|
+
'Base$': '(Base)$',
|
|
97
|
+
'BaseColumn$': '(BaseColumn)$',
|
|
98
|
+
'Center$': '(Center)$',
|
|
99
|
+
'Check(B|b)ox$': '(Checkbox)$',
|
|
100
|
+
'Check(B|b)ox(e)?s$': '(Checkboxes)$',
|
|
101
|
+
'Cluster$': '(Cluster)$',
|
|
102
|
+
'Combo(B|b)ox$': '(Combobox)$',
|
|
103
|
+
'Fieldsets$': '(Fieldsets)$',
|
|
104
|
+
'FilterDropdown$': '(FilterDropdown)$',
|
|
105
|
+
'FormControl$': '(FormControl)$',
|
|
106
|
+
'FormControls$': '(FormControls)$',
|
|
107
|
+
'FormDialog$': '(FormDialog)$',
|
|
108
|
+
'FormGroup$': '(FormGroup)$',
|
|
109
|
+
'IndexNav$': '(IndexNav)$',
|
|
110
|
+
'InputFile$': '(InputFile)$',
|
|
111
|
+
'RadioButton$': '(RadioButton)$',
|
|
112
|
+
'RadioButtonPanel$': '(RadioButtonPanel)$',
|
|
113
|
+
'RadioButtonPanels$': '(RadioButtonPanels)$',
|
|
114
|
+
'RadioButtons$': '(RadioButtons)$',
|
|
115
|
+
'Reel$': '(Reel)$',
|
|
116
|
+
'RemoteTrigger(.*)FormDialog$': '(RemoteTrigger(.*)FormDialog)$',
|
|
117
|
+
'SearchInput$': '(SearchInput)$',
|
|
118
|
+
'SideNav$': '(SideNav)$',
|
|
119
|
+
'Sidebar$': '(Sidebar)$',
|
|
120
|
+
'Stack$': '(Stack)$',
|
|
121
|
+
'TimePicker$': '(TimePicker)$',
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
const SCHEMA = []
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
129
|
+
*/
|
|
130
|
+
module.exports = {
|
|
131
|
+
meta: {
|
|
132
|
+
type: 'problem',
|
|
133
|
+
schema: SCHEMA,
|
|
134
|
+
},
|
|
135
|
+
create(context) {
|
|
136
|
+
return generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES })
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
module.exports.schema = SCHEMA
|
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
const { generateTagFormatter } = require('../../libs/format_styled_components')
|
|
2
|
-
|
|
3
|
-
const EXPECTED_NAMES = {
|
|
4
|
-
'(Button)$': '(Button)$',
|
|
5
|
-
'(Link)$': '(Link)$',
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const UNEXPECTED_NAMES = EXPECTED_NAMES
|
|
9
|
-
|
|
10
1
|
const SCHEMA = []
|
|
11
2
|
|
|
12
|
-
const REGEX_PATTERN =
|
|
3
|
+
const REGEX_PATTERN = /(Button|Link)$/
|
|
13
4
|
|
|
14
5
|
/**
|
|
15
6
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
@@ -21,7 +12,6 @@ module.exports = {
|
|
|
21
12
|
},
|
|
22
13
|
create(context) {
|
|
23
14
|
return {
|
|
24
|
-
...generateTagFormatter({ context, EXPECTED_NAMES, UNEXPECTED_NAMES }),
|
|
25
15
|
JSXOpeningElement: (node) => {
|
|
26
16
|
const nodeName = node.name.name
|
|
27
17
|
|
|
@@ -21,6 +21,14 @@ const SCHEMA = [
|
|
|
21
21
|
}
|
|
22
22
|
]
|
|
23
23
|
|
|
24
|
+
const CWD = process.cwd()
|
|
25
|
+
const REPLACE_PATH_ENTRIES = Object.entries(replacePaths)
|
|
26
|
+
const REPLACE_PATH_KEY_REGEX = new RegExp(`^(${Object.keys(replacePaths).join('|')})`)
|
|
27
|
+
const DIR_SEPARATE_REGEX = /\//g
|
|
28
|
+
const MULTIPLE_DIR_SEPARATE_REGEX =/(\/)+/g
|
|
29
|
+
|
|
30
|
+
const dirCount = (dir) => dir.match(DIR_SEPARATE_REGEX).length
|
|
31
|
+
|
|
24
32
|
const convertType = (calcContext, calcDomainNode) => {
|
|
25
33
|
const { option: { format: { all, outside, globalModule, module, domain, lower } } } = calcContext
|
|
26
34
|
const { isGlobalModuleImport, isModuleImport, isDomainImport, isLowerImport } = calcDomainNode
|
|
@@ -49,14 +57,14 @@ const calculateAbsoluteImportPath = ({ importPath, resolvedImportPath }) => {
|
|
|
49
57
|
return importPath
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
return
|
|
60
|
+
return REPLACE_PATH_ENTRIES.reduce((prev, [key, values]) => {
|
|
53
61
|
if (resolvedImportPath === prev) {
|
|
54
62
|
return values.reduce((p, v) => {
|
|
55
63
|
if (prev === p) {
|
|
56
|
-
const regexp = new RegExp(`^${path.resolve(`${
|
|
64
|
+
const regexp = new RegExp(`^${path.resolve(`${CWD}/${v}`)}(.+)$`)
|
|
57
65
|
|
|
58
|
-
if (
|
|
59
|
-
return p.replace(regexp, `${key}/$1`).replace(
|
|
66
|
+
if (regexp.test(prev)) {
|
|
67
|
+
return p.replace(regexp, `${key}/$1`).replace(MULTIPLE_DIR_SEPARATE_REGEX, '/')
|
|
60
68
|
}
|
|
61
69
|
}
|
|
62
70
|
|
|
@@ -71,7 +79,7 @@ const calculateRelativeImportPath = ({ importPath, filteredDirs, filteredPaths }
|
|
|
71
79
|
// HINT: 相対パスの場合でも、余計にさかのぼっていたりする場合もあるので修正対象とする
|
|
72
80
|
if (
|
|
73
81
|
importPath[0] !== '.' &&
|
|
74
|
-
!
|
|
82
|
+
!REPLACE_PATH_KEY_REGEX.test(importPath)
|
|
75
83
|
) {
|
|
76
84
|
return importPath
|
|
77
85
|
}
|
|
@@ -113,7 +121,7 @@ module.exports = {
|
|
|
113
121
|
|
|
114
122
|
// HINT: 記述するdirの数でより近い方を選択する。
|
|
115
123
|
// 相対・絶対記法が同一の場合、おそらくimport元から距離はあるため、たどりやすくするために絶対記法を選択する
|
|
116
|
-
return (absoluted
|
|
124
|
+
return (dirCount(absoluted) <= dirCount(relatived)) ? absoluted : relatived
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
return importPath
|
|
@@ -13,6 +13,8 @@ const SCHEMA = [
|
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
|
|
16
|
+
const NOOP = () => {}
|
|
17
|
+
|
|
16
18
|
/**
|
|
17
19
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
18
20
|
*/
|
|
@@ -23,7 +25,7 @@ module.exports = {
|
|
|
23
25
|
},
|
|
24
26
|
create(context) {
|
|
25
27
|
const { componentPath, componentName, prohibitAttributies } = context.options[0]
|
|
26
|
-
let JSXAttribute =
|
|
28
|
+
let JSXAttribute = NOOP
|
|
27
29
|
|
|
28
30
|
if (prohibitAttributies) {
|
|
29
31
|
JSXAttribute = (node) => {
|
|
@@ -46,7 +46,7 @@ module.exports = {
|
|
|
46
46
|
const calcContext = calculateDomainContext(context)
|
|
47
47
|
|
|
48
48
|
// 対象外ファイル
|
|
49
|
-
if (!calcContext.isTarget || calcContext.option.ignores && calcContext.option.ignores.some((i) =>
|
|
49
|
+
if (!calcContext.isTarget || calcContext.option.ignores && calcContext.option.ignores.some((i) => (new RegExp(i)).test(calcContext.filename))) {
|
|
50
50
|
return {}
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -55,8 +55,8 @@ module.exports = {
|
|
|
55
55
|
humanizeParentDir,
|
|
56
56
|
} = calcContext
|
|
57
57
|
|
|
58
|
-
const targetPathRegexs = Object.keys(option
|
|
59
|
-
const targetAllowedImports = targetPathRegexs.filter((regex) =>
|
|
58
|
+
const targetPathRegexs = option?.allowedImports ? Object.keys(option.allowedImports) : []
|
|
59
|
+
const targetAllowedImports = targetPathRegexs.filter((regex) => (new RegExp(regex)).test(calcContext.filename))
|
|
60
60
|
|
|
61
61
|
return {
|
|
62
62
|
ImportDeclaration: (node) => {
|
|
@@ -90,11 +90,11 @@ module.exports = {
|
|
|
90
90
|
})
|
|
91
91
|
})
|
|
92
92
|
|
|
93
|
-
if (isDenyPath
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
93
|
+
if (isDenyPath) {
|
|
94
|
+
if (deniedModules[0] === true) {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
} else if (deniedModules.length === 1 && deniedModules[0].length === 0) {
|
|
98
98
|
return
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -19,7 +19,7 @@ module.exports = {
|
|
|
19
19
|
},
|
|
20
20
|
create(context) {
|
|
21
21
|
const options = context.options[0]
|
|
22
|
-
const targetPaths = Object.keys(options).filter((regex) =>
|
|
22
|
+
const targetPaths = Object.keys(options).filter((regex) => (new RegExp(regex)).test(context.filename))
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
if (targetPaths.length === 0) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const path = require('path')
|
|
2
|
+
const { getParentDir } = require('../../libs/util')
|
|
2
3
|
|
|
3
4
|
const SCHEMA = [{
|
|
4
5
|
type: 'object',
|
|
@@ -30,6 +31,8 @@ const SCHEMA = [{
|
|
|
30
31
|
additionalProperties: true,
|
|
31
32
|
}]
|
|
32
33
|
|
|
34
|
+
const CWD = process.cwd()
|
|
35
|
+
|
|
33
36
|
const defaultReportMessage = (moduleName, exportName) => `${moduleName}${typeof exportName == 'string' ? `/${exportName}`: ''} は利用しないでください`
|
|
34
37
|
|
|
35
38
|
/**
|
|
@@ -42,14 +45,9 @@ module.exports = {
|
|
|
42
45
|
},
|
|
43
46
|
create(context) {
|
|
44
47
|
const options = context.options[0]
|
|
45
|
-
const parentDir = (
|
|
46
|
-
const dir = context.filename.match(/^(.+?)\..+?$/)[1].split('/')
|
|
47
|
-
dir.pop()
|
|
48
|
-
|
|
49
|
-
return dir.join('/')
|
|
50
|
-
})()
|
|
48
|
+
const parentDir = getParentDir(context.filename)
|
|
51
49
|
const targetPathRegexs = Object.keys(options)
|
|
52
|
-
const targetProhibits = targetPathRegexs.filter((regex) =>
|
|
50
|
+
const targetProhibits = targetPathRegexs.filter((regex) => (new RegExp(regex)).test(context.filename))
|
|
53
51
|
|
|
54
52
|
if (targetProhibits.length === 0) {
|
|
55
53
|
return {}
|
|
@@ -63,32 +61,32 @@ module.exports = {
|
|
|
63
61
|
|
|
64
62
|
targetModules.forEach((targetModule) => {
|
|
65
63
|
const { imported, reportMessage } = Object.assign({imported: true}, option[targetModule])
|
|
66
|
-
const actualTarget = targetModule[0] !== '.' ? targetModule : path.resolve(`${
|
|
64
|
+
const actualTarget = targetModule[0] !== '.' ? targetModule : path.resolve(`${CWD}/${targetModule}`)
|
|
67
65
|
let sourceValue = node.source.value
|
|
68
66
|
|
|
69
67
|
if (actualTarget[0] === '/') {
|
|
70
68
|
sourceValue = path.resolve(`${parentDir}/${sourceValue}`)
|
|
71
69
|
}
|
|
72
70
|
|
|
73
|
-
if (actualTarget
|
|
74
|
-
|
|
75
|
-
}
|
|
71
|
+
if (actualTarget === sourceValue) {
|
|
72
|
+
let useImported = false
|
|
76
73
|
|
|
77
|
-
const useImported = (() => {
|
|
78
74
|
if (!Array.isArray(imported)) {
|
|
79
|
-
|
|
80
|
-
}
|
|
75
|
+
useImported = !!imported
|
|
76
|
+
} else {
|
|
77
|
+
const specifier = node.specifiers.find((s) => s.imported && imported.includes(s.imported.name))
|
|
81
78
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
if (specifier) {
|
|
80
|
+
useImported = specifier.imported.name
|
|
81
|
+
}
|
|
82
|
+
}
|
|
86
83
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
if (useImported) {
|
|
85
|
+
context.report({
|
|
86
|
+
node,
|
|
87
|
+
message: reportMessage ? reportMessage.replaceAll('{{module}}', node.source.value).replaceAll('{{export}}', useImported) : defaultReportMessage(node.source.value, useImported)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
92
90
|
}
|
|
93
91
|
})
|
|
94
92
|
})
|
|
@@ -13,7 +13,7 @@ const recursiveFetchName = (obj, chained = '') => {
|
|
|
13
13
|
const recursiveFetchRootNameIsPath = (obj, regex) => {
|
|
14
14
|
const [name, chained] = recursiveFetchName(obj, '')
|
|
15
15
|
|
|
16
|
-
return
|
|
16
|
+
return regex.test(name) ? chained : null
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const SCHEMA = [
|