eslint-plugin-smarthr 0.2.1 → 0.2.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.2.4](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.2.3...v0.2.4) (2022-08-30)
6
+
7
+
8
+ ### Features
9
+
10
+ * ruleを更新する ([#27](https://github.com/kufu/eslint-plugin-smarthr/issues/27)) ([1ec7783](https://github.com/kufu/eslint-plugin-smarthr/commit/1ec7783395c9dafa547c453724f3671df489997b))
11
+
12
+ ### [0.2.3](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.2.2...v0.2.3) (2022-08-26)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * a11y-xxxx: styled(Hoge)の実行結果を変数に代入しないパターンに対応する ([#26](https://github.com/kufu/eslint-plugin-smarthr/issues/26)) ([0bb0259](https://github.com/kufu/eslint-plugin-smarthr/commit/0bb02595a3be35802c1fe8bc41011fd1e55bf319))
18
+
19
+ ### [0.2.2](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.2.1...v0.2.2) (2022-08-18)
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * redundant-name rule の allowedNames オプションが適用されないバグがあったため修正する ([#23](https://github.com/kufu/eslint-plugin-smarthr/issues/23)) ([86a7bc5](https://github.com/kufu/eslint-plugin-smarthr/commit/86a7bc548398261885f31c75b56d8edffe5017c3))
25
+
5
26
  ### [0.2.1](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.2.0...v0.2.1) (2022-08-15)
6
27
 
7
28
 
package/libs/common.js CHANGED
@@ -28,7 +28,7 @@ const rootPath = (() => {
28
28
  return ''
29
29
  }
30
30
 
31
- const p = (replacePaths['@/'] || [])[0]
31
+ const p = (replacePaths['@/'] || replacePaths['~/'] || [])[0]
32
32
 
33
33
  if (!p) {
34
34
  return ''
@@ -1,3 +1,31 @@
1
+ const getExtendedComponentName = (node) => {
2
+ if (!node.parent) {
3
+ return null
4
+ }
5
+
6
+ return node.parent.id?.name || getExtendedComponentName(node.parent)
7
+ }
8
+ const getBaseComponentName = (node) => {
9
+ if (!node) {
10
+ return null
11
+ }
12
+
13
+ if (node.type === 'CallExpression') {
14
+ if (node.callee.name === 'styled') {
15
+ return node.arguments[0].name
16
+ }
17
+ if (node.callee.object?.name === 'styled') {
18
+ return node.callee.property.name
19
+ }
20
+ }
21
+
22
+ if (node?.object?.name === 'styled') {
23
+ return node.property.name
24
+ }
25
+
26
+ return getBaseComponentName(node.parent)
27
+ }
28
+
1
29
  const generateTagFormatter = ({ context, EXPECTED_NAMES }) => ({
2
30
  ImportDeclaration: (node) => {
3
31
  if (node.source.value !== 'styled-components') {
@@ -17,40 +45,29 @@ const generateTagFormatter = ({ context, EXPECTED_NAMES }) => ({
17
45
  }
18
46
  },
19
47
  TaggedTemplateExpression: (node) => {
20
- const { tag } = node
21
- const base = (() => {
22
- if (tag.type === 'CallExpression' && tag.callee.name === 'styled') {
23
- return tag.arguments[0].name
24
- }
25
-
26
- if (tag?.object?.name === 'styled') {
27
- return tag.property.name
28
- }
48
+ const extended = getExtendedComponentName(node)
29
49
 
30
- return null
31
- })()
50
+ if (extended) {
51
+ const base = getBaseComponentName(node.tag)
32
52
 
33
- if (!base) {
34
- return
35
- }
53
+ if (base) {
54
+ Object.entries(EXPECTED_NAMES).forEach(([b, e]) => {
55
+ if (base.match(new RegExp(b))) {
56
+ const extendedregex = new RegExp(e)
36
57
 
37
- const extended = node.parent.id.name
38
-
39
- Object.entries(EXPECTED_NAMES).forEach(([b, e]) => {
40
- if (base.match(new RegExp(b))) {
41
- const extendedregex = new RegExp(e)
42
-
43
- if (!extended.match(extendedregex)) {
44
- context.report({
45
- node: node.parent,
46
- messageId: 'format-styled-components',
47
- data: {
48
- message: `${extended}を正規表現 "${extendedregex.toString()}" がmatchする名称に変更してください`,
49
- },
50
- });
51
- }
58
+ if (!extended.match(extendedregex)) {
59
+ context.report({
60
+ node: node.parent,
61
+ messageId: 'format-styled-components',
62
+ data: {
63
+ message: `${extended}を正規表現 "${extendedregex.toString()}" がmatchする名称に変更してください`,
64
+ },
65
+ });
66
+ }
67
+ }
68
+ })
52
69
  }
53
- })
70
+ }
54
71
  },
55
72
  })
56
73
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "0.2.1",
3
+ "version": "0.2.4",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -1,6 +1,7 @@
1
1
  const { generateTagFormatter } = require('../../libs/format_styled_components')
2
2
 
3
3
  const EXPECTED_NAMES = {
4
+ 'SmartHRLogo$': 'SmartHRLogo$',
4
5
  '(b|B)utton$': 'Button$',
5
6
  'Anchor$': 'Anchor$',
6
7
  'Link$': 'Link$',
@@ -35,23 +36,17 @@ module.exports = {
35
36
  return
36
37
  }
37
38
 
38
- const recursiveSearch = (c) => {
39
- if (['JSXText', 'JSXExpressionContainer'].includes(c.type)) {
40
- return true
41
- }
42
-
43
- if (c.type === 'JSXElement') {
44
- if (c.openingElement.attributes.some((a) => (['visuallyHiddenText', 'alt'].includes(a.name.name) && !!a.value.value))) {
45
- return true
46
- }
47
-
48
- if (c.children && filterFalsyJSXText(c.children).some(recursiveSearch)) {
49
- return true
50
- }
51
- }
52
-
53
- return false
54
- }
39
+ const recursiveSearch = (c) => (
40
+ ['JSXText', 'JSXExpressionContainer'].includes(c.type) ||
41
+ (
42
+ c.type === 'JSXElement' && (
43
+ // HINT: SmartHRLogo コンポーネントは内部でaltを持っているため対象外にする
44
+ c.openingElement.name.name.match(/SmartHRLogo$/) ||
45
+ c.openingElement.attributes.some((a) => (['visuallyHiddenText', 'alt'].includes(a.name.name) && !!a.value.value)) ||
46
+ (c.children && filterFalsyJSXText(c.children).some(recursiveSearch))
47
+ )
48
+ )
49
+ )
55
50
 
56
51
  const child = filterFalsyJSXText(parentNode.children).find(recursiveSearch)
57
52
 
@@ -47,7 +47,21 @@ module.exports = {
47
47
  return
48
48
  }
49
49
 
50
- filterFalsyJSXText(parentNode.children).forEach((c) => {
50
+ const children = filterFalsyJSXText(parentNode.children)
51
+
52
+ if (children.length > 1) {
53
+ context.report({
54
+ node,
55
+ messageId: 'a11y-trigger-has-button',
56
+ data: {
57
+ message: `${match[1]}Trigger の直下には複数のコンポーネントを設置することは出来ません。buttonコンポーネントが一つだけ設置されている状態にしてください`,
58
+ },
59
+ })
60
+
61
+ return
62
+ }
63
+
64
+ children.forEach((c) => {
51
65
  // `<DialogTrigger>{button}</DialogTrigger>` のような場合は許可する
52
66
  if (c.type === 'JSXExpressionContainer') {
53
67
  return false
@@ -56,7 +70,7 @@ module.exports = {
56
70
  if (
57
71
  c.type !== 'JSXElement' ||
58
72
  !c.openingElement.name.name.match(/(b|B)utton$/) ||
59
- c.openingElement.name.name.match(/AnchorButton?/)
73
+ c.openingElement.name.name.match(/AnchorButton$/)
60
74
  ) {
61
75
  context.report({
62
76
  node: c,
@@ -16,7 +16,6 @@ const DEFAULT_CONFIG = {
16
16
  ],
17
17
  SUFFIX: ['Props', 'Type'],
18
18
  },
19
- typeProperty: COMMON_DEFAULT_CONFIG,
20
19
  file: COMMON_DEFAULT_CONFIG,
21
20
  property: COMMON_DEFAULT_CONFIG,
22
21
  function: COMMON_DEFAULT_CONFIG,
@@ -62,7 +61,6 @@ const SCHEMA = [
62
61
  ...DEFAULT_SCHEMA_PROPERTY,
63
62
  suffix: { type: 'array', items: { type: 'string' } },
64
63
  },
65
- typeProperty: DEFAULT_SCHEMA_PROPERTY,
66
64
  file: DEFAULT_SCHEMA_PROPERTY,
67
65
  property: DEFAULT_SCHEMA_PROPERTY,
68
66
  function: DEFAULT_SCHEMA_PROPERTY,
@@ -89,7 +87,6 @@ const generateRedundantKeywords = ({ args, key, terminalImportName }) => {
89
87
  const option = args.option[key] || {}
90
88
  const ignoreKeywords = option.ignoreKeywords || DEFAULT_CONFIG[key].IGNORE_KEYWORDS
91
89
  const terminalImportKeyword = terminalImportName ? terminalImportName.toLowerCase() : ''
92
-
93
90
  return args.keywords.reduce((prev, keyword) => {
94
91
  if (keyword === terminalImportKeyword || ignoreKeywords.includes(keyword)) {
95
92
  return prev
@@ -203,7 +200,7 @@ const handleReportBetterName = ({
203
200
  }
204
201
 
205
202
  const generateTypeRedundant = (args) => {
206
- const { context } = args
203
+ const { context, filename } = args
207
204
  const key = 'type'
208
205
  const redundantKeywords = generateRedundantKeywords({ args, key })
209
206
  const option = args.option[key]
@@ -211,6 +208,14 @@ const generateTypeRedundant = (args) => {
211
208
 
212
209
  return (node) => {
213
210
  const typeName = node.id.name
211
+
212
+ if (
213
+ option.allowedNames &&
214
+ Object.entries(option.allowedNames).find(([regex, calcs]) => filename.match(new RegExp(regex)) && calcs.find((c) => c === typeName))
215
+ ) {
216
+ return
217
+ }
218
+
214
219
  const suffix = option.suffix || defaultConfig.SUFFIX
215
220
 
216
221
  let SuffixedName = typeName
@@ -254,8 +259,7 @@ const generateTypeRedundant = (args) => {
254
259
  }
255
260
 
256
261
  const generateTypePropertyRedundant = (args) => {
257
- const key = 'typeProperty'
258
-
262
+ const key = 'property'
259
263
  return handleReportBetterName({
260
264
  ...args,
261
265
  key,
@@ -266,7 +270,7 @@ const generateTypePropertyRedundant = (args) => {
266
270
  })
267
271
  }
268
272
  const generateTypePropertyFunctionParamsRedundant = (args) => {
269
- const key = 'typeProperty'
273
+ const key = 'property'
270
274
  const redundant = handleReportBetterName({
271
275
  ...args,
272
276
  key,
@@ -290,7 +294,15 @@ const generatePropertyRedundant = (args) => {
290
294
  option: args.option[key],
291
295
  redundantKeywords: generateRedundantKeywords({ args, key }),
292
296
  defaultBetterName: 'item',
293
- fetchName: (node) => node.key.name,
297
+ fetchName: (node) => {
298
+ // argumentsとしてわたされたobjectの展開などの場合は許可する
299
+ // このファイル内で修正すべき場合などは冗長な名前を修正するべき場合はtype propertyなどで判断出来る
300
+ if (node.parent.type === 'ObjectPattern') {
301
+ return null
302
+ }
303
+
304
+ return node.key.name
305
+ },
294
306
  })
295
307
  }
296
308
 
@@ -383,7 +395,6 @@ module.exports = {
383
395
  'file-name': ' {{ message }}',
384
396
  'type-name': '{{ message }}',
385
397
  'type-name/invalid-suffix': '{{ message }}',
386
- 'typeProperty-name': '{{ message }}',
387
398
  'property-name': ' {{ message }}',
388
399
  'function-name': ' {{ message }}',
389
400
  'functionParams-name': ' {{ message }}',
@@ -441,9 +452,10 @@ module.exports = {
441
452
  addRule('TSTypeAliasDeclaration', generateTypeRedundant(args))
442
453
  // addRule('TSInterfaceDeclaration', generateTypeRedundant(args)) // 必要になったら実装する
443
454
  }
444
- if (option.typeProperty) {
455
+ if (option.property) {
445
456
  const typePropRedundant = generateTypePropertyRedundant(args)
446
457
  const typeFuncParamRedundant = generateTypePropertyFunctionParamsRedundant(args)
458
+ const redundant = generatePropertyRedundant(args)
447
459
 
448
460
  addRule('TSPropertySignature', (node) => {
449
461
  typePropRedundant(node)
@@ -452,10 +464,6 @@ module.exports = {
452
464
  typeFuncParamRedundant(node.typeAnnotation.typeAnnotation)
453
465
  }
454
466
  })
455
- }
456
- if (option.property) {
457
- const redundant = generatePropertyRedundant(args)
458
-
459
467
  addRule('Property', redundant)
460
468
  addRule('PropertyDefinition', redundant)
461
469
  }
@@ -104,7 +104,7 @@ module.exports = {
104
104
  sourceValue = sources.join('/')
105
105
  }
106
106
 
107
- if (barrel) {
107
+ if (barrel && !barrel.match(new RegExp(`^${rootPath}/index\.`))) {
108
108
  barrel = calculateReplacedImportPath(barrel)
109
109
 
110
110
  context.report({
@@ -26,6 +26,7 @@ ruleTester.run('a11y-clickable-element-has-text', rule, {
26
26
  { code: 'const HogeLink = styled(Link)``' },
27
27
  { code: 'const HogeButton = styled(Button)``' },
28
28
  { code: 'const FugaAnchor = styled(HogeAnchor)``' },
29
+ { code: 'const FugaSmartHRLogo = styled(SmartHRLogo)``' },
29
30
  {
30
31
  code: `<a>ほげ</a>`,
31
32
  },
@@ -89,6 +90,12 @@ ruleTester.run('a11y-clickable-element-has-text', rule, {
89
90
  {
90
91
  code: `<a><span>{any}</span></a>`,
91
92
  },
93
+ {
94
+ code: `<a><SmartHRLogo /></a>`,
95
+ },
96
+ {
97
+ code: `<a><PrefixSmartHRLogo /></a>`,
98
+ },
92
99
  ],
93
100
  invalid: [
94
101
  { code: `import hoge from 'styled-components'`, errors: [ { message: "styled-components をimportする際は、名称が`styled` となるようにしてください。例: `import styled from 'styled-components'`" } ] },
@@ -98,6 +105,8 @@ ruleTester.run('a11y-clickable-element-has-text', rule, {
98
105
  { code: 'const Hoge = styled(Link)``', errors: [ { message: `Hogeを正規表現 "/Link$/" がmatchする名称に変更してください` } ] },
99
106
  { code: 'const Hoge = styled(Button)``', errors: [ { message: `Hogeを正規表現 "/Button$/" がmatchする名称に変更してください` } ] },
100
107
  { code: 'const Fuga = styled(HogeAnchor)``', errors: [ { message: `Fugaを正規表現 "/Anchor$/" がmatchする名称に変更してください` } ] },
108
+ { code: 'const Fuga = styled(HogeAnchor)``', errors: [ { message: `Fugaを正規表現 "/Anchor$/" がmatchする名称に変更してください` } ] },
109
+ { code: 'const Fuga = styled(SmartHRLogo)``', errors: [ { message: `Fugaを正規表現 "/SmartHRLogo$/" がmatchする名称に変更してください` } ] },
101
110
  {
102
111
  code: `<a><img src="hoge.jpg" /></a>`,
103
112
  errors: [{ message: defaultErrorMessage }]
@@ -138,5 +147,9 @@ ruleTester.run('a11y-clickable-element-has-text', rule, {
138
147
  code: `<button><AnyComponent><Icon visuallyHiddenText="" /></AnyComponent></button>`,
139
148
  errors: [{ message: defaultErrorMessage }]
140
149
  },
150
+ {
151
+ code: `<button><SmartHRLogoSuffix /></button>`,
152
+ errors: [{ message: defaultErrorMessage }]
153
+ },
141
154
  ]
142
155
  })
@@ -33,18 +33,20 @@ ruleTester.run('a11y-trigger-has-button', rule, {
33
33
  ],
34
34
  invalid: [
35
35
  { code: `import hoge from 'styled-components'`, errors: [ { message: "styled-components をimportする際は、名称が`styled` となるようにしてください。例: `import styled from 'styled-components'`" } ] },
36
- { code: 'const Hoge = styled.button``', errors: [ { message: `Hogeを正規表現 "/Button$/" がmatchする名称に変更してください` } ] },
37
- { code: 'const Hoge = styled.a``', errors: [ { message: `Hogeを正規表現 "/(Anchor|Link)$/" がmatchする名称に変更してください` } ] },
38
- { code: 'const Hoge = styled(Button)``', errors: [ { message: `Hogeを正規表現 "/Button$/" がmatchする名称に変更してください` } ] },
39
- { code: 'const Hoge = styled(AnchorButton)``', errors: [ { message: `Hogeを正規表現 "/Button$/" がmatchする名称に変更してください` },{ message: `Hogeを正規表現 "/AnchorButton$/" がmatchする名称に変更してください` } ] },
40
- { code: 'const Hoge = styled(ButtonAnchor)``', errors: [ { message: `Hogeを正規表現 "/ButtonAnchor$/" がmatchする名称に変更してください` }, { message: `Hogeを正規表現 "/Anchor$/" がmatchする名称に変更してください` } ] },
41
- { code: 'const Hoge = styled(Anchor)``', errors: [ { message: `Hogeを正規表現 "/Anchor$/" がmatchする名称に変更してください` } ] },
42
- { code: 'const Hoge = styled(Link)``', errors: [ { message: `Hogeを正規表現 "/Link$/" がmatchする名称に変更してください` } ] },
43
- { code: 'const Hoge = styled(DropdownTrigger)``', errors: [ { message: `Hogeを正規表現 "/DropdownTrigger$/" がmatchする名称に変更してください` } ] },
44
- { code: 'const Hoge = styled(DialogTrigger)``', errors: [ { message: `Hogeを正規表現 "/DialogTrigger$/" がmatchする名称に変更してください` } ] },
45
- { code: '<DropdownTrigger>ほげ</DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下にはbuttonコンポーネントのみ設置してください' } ] },
46
- { code: '<DialogTrigger><span><Button>ほげ</Button></span></DialogTrigger>', errors: [ { message: 'DialogTrigger の直下にはbuttonコンポーネントのみ設置してください' } ] },
47
- { code: '<DropdownTrigger><AnchorButton>ほげ</AnchorButton></DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下にはbuttonコンポーネントのみ設置してください' } ] },
48
- { code: '<DropdownTrigger><ButtonAnchor>ほげ</ButtonAnchor></DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下にはbuttonコンポーネントのみ設置してください' } ] },
36
+ { code: 'const Hoge = styled.button``', errors: [ { message: `Hogeを正規表現 "/Button$/" がmatchする名称に変更してください` } ] },
37
+ { code: 'const Hoge = styled.a``', errors: [ { message: `Hogeを正規表現 "/(Anchor|Link)$/" がmatchする名称に変更してください` } ] },
38
+ { code: 'const Hoge = styled(Button)``', errors: [ { message: `Hogeを正規表現 "/Button$/" がmatchする名称に変更してください` } ] },
39
+ { code: 'const Hoge = styled(AnchorButton)``', errors: [ { message: `Hogeを正規表現 "/Button$/" がmatchする名称に変更してください` },{ message: `Hogeを正規表現 "/AnchorButton$/" がmatchする名称に変更してください` } ] },
40
+ { code: 'const Hoge = styled(ButtonAnchor)``', errors: [ { message: `Hogeを正規表現 "/ButtonAnchor$/" がmatchする名称に変更してください` }, { message: `Hogeを正規表現 "/Anchor$/" がmatchする名称に変更してください` } ] },
41
+ { code: 'const Hoge = styled(Anchor)``', errors: [ { message: `Hogeを正規表現 "/Anchor$/" がmatchする名称に変更してください` } ] },
42
+ { code: 'const Hoge = styled(Link)``', errors: [ { message: `Hogeを正規表現 "/Link$/" がmatchする名称に変更してください` } ] },
43
+ { code: 'const Hoge = styled(DropdownTrigger)``', errors: [ { message: `Hogeを正規表現 "/DropdownTrigger$/" がmatchする名称に変更してください` } ] },
44
+ { code: 'const Hoge = styled(DialogTrigger)``', errors: [ { message: `Hogeを正規表現 "/DialogTrigger$/" がmatchする名称に変更してください` } ] },
45
+ { code: '<DropdownTrigger>ほげ</DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下にはbuttonコンポーネントのみ設置してください' } ] },
46
+ { code: '<DialogTrigger><span><Button>ほげ</Button></span></DialogTrigger>', errors: [ { message: 'DialogTrigger の直下にはbuttonコンポーネントのみ設置してください' } ] },
47
+ { code: '<DropdownTrigger><AnchorButton>ほげ</AnchorButton></DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下にはbuttonコンポーネントのみ設置してください' } ] },
48
+ { code: '<DropdownTrigger><ButtonAnchor>ほげ</ButtonAnchor></DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下にはbuttonコンポーネントのみ設置してください' } ] },
49
+ { code: '<DialogTrigger><button>{hoge}</button>{hoge}</DialogTrigger>', errors: [ { message: 'DialogTrigger の直下には複数のコンポーネントを設置することは出来ません。buttonコンポーネントが一つだけ設置されている状態にしてください' } ] },
50
+ { code: '<DropdownTrigger>{hoge}<span>text</span></DropdownTrigger>', errors: [ { message: 'DropdownTrigger の直下には複数のコンポーネントを設置することは出来ません。buttonコンポーネントが一つだけ設置されている状態にしてください' } ] },
49
51
  ]
50
52
  })