eslint-plugin-smarthr 0.0.0 → 0.1.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 CHANGED
@@ -2,9 +2,28 @@
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.0.2](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.0.1...v0.0.2) (2022-01-26)
5
+ ### [0.1.1](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.1.0...v0.1.1) (2022-03-08)
6
+
7
+
8
+ ### Features
9
+
10
+ * add require-barrel-import rule ([#13](https://github.com/kufu/eslint-plugin-smarthr/issues/13)) ([79ee88d](https://github.com/kufu/eslint-plugin-smarthr/commit/79ee88d355e01bb8344dc95bd65157e2fbcf916e))
11
+
12
+ ## [0.1.0](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.0.1...v0.1.0) (2022-02-09)
13
+
14
+
15
+ ### ⚠ BREAKING CHANGES
16
+
17
+ * BREAKING CHANGE: add require-import & update prohibit-import (#12) ([e6c5c44](https://github.com/kufu/eslint-plugin-smarthr/commit/e6c5c445a21620d4b796ded00a685e5da367c7bb)), closes [#12](https://github.com/kufu/eslint-plugin-smarthr/issues/12)
18
+
19
+ ### [0.0.1](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.0.0...v0.0.1) (2022-02-08)
20
+
21
+
22
+ ### Features
23
+
24
+ * add type property function params redundant ([758df90](https://github.com/kufu/eslint-plugin-smarthr/commit/758df90f89bd27dd589aeeb55165e27c8e072b08))
25
+ * redundant-name の修正候補を操作できるように改修 ([20991e8](https://github.com/kufu/eslint-plugin-smarthr/commit/20991e874890556e84e7c682e789e4b2650a85b0))
6
26
 
7
- ### [0.0.1](https://github.com/kufu/eslint-plugin-smarthr/compare/v0.0.0...v0.0.1) (2022-01-26)
8
27
 
9
28
  ## 0.0.0 (2022-01-25)
10
29
 
package/README.md CHANGED
@@ -145,13 +145,19 @@ import globalModulePart from '@/modules/views/parts'
145
145
  ## smarthr/jsx-start-with-spread-attributes
146
146
 
147
147
  - jsxを記述する際、意図しない属性の上書きを防ぐため、spread-attributesを先に指定するように強制するruleです
148
+ - eslint を `--fix` オプション付きで実行する際、 fix option を true にすると自動修正します
148
149
 
149
150
  ### rules
150
151
 
151
152
  ```js
152
153
  {
153
154
  rules: {
154
- 'smarthr/jsx-start-with-spread-attributes': 'error', // 'warn', 'off'
155
+ 'smarthr/jsx-start-with-spread-attributes': [
156
+ 'error', // 'warn', 'off'
157
+ {
158
+ fix: false, // true
159
+ },
160
+ ]
155
161
  },
156
162
  }
157
163
  ```
@@ -268,12 +274,17 @@ import globalModulePart from '@/modules/views/parts'
268
274
  'smarthr/prohibit-import': [
269
275
  'error', // 'warn', 'off'
270
276
  {
271
- targets: {
272
- '^query-string$': true, // key は 正規表現を指定する
273
- '^smarthr-ui$': ['SecondaryButtonAnchor'],
277
+ '^.+$': {
278
+ 'smarthr-ui': {
279
+ imported: ['SecondaryButtonAnchor'],
280
+ reportMessage: `{{module}}/{{export}} はXxxxxxなので利用せず yyyy/zzzz を利用してください`
281
+ },
282
+ }
283
+ '\/pages\/views\/': {
284
+ 'query-string': {
285
+ imported: true,
286
+ },
274
287
  },
275
- // generateReportMessage: (source, imported) =>
276
- // `${source}${imported && `/${imported}`} はXxxxxxなので利用せず yyyy/zzzz を利用してください`
277
288
  }
278
289
  ]
279
290
  },
@@ -283,6 +294,7 @@ import globalModulePart from '@/modules/views/parts'
283
294
  ### ❌ Incorrect
284
295
 
285
296
  ```js
297
+ // src/pages/views/Page.tsx
286
298
  import queryString from 'query-string'
287
299
  import { SecondaryButtonAnchor } from 'smarthr-ui'
288
300
  ```
@@ -291,10 +303,101 @@ import { SecondaryButtonAnchor } from 'smarthr-ui'
291
303
 
292
304
 
293
305
  ```js
306
+ // src/pages/views/Page.tsx
294
307
  import { PrimaryButton, SecondaryButton } from 'smarthr-ui'
295
308
  ```
296
309
 
310
+ ## smarthr/require-import
311
+
312
+ - 対象ファイルにimportを強制させたい場合に利用します
313
+ - 例: Page.tsx ではページタイトルを設定させたいので useTitle を必ずimportさせたい
314
+
315
+ ### rules
316
+
317
+ ```js
318
+ {
319
+ rules: {
320
+ 'smarthr/require-import': [
321
+ 'error',
322
+ {
323
+ 'Buttons\/.+\.tsx': {
324
+ 'smarthr-ui': {
325
+ imported: ['SecondaryButton'],
326
+ reportMessage: 'Buttons以下のコンポーネントでは {{module}}/{{export}} を拡張するようにしてください',
327
+ },
328
+ },
329
+ 'Page.tsx$': {
330
+ './client/src/hooks/useTitle': {
331
+ imported: true,
332
+ reportMessage: '{{module}} を利用してください(ページタイトルを設定するため必要です)',
333
+ },
334
+ },
335
+ },
336
+ ]
337
+ },
338
+ }
339
+ ```
340
+
341
+ ### ❌ Incorrect
342
+
343
+ ```js
344
+ // client/src/Buttons/SecondaryButton.tsx
345
+ import { SecondaryButtonAnchor } from 'smarthr-ui'
346
+
347
+ // client/src/Page.tsx
348
+ import { SecondaryButton } from 'smarthr-ui'
349
+ ```
350
+
351
+ ### ✅ Correct
352
+
353
+
354
+ ```js
355
+ // client/src/Buttons/SecondaryButton.tsx
356
+ import { SecondaryButton } from 'smarthr-ui'
357
+
358
+ // client/src/Page.tsx
359
+ import useTitle from '.hooks/useTitle'
360
+ ```
361
+
362
+ ## smarthr/require-barrel-import
363
+
364
+ - tsconfig.json の compilerOptions.pathsに '@/*' としてroot path を指定する必要があります
365
+ - importした対象が本来exportされているべきであるbarrel(index.tsなど)が有る場合、import pathの変更を促します
366
+ - 例: Page/parts/Menu/Item の import は Page/parts/Menu から行わせたい
367
+ - ディレクトリ内のindexファイルを捜査し、対象を決定します
368
+
369
+ ### rules
370
+
371
+ ```js
372
+ {
373
+ rules: {
374
+ 'smarthr/require-barrel-import': 'error',
375
+ },
376
+ }
377
+ ```
378
+
379
+ ### ❌ Incorrect
297
380
 
381
+ ```js
382
+ // client/src/views/Page/parts/Menu/index.ts
383
+ export { Menu } from './Menu'
384
+ export { Item } from './Item'
385
+
386
+ // client/src/App.tsx
387
+ import { Item } from './Page/parts/Menu/Item'
388
+ ```
389
+
390
+ ### ✅ Correct
391
+
392
+
393
+ ```js
394
+ // client/src/views/Page/parts/Menu/index.ts
395
+ export { Menu } from './Menu'
396
+ export { Item } from './Item'
397
+
398
+ // client/src/App.tsx
399
+ import { Item } from './Page/parts/Menu'
400
+ ```
298
401
 
299
402
 
300
403
  ## smarthr/redundant-name
@@ -305,47 +408,14 @@ import { PrimaryButton, SecondaryButton } from 'smarthr-ui'
305
408
  ### config
306
409
 
307
410
  - tsconfig.json の compilerOptions.pathsに '@/*' としてroot path を指定する必要があります
308
- - 以下の設定を記述する必要があります
411
+ - 以下の設定を行えます。全て省略可能です。
309
412
  - ignoreKeywords
310
413
  - ディレクトリ名から生成されるキーワードに含めたくない文字列を指定します
311
- - keywordGenerator
312
- - ディレクトリ名から生成されるキーワードを元に最終的なキーワードを生成します
313
- - suffixGenerator:
414
+ - betterNames
415
+ - 対象の名前を修正する候補を指定します
416
+ - suffix:
314
417
  - type のみ指定出来ます
315
- - type のsuffixを生成します
316
-
317
- #### 指定例
318
- ```
319
- const ignorekeywords = ['views', 'parts']
320
- const keywordGenerator = ({ keywords }) => (
321
- keywords.reduce((prev, keyword, index) => {
322
- switch (keyword) {
323
- case 'repositories':
324
- return [...prev, keyword, 'repository']
325
- }
326
-
327
- const replacedKeyword = keyword.replace(/(repository|s)$/, '')
328
- if (keyword !== replacedKeyword) {
329
- return [...prev, keyword, replacedKeyword]
330
- }
331
-
332
- return [...prev, keyword]
333
- }, [])
334
- )
335
- // 例: actions 以下の場合だけ 'Action' もしくは `Actions` のSuffixを許可する
336
- const suffixGenerator = ({ node, filename }) => {
337
- let suffix = ['Props', 'Type']
338
-
339
- if (filename.match(/\/actions\//)) {
340
- suffix = [
341
- isUnionType || (node.typeAnnotation.type === 'TSTypeReference' && node.id.name.match(/Actions$/))
342
- ? 'Actions'
343
- : 'Action',
344
- ...suffix,
345
- ]
346
- }
347
- }
348
- ```
418
+ - type のsuffixを指定します
349
419
 
350
420
  #### ファイル例
351
421
  - `@/crews/index/views/page.tsx` の場合
@@ -359,17 +429,33 @@ const suffixGenerator = ({ node, filename }) => {
359
429
  ### rules
360
430
 
361
431
  ```js
432
+ const ignorekeywords = ['views', 'parts']
433
+ const betterNames = {
434
+ '\/repositories\/': {
435
+ operator: '-',
436
+ names: ['repository', 'Repository'],
437
+ },
438
+ '\/entities\/': {
439
+ operator: '+',
440
+ names: ['entity'],
441
+ },
442
+ '\/slices\/': {
443
+ operator: '=',
444
+ names: ['index'],
445
+ },
446
+ }
447
+
362
448
  {
363
449
  rules: {
364
450
  'smarthr/redundant-name': [
365
451
  'error', // 'warn', 'off'
366
452
  {
367
- type: { ignorekeywords, keywordGenerator, suffixGenerator },
368
- file: { ignorekeywords, keywordGenerator },
369
- // property: { ignorekeywords, keywordGenerator },
370
- // function: { ignorekeywords, keywordGenerator },
371
- // variable: { ignorekeywords, keywordGenerator },
372
- // class: { ignorekeywords, keywordGenerator },
453
+ type: { ignorekeywords, suffix: ['Props', 'Type'] },
454
+ file: { ignorekeywords, betternames },
455
+ // property: { ignorekeywords },
456
+ // function: { ignorekeywords },
457
+ // variable: { ignorekeywords },
458
+ // class: { ignorekeywords },
373
459
  }
374
460
  ]
375
461
  },
package/jest.config.js ADDED
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ moduleFileExtensions: ['js'],
3
+ testMatch: ['**/test/**/*.+(js)'],
4
+ roots: ['<rootDir>/']
5
+ }
@@ -6,7 +6,6 @@ const BASE_SCHEMA_PROPERTIES = {
6
6
  globalModuleDir: { type: 'array', items: { type: 'string' } },
7
7
  domainModuleDir: { type: 'array', items: { type: 'string' }, default: [] },
8
8
  domainConstituteDir: { type: 'array', items: { type: 'string' } },
9
- isDomain: { type: 'function' },
10
9
  }
11
10
 
12
11
  const calculateDomainContext = (context) => {
@@ -87,13 +86,14 @@ const calculateDomainNode = (calclatedContext, node) => {
87
86
  let filteredDirs = dirs
88
87
  let filteredPaths = paths
89
88
 
89
+ const deductedNames = []
90
90
  const recursiveDeductionEq = () => {
91
91
  if (dirs.length === 0 || paths.length === 0) {
92
92
  return
93
93
  }
94
94
 
95
95
  if (dirs[0] === paths[0]) {
96
- dirs.shift()
96
+ deductedNames.push(dirs.shift())
97
97
  paths.shift()
98
98
  recursiveDeductionEq()
99
99
  }
@@ -102,21 +102,22 @@ const calculateDomainNode = (calclatedContext, node) => {
102
102
  filteredDirs = dirs
103
103
  filteredPaths = paths
104
104
 
105
+ let isDomainConstitute = false
106
+
105
107
  if (option.domainConstituteDir) {
108
+ const { domainConstituteDir } = option
109
+ isDomainConstitute =
110
+ !!deductedNames.find((d) => domainConstituteDir.includes(d)) || // 同一dirを削り、その中にconstitute dir があれば同一ドメイン
111
+ domainConstituteDir.includes(dirs[0]) && domainConstituteDir.includes(paths[0]) // 同一を削りきった先頭が両方constitute dirならば同一ドメイン
112
+
106
113
  // HINT: 同一ドメイン内(例: workflows/index)で定形で利用されるディレクトリ名を省くことで
107
114
  // ドメインの識別に利用される文字を抽出する
108
115
  dirs = dirs.filter((k) => !option.domainConstituteDir.includes(k))
109
116
  paths = paths.filter((k) => !option.domainConstituteDir.includes(k))
110
117
  }
111
118
 
112
- if (option.isDomain) {
113
- // HINT: ドメインの識別に利用される文字を抽出する
114
- dirs = dirs.filter((k) => option.isDomain(k))
115
- paths = paths.filter((k) => option.isDomain(k))
116
- }
117
-
118
119
  const isLowerImport = filteredDirs.length === 0 // 同一階層、もしくは下層からのimport
119
- const isDomainImport = dirs.length === 0 // 同一ドメイン内、もしくは同一階層・下層からのimport
120
+ const isDomainImport = dirs.length === 0 || isDomainConstitute // 同一ドメイン内、もしくは同一階層・下層からのimport
120
121
  const isModuleImport = paths.length > 0 && option.domainModuleDir.includes(paths[0]) // ドメイン内共通パーツ
121
122
 
122
123
  return {
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "0.0.0",
3
+ "version": "0.1.1",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
+ "test": "jest",
9
10
  "release:dryrun": "standard-version --dry-run",
10
11
  "release": "standard-version"
11
12
  },
@@ -21,9 +22,12 @@
21
22
  "url": "https://github.com/kufu/eslint-plugin-smarthr/issues"
22
23
  },
23
24
  "dependencies": {
25
+ "inflected": "^2.1.0",
24
26
  "json5": "^2.2.0"
25
27
  },
26
28
  "devDependencies": {
29
+ "eslint": "^8.8.0",
30
+ "jest": "^27.4.7",
27
31
  "standard-version": "^9.3.2"
28
32
  },
29
33
  "peerDependencies": {
@@ -1,38 +1,72 @@
1
+ const SCHEMA = [
2
+ {
3
+ type: 'object',
4
+ properties: {
5
+ fix: { type: 'boolean', default: false },
6
+ },
7
+ additionalProperties: false,
8
+ }
9
+ ]
10
+
1
11
  module.exports = {
2
12
  meta: {
3
13
  type: 'suggestion',
4
14
  messages: {
5
15
  'jsx-start-with-spread-attributes': '{{ message }}',
6
16
  },
7
- schema: [],
17
+ fixable: 'code',
18
+ schema: SCHEMA,
8
19
  },
9
20
  create(context) {
10
21
  return {
11
22
  JSXSpreadAttribute: (node) => {
12
- // HINT: 0: 計算中 1: 見つからなかった 2: 見つかった
13
- const hit = node.parent.attributes.reduce((h, a) => {
14
- if (h === 0) {
23
+ // HINT: -2: 計算中 -1: 見つからなかった >= 0: 見つかった
24
+ const insertIndex = node.parent.attributes.reduce((h, a, i) => {
25
+ if (h === -2) {
15
26
  if (a === node) {
16
- return 1
27
+ return -1
17
28
  }
18
29
 
19
- return a.type !== 'JSXSpreadAttribute' ? 2 : 0
30
+ return a.type !== 'JSXSpreadAttribute' ? i : h
20
31
  }
21
32
 
22
33
  return h
23
- }, 0)
34
+ }, -2)
35
+
36
+ if (insertIndex >= 0) {
37
+ const option = context.options[0]
38
+ const sourceCode = context.getSourceCode()
39
+ const attributeCode = sourceCode.getText(node)
24
40
 
25
- if (hit === 2) {
26
41
  context.report({
27
42
  node,
28
43
  messageId: 'jsx-start-with-spread-attributes',
29
44
  data: {
30
- message: `"${context.getSourceCode().getText(node)}" は他の属性より先に記述してください`,
45
+ message: `"${attributeCode}" は意図しない上書きを防ぐため、spread attributesでない属性より先に記述してください`,
31
46
  },
47
+ fix: option?.fix ? (fixer) => {
48
+ const elementNode = node.parent
49
+ const sortedAttributes = [...elementNode.attributes].reduce((p, a, i) => {
50
+ if (insertIndex === i) {
51
+ p = [attributeCode, ...p]
52
+ }
53
+
54
+ if (a !== node) {
55
+ p = [...p, sourceCode.getText(a)]
56
+ }
57
+
58
+ return p
59
+ }, [])
60
+
61
+ return fixer.replaceText(
62
+ elementNode,
63
+ `<${elementNode.name.name} ${sortedAttributes.join(' ')}${elementNode.selfClosing ? '/' : ''}>`
64
+ )
65
+ } : null
32
66
  });
33
67
  }
34
68
  },
35
69
  }
36
70
  },
37
71
  }
38
- module.exports.schema = []
72
+ module.exports.schema = SCHEMA
@@ -1,15 +1,36 @@
1
- const SCHEMA = [
2
- {
3
- type: 'object',
4
- properties: {
5
- targets: { type: 'object', default: {} },
6
- generateReportMessage: { type: 'function' },
1
+ const path = require('path')
2
+
3
+ const SCHEMA = [{
4
+ type: 'object',
5
+ patternProperties: {
6
+ '.+': {
7
+ type: 'object',
8
+ patternProperties: {
9
+ '.+': {
10
+ type: 'object',
11
+ required: [
12
+ 'imported',
13
+ ],
14
+ properties: {
15
+ imported: {
16
+ type: ['boolean', 'array'],
17
+ items: {
18
+ type: 'string',
19
+ },
20
+ },
21
+ reportMessage: {
22
+ type: 'string',
23
+ },
24
+ },
25
+ additionalProperties: false
26
+ }
27
+ }
7
28
  },
8
- additionalProperties: false,
9
- }
10
- ]
29
+ },
30
+ additionalProperties: true,
31
+ }]
11
32
 
12
- const generateDefaultReportMessage = (source, imported) => `${source}${imported && `/${imported}`} は利用しないでください`
33
+ const defaultReportMessage = (moduleName, exportName) => `${moduleName}${typeof exportName == 'string' ? `/${exportName}`: ''} は利用しないでください`
13
34
 
14
35
  module.exports = {
15
36
  meta: {
@@ -20,38 +41,64 @@ module.exports = {
20
41
  schema: SCHEMA,
21
42
  },
22
43
  create(context) {
23
- const option = context.options[0]
24
- const parsedOption = Object.entries(option.targets)
25
- const generateReportMessage = option.generateReportMessage || generateDefaultReportMessage
44
+ const options = context.options[0]
45
+ const filename = context.getFilename()
46
+ const parentDir = (() => {
47
+ const dir = filename.match(/^(.+?)\..+?$/)[1].split('/')
48
+ dir.pop()
49
+
50
+ return dir.join('/')
51
+ })()
52
+ const targetPathRegexs = Object.keys(options)
53
+ const targetProhibits = targetPathRegexs.filter((regex) => !!filename.match(new RegExp(regex)))
54
+
55
+ if (targetProhibits.length === 0) {
56
+ return {}
57
+ }
26
58
 
27
59
  return {
28
60
  ImportDeclaration: (node) => {
29
- parsedOption.forEach(([matchText, importNames]) => {
30
- if (!node.source.value.match(new RegExp(matchText))) {
31
- return
32
- }
33
-
34
- const imported = (() => {
35
- if (!Array.isArray(importNames)) {
36
- return !!importNames
61
+ targetProhibits.forEach((prohibitKey) => {
62
+ const option = options[prohibitKey]
63
+ const targetModules = Object.keys(option)
64
+
65
+ targetModules.forEach((targetModule) => {
66
+ const { imported, reportMessage } = Object.assign({imported: true}, option[targetModule])
67
+ const actualTarget = targetModule[0] !== '.' ? targetModule : path.resolve(`${process.cwd()}/${targetModule}`)
68
+ let sourceValue = node.source.value
69
+
70
+ if (actualTarget[0] === '/') {
71
+ sourceValue = path.resolve(`${parentDir}/${sourceValue}`)
72
+ }
73
+
74
+ if (actualTarget !== sourceValue) {
75
+ return
37
76
  }
38
- const specifier = node.specifiers.find((s) => importNames.includes(s.imported.name))
77
+
78
+ const useImported = (() => {
79
+ if (!Array.isArray(imported)) {
80
+ return !!imported
81
+ }
39
82
 
40
- return specifier ? specifier.imported.name : false
41
- })()
83
+ const specifier = node.specifiers.find((s) => s.imported && imported.includes(s.imported.name))
42
84
 
43
- if (imported) {
44
- context.report({
45
- node,
46
- messageId: 'prohibit_import',
47
- data: {
48
- message: generateReportMessage(node.source.value, imported === true ? '' : imported),
49
- },
50
- });
51
- }
85
+ return specifier ? specifier.imported.name : false
86
+ })()
87
+
88
+ if (useImported) {
89
+ context.report({
90
+ node,
91
+ messageId: 'prohibit_import',
92
+ data: {
93
+ message: reportMessage ? reportMessage.replace('{{module}}', node.source.value).replace('{{export}}', useImported) : defaultReportMessage(node.source.value, useImported)
94
+ },
95
+ });
96
+ }
97
+ })
52
98
  })
53
99
  },
54
100
  }
55
101
  },
56
102
  }
103
+
57
104
  module.exports.schema = SCHEMA