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 +21 -2
- package/README.md +136 -50
- package/jest.config.js +5 -0
- package/libs/common_domain.js +10 -9
- package/package.json +5 -1
- package/rules/jsx-start-with-spread-attributes.js +44 -10
- package/rules/prohibit-import.js +80 -33
- package/rules/redundant-name.js +206 -86
- package/rules/require-barrel-import.js +125 -0
- package/rules/require-import.js +113 -0
- package/test/prohibit-import.js +201 -0
- package/test/require-import.js +184 -0
package/rules/redundant-name.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
const path = require('path')
|
|
2
|
+
const Inflector = require('inflected')
|
|
3
|
+
|
|
2
4
|
const { rootPath } = require('../libs/common')
|
|
3
5
|
|
|
4
6
|
const uniq = (array) => array.filter((elem, index, self) => self.indexOf(elem) === index)
|
|
5
7
|
|
|
8
|
+
const COMMON_DEFAULT_CONFIG = {
|
|
9
|
+
IGNORE_KEYWORDS: ['redux', 'views', 'pages', 'parts'],
|
|
10
|
+
}
|
|
6
11
|
const DEFAULT_CONFIG = {
|
|
7
12
|
type: {
|
|
8
13
|
IGNORE_KEYWORDS: [
|
|
@@ -11,44 +16,56 @@ const DEFAULT_CONFIG = {
|
|
|
11
16
|
],
|
|
12
17
|
SUFFIX: ['Props', 'Type'],
|
|
13
18
|
},
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
},
|
|
23
|
-
variable: {
|
|
24
|
-
IGNORE_KEYWORDS: ['redux', 'views', 'pages', 'parts'],
|
|
25
|
-
},
|
|
26
|
-
class: {
|
|
27
|
-
IGNORE_KEYWORDS: ['redux', 'views', 'pages', 'parts'],
|
|
28
|
-
},
|
|
29
|
-
method: {
|
|
30
|
-
IGNORE_KEYWORDS: ['redux', 'views', 'pages', 'parts'],
|
|
31
|
-
},
|
|
19
|
+
typeProperty: COMMON_DEFAULT_CONFIG,
|
|
20
|
+
file: COMMON_DEFAULT_CONFIG,
|
|
21
|
+
property: COMMON_DEFAULT_CONFIG,
|
|
22
|
+
function: COMMON_DEFAULT_CONFIG,
|
|
23
|
+
functionParams: COMMON_DEFAULT_CONFIG,
|
|
24
|
+
variable: COMMON_DEFAULT_CONFIG,
|
|
25
|
+
class: COMMON_DEFAULT_CONFIG,
|
|
26
|
+
method: COMMON_DEFAULT_CONFIG,
|
|
32
27
|
}
|
|
33
28
|
|
|
29
|
+
const BETTER_NAMES_CALCULATER_PROPERTY = {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
operator: ['-', '+', '='],
|
|
33
|
+
names: {
|
|
34
|
+
type: 'array',
|
|
35
|
+
items: 'string',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}
|
|
34
39
|
const DEFAULT_SCHEMA_PROPERTY = {
|
|
35
40
|
ignoreKeywords: { type: 'array', items: { type: 'string' } },
|
|
36
|
-
|
|
41
|
+
betterNames: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
operator: ['-', '+', '='],
|
|
45
|
+
names: {
|
|
46
|
+
type: 'array',
|
|
47
|
+
items: 'string',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
const SCHEMA = [
|
|
40
54
|
{
|
|
41
55
|
type: 'object',
|
|
42
56
|
properties: {
|
|
43
|
-
file: DEFAULT_SCHEMA_PROPERTY,
|
|
44
57
|
type: {
|
|
45
58
|
...DEFAULT_SCHEMA_PROPERTY,
|
|
46
|
-
|
|
59
|
+
suffix: { type: 'array', items: { type: 'string' } },
|
|
47
60
|
},
|
|
61
|
+
typeProperty: DEFAULT_SCHEMA_PROPERTY,
|
|
62
|
+
file: DEFAULT_SCHEMA_PROPERTY,
|
|
48
63
|
property: DEFAULT_SCHEMA_PROPERTY,
|
|
49
64
|
function: DEFAULT_SCHEMA_PROPERTY,
|
|
65
|
+
functionParams: DEFAULT_SCHEMA_PROPERTY,
|
|
50
66
|
variable: DEFAULT_SCHEMA_PROPERTY,
|
|
51
67
|
class: DEFAULT_SCHEMA_PROPERTY,
|
|
68
|
+
method: DEFAULT_SCHEMA_PROPERTY,
|
|
52
69
|
},
|
|
53
70
|
additionalProperties: false,
|
|
54
71
|
}
|
|
@@ -68,21 +85,22 @@ const generateRedundantKeywords = ({ args, key, terminalImportName }) => {
|
|
|
68
85
|
const option = args.option[key] || {}
|
|
69
86
|
const ignoreKeywords = option.ignoreKeywords || DEFAULT_CONFIG[key].IGNORE_KEYWORDS
|
|
70
87
|
const terminalImportKeyword = terminalImportName ? terminalImportName.toLowerCase() : ''
|
|
71
|
-
const filterKeywords = (keys) => keys.filter((k) => k !== terminalImportKeyword && !ignoreKeywords.includes(k))
|
|
72
88
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
89
|
+
return args.keywords.reduce((prev, keyword) => {
|
|
90
|
+
if (keyword === terminalImportKeyword || ignoreKeywords.includes(keyword)) {
|
|
91
|
+
return prev
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const singularized = Inflector.singularize(keyword)
|
|
80
95
|
|
|
81
|
-
|
|
96
|
+
return singularized === keyword ? [...prev, keyword] : [...prev, keyword, singularized]
|
|
97
|
+
}, [])
|
|
82
98
|
}
|
|
83
99
|
const handleReportBetterName = ({
|
|
84
100
|
key,
|
|
85
101
|
context,
|
|
102
|
+
option,
|
|
103
|
+
filename,
|
|
86
104
|
redundantKeywords,
|
|
87
105
|
defaultBetterName,
|
|
88
106
|
fetchName,
|
|
@@ -99,40 +117,75 @@ const handleReportBetterName = ({
|
|
|
99
117
|
return
|
|
100
118
|
}
|
|
101
119
|
|
|
102
|
-
let
|
|
103
|
-
let
|
|
104
|
-
const
|
|
120
|
+
let candidates = []
|
|
121
|
+
let conciseName = redundantKeywords.reduce((prev, keyword) => {
|
|
122
|
+
const regex = new RegExp(`(${keyword})`, 'i')
|
|
123
|
+
const matcher = prev.match(regex)
|
|
124
|
+
|
|
125
|
+
if (matcher) {
|
|
126
|
+
candidates.push(matcher[1])
|
|
105
127
|
|
|
106
|
-
|
|
107
|
-
hitCount++
|
|
128
|
+
return prev.replace(regex, '')
|
|
108
129
|
}
|
|
109
130
|
|
|
110
|
-
return
|
|
131
|
+
return prev
|
|
111
132
|
}, name)
|
|
112
133
|
|
|
113
|
-
if (name !==
|
|
114
|
-
|
|
134
|
+
if (name !== conciseName) {
|
|
135
|
+
conciseName = conciseName
|
|
115
136
|
.replace(/^_+/, '')
|
|
116
137
|
.replace(/_+$/, '')
|
|
117
138
|
.replace(/_+/, '_')
|
|
139
|
+
let fullRedundant = false
|
|
118
140
|
|
|
119
|
-
if (!
|
|
141
|
+
if (!conciseName) {
|
|
142
|
+
fullRedundant = true
|
|
120
143
|
// HINT: 1keywordで構成されている名称はそのままにする
|
|
121
|
-
|
|
144
|
+
conciseName = candidates.length === 1 ? name : defaultBetterName
|
|
122
145
|
}
|
|
123
146
|
|
|
124
147
|
// HINT: camelCase、lower_snake_case の場合、keywordが取り除かれた結果違うケースになってしまう場合があるので対応する
|
|
125
|
-
if (name.match(/^[a-z]/) &&
|
|
126
|
-
|
|
148
|
+
if (name.match(/^[a-z]/) && conciseName.match(/^[A-Z]/)) {
|
|
149
|
+
conciseName = `${conciseName[0].toLowerCase()}${conciseName.slice(1)}`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (fullRedundant) {
|
|
153
|
+
if (name.match(/^[A-Z]/)) {
|
|
154
|
+
candidates = candidates.map((k) => `${k[0].toUpperCase()}${k.slice(1)}`)
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
candidates = []
|
|
127
158
|
}
|
|
159
|
+
|
|
160
|
+
candidates = uniq([conciseName, ...candidates].filter((k) => !!k))
|
|
161
|
+
|
|
162
|
+
if (option.betterNames) {
|
|
163
|
+
Object.entries(option.betterNames).forEach(([regex, calc]) => {
|
|
164
|
+
if (calc && filename.match(new RegExp(regex))) {
|
|
165
|
+
switch(calc.operator) {
|
|
166
|
+
case '=':
|
|
167
|
+
candidates = calc.names
|
|
168
|
+
break
|
|
169
|
+
case '-':
|
|
170
|
+
candidates = candidates.filter((c) => !calc.names.includes(c))
|
|
171
|
+
break
|
|
172
|
+
case '+':
|
|
173
|
+
candidates = uniq([...candidates, ...calc.names])
|
|
174
|
+
break
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
candidates = candidates.filter((c) => c !== name)
|
|
128
181
|
}
|
|
129
182
|
|
|
130
|
-
if (
|
|
183
|
+
if (candidates.length > 0) {
|
|
131
184
|
context.report({
|
|
132
185
|
node,
|
|
133
186
|
messageId: `${key}-name`,
|
|
134
187
|
data: {
|
|
135
|
-
message: generateMessage({ name, betterName }),
|
|
188
|
+
message: generateMessage({ name, betterName: candidates.join(', ') }),
|
|
136
189
|
},
|
|
137
190
|
});
|
|
138
191
|
}
|
|
@@ -145,17 +198,10 @@ const generateTypeRedundant = (args) => {
|
|
|
145
198
|
const redundantKeywords = generateRedundantKeywords({ args, key })
|
|
146
199
|
const option = args.option[key]
|
|
147
200
|
const defaultConfig = DEFAULT_CONFIG[key]
|
|
148
|
-
const actualArgs = {
|
|
149
|
-
...args,
|
|
150
|
-
redundantKeywords,
|
|
151
|
-
}
|
|
152
201
|
|
|
153
202
|
return (node) => {
|
|
154
203
|
const typeName = node.id.name
|
|
155
|
-
const suffix = option.
|
|
156
|
-
...actualArgs,
|
|
157
|
-
node,
|
|
158
|
-
}) : defaultConfig.SUFFIX
|
|
204
|
+
const suffix = option.suffix || defaultConfig.SUFFIX
|
|
159
205
|
|
|
160
206
|
let SuffixedName = typeName
|
|
161
207
|
let report = null
|
|
@@ -197,12 +243,41 @@ const generateTypeRedundant = (args) => {
|
|
|
197
243
|
}
|
|
198
244
|
}
|
|
199
245
|
|
|
246
|
+
const generateTypePropertyRedundant = (args) => {
|
|
247
|
+
const key = 'typeProperty'
|
|
248
|
+
|
|
249
|
+
return handleReportBetterName({
|
|
250
|
+
...args,
|
|
251
|
+
key,
|
|
252
|
+
option: args.option[key],
|
|
253
|
+
redundantKeywords: generateRedundantKeywords({ args, key }),
|
|
254
|
+
defaultBetterName: '',
|
|
255
|
+
fetchName: (node) => node.key.name,
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
const generateTypePropertyFunctionParamsRedundant = (args) => {
|
|
259
|
+
const key = 'typeProperty'
|
|
260
|
+
const redundant = handleReportBetterName({
|
|
261
|
+
...args,
|
|
262
|
+
key,
|
|
263
|
+
option: args.option[key],
|
|
264
|
+
redundantKeywords: generateRedundantKeywords({ args, key }),
|
|
265
|
+
defaultBetterName: '',
|
|
266
|
+
fetchName: (node) => node.name,
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
return (node) => {
|
|
270
|
+
node.params.forEach((param) => redundant(param))
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
200
274
|
const generatePropertyRedundant = (args) => {
|
|
201
275
|
const key = 'property'
|
|
202
276
|
|
|
203
277
|
return handleReportBetterName({
|
|
278
|
+
...args,
|
|
204
279
|
key,
|
|
205
|
-
|
|
280
|
+
option: args.option[key],
|
|
206
281
|
redundantKeywords: generateRedundantKeywords({ args, key }),
|
|
207
282
|
defaultBetterName: 'item',
|
|
208
283
|
fetchName: (node) => node.key.name,
|
|
@@ -214,8 +289,9 @@ const generateFileRedundant = (args) => {
|
|
|
214
289
|
const terminalImportName = fetchTerminalImportName(args.filename)
|
|
215
290
|
|
|
216
291
|
return handleReportBetterName({
|
|
292
|
+
...args,
|
|
217
293
|
key,
|
|
218
|
-
|
|
294
|
+
option: args.option[key],
|
|
219
295
|
redundantKeywords: generateRedundantKeywords({ args, key, terminalImportName }),
|
|
220
296
|
defaultBetterName: 'index',
|
|
221
297
|
fetchName: () => terminalImportName,
|
|
@@ -227,20 +303,37 @@ const generateFunctionRedundant = (args) => {
|
|
|
227
303
|
const key = 'function'
|
|
228
304
|
|
|
229
305
|
return handleReportBetterName({
|
|
306
|
+
...args,
|
|
230
307
|
key,
|
|
231
|
-
|
|
308
|
+
option: args.option[key],
|
|
232
309
|
redundantKeywords: generateRedundantKeywords({ args, key, terminalImportName: fetchTerminalImportName(args.filename) }),
|
|
233
310
|
defaultBetterName: '',
|
|
234
311
|
fetchName: (node) => node.id.name,
|
|
235
312
|
})
|
|
236
313
|
}
|
|
314
|
+
const generateFunctionParamsRedundant = (args) => {
|
|
315
|
+
const key = 'functionParams'
|
|
316
|
+
const redundant = handleReportBetterName({
|
|
317
|
+
...args,
|
|
318
|
+
key,
|
|
319
|
+
option: args.option[key],
|
|
320
|
+
redundantKeywords: generateRedundantKeywords({ args, key }),
|
|
321
|
+
defaultBetterName: '',
|
|
322
|
+
fetchName: (node) => node.name,
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
return (node) => {
|
|
326
|
+
node.params.forEach((param) => redundant(param))
|
|
327
|
+
}
|
|
328
|
+
}
|
|
237
329
|
|
|
238
330
|
const generateVariableRedundant = (args) => {
|
|
239
331
|
const key = 'variable'
|
|
240
332
|
|
|
241
333
|
return handleReportBetterName({
|
|
334
|
+
...args,
|
|
242
335
|
key,
|
|
243
|
-
|
|
336
|
+
option: args.option[key],
|
|
244
337
|
redundantKeywords: generateRedundantKeywords({ args, key, terminalImportName: fetchTerminalImportName(args.filename) }),
|
|
245
338
|
defaultBetterName: '',
|
|
246
339
|
fetchName: (node) => node.id.name,
|
|
@@ -251,8 +344,9 @@ const generateClassRedundant = (args) => {
|
|
|
251
344
|
const key = 'class'
|
|
252
345
|
|
|
253
346
|
return handleReportBetterName({
|
|
347
|
+
...args,
|
|
254
348
|
key,
|
|
255
|
-
|
|
349
|
+
option: args.option[key],
|
|
256
350
|
redundantKeywords: generateRedundantKeywords({ args, key, terminalImportName: fetchTerminalImportName(args.filename) }),
|
|
257
351
|
defaultBetterName: '',
|
|
258
352
|
fetchName: (node) => node.id.name,
|
|
@@ -263,8 +357,9 @@ const generateMethodRedundant = (args) => {
|
|
|
263
357
|
const key = 'method'
|
|
264
358
|
|
|
265
359
|
return handleReportBetterName({
|
|
360
|
+
...args,
|
|
266
361
|
key,
|
|
267
|
-
|
|
362
|
+
option: args.option[key],
|
|
268
363
|
redundantKeywords: generateRedundantKeywords({ args, key }),
|
|
269
364
|
defaultBetterName: 'item',
|
|
270
365
|
fetchName: (node) => node.key.name,
|
|
@@ -278,8 +373,10 @@ module.exports = {
|
|
|
278
373
|
'file-name': ' {{ message }}',
|
|
279
374
|
'type-name': '{{ message }}',
|
|
280
375
|
'type-name/invalid-suffix': '{{ message }}',
|
|
376
|
+
'typeProperty-name': '{{ message }}',
|
|
281
377
|
'property-name': ' {{ message }}',
|
|
282
378
|
'function-name': ' {{ message }}',
|
|
379
|
+
'functionParams-name': ' {{ message }}',
|
|
283
380
|
'variable-name': ' {{ message }}',
|
|
284
381
|
'class-name': ' {{ message }}',
|
|
285
382
|
'method-name': ' {{ message }}',
|
|
@@ -324,50 +421,73 @@ module.exports = {
|
|
|
324
421
|
keywords,
|
|
325
422
|
}
|
|
326
423
|
|
|
424
|
+
const addRule = (key, redundant) => {
|
|
425
|
+
const addedRules = rules[key] || []
|
|
426
|
+
|
|
427
|
+
rules[key] = [...addedRules, redundant]
|
|
428
|
+
}
|
|
429
|
+
|
|
327
430
|
if (option.type) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
431
|
+
addRule('TSTypeAliasDeclaration', generateTypeRedundant(args))
|
|
432
|
+
// addRule('TSInterfaceDeclaration', generateTypeRedundant(args)) // 必要になったら実装する
|
|
433
|
+
}
|
|
434
|
+
if (option.typeProperty) {
|
|
435
|
+
const typePropRedundant = generateTypePropertyRedundant(args)
|
|
436
|
+
const typeFuncParamRedundant = generateTypePropertyFunctionParamsRedundant(args)
|
|
437
|
+
|
|
438
|
+
addRule('TSPropertySignature', (node) => {
|
|
439
|
+
typePropRedundant(node)
|
|
440
|
+
|
|
441
|
+
if (node.typeAnnotation.typeAnnotation.type === 'TSFunctionType') {
|
|
442
|
+
typeFuncParamRedundant(node.typeAnnotation.typeAnnotation)
|
|
443
|
+
}
|
|
444
|
+
})
|
|
332
445
|
}
|
|
333
446
|
if (option.property) {
|
|
334
|
-
|
|
447
|
+
const redundant = generatePropertyRedundant(args)
|
|
335
448
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
Property: propRedundant,
|
|
339
|
-
PropertyDefinition: propRedundant,
|
|
340
|
-
}
|
|
449
|
+
addRule('Property', redundant)
|
|
450
|
+
addRule('PropertyDefinition', redundant)
|
|
341
451
|
}
|
|
342
452
|
if (option.file) {
|
|
343
|
-
|
|
344
|
-
...rules,
|
|
345
|
-
Program: generateFileRedundant(args),
|
|
346
|
-
}
|
|
453
|
+
addRule('Program', generateFileRedundant(args))
|
|
347
454
|
}
|
|
348
455
|
if (option.function) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
456
|
+
addRule('FunctionDeclaration', generateFunctionRedundant(args))
|
|
457
|
+
}
|
|
458
|
+
if (option.functionParams) {
|
|
459
|
+
const redundant = generateFunctionParamsRedundant(args)
|
|
460
|
+
|
|
461
|
+
addRule('FunctionDeclaration', redundant)
|
|
462
|
+
addRule('ArrowFunctionExpression', redundant)
|
|
463
|
+
addRule('MethodDefinition', (node) => {
|
|
464
|
+
if (node.value.type === 'FunctionExpression') {
|
|
465
|
+
redundant(node.value)
|
|
466
|
+
}
|
|
467
|
+
})
|
|
353
468
|
}
|
|
354
469
|
if (option.variable) {
|
|
355
470
|
const redundant = generateVariableRedundant(args)
|
|
356
471
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
VariableDeclarator: redundant,
|
|
360
|
-
TSEnumDeclaration: redundant,
|
|
361
|
-
}
|
|
472
|
+
addRule('VariableDeclarator', redundant)
|
|
473
|
+
addRule('TSEnumDeclaration', redundant)
|
|
362
474
|
}
|
|
363
475
|
if (option.class) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
476
|
+
addRule('ClassDeclaration', generateClassRedundant(args))
|
|
477
|
+
}
|
|
478
|
+
if (option.method) {
|
|
479
|
+
addRule('MethodDefinition', generateMethodRedundant(args))
|
|
369
480
|
}
|
|
370
481
|
|
|
482
|
+
Object.keys(rules).forEach((key) => {
|
|
483
|
+
const redundants = rules[key]
|
|
484
|
+
rules[key] = (node) => {
|
|
485
|
+
redundants.forEach((redundant) => {
|
|
486
|
+
redundant(node)
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
})
|
|
490
|
+
|
|
371
491
|
return rules
|
|
372
492
|
},
|
|
373
493
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const { replacePaths, rootPath } = require('../libs/common')
|
|
4
|
+
const calculateAbsoluteImportPath = (source) => {
|
|
5
|
+
if (source[0] === '/') {
|
|
6
|
+
return source
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return Object.entries(replacePaths).reduce((prev, [key, values]) => {
|
|
10
|
+
if (source === prev) {
|
|
11
|
+
return values.reduce((p, v) => {
|
|
12
|
+
if (prev === p) {
|
|
13
|
+
const regexp = new RegExp(`^${key}(.+)$`)
|
|
14
|
+
|
|
15
|
+
if (prev.match(regexp)) {
|
|
16
|
+
return p.replace(regexp, `${path.resolve(`${process.cwd()}/${v}`)}/$1`)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return p
|
|
21
|
+
}, prev)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return prev
|
|
25
|
+
}, source)
|
|
26
|
+
}
|
|
27
|
+
const calculateReplacedImportPath = (source) => {
|
|
28
|
+
return Object.entries(replacePaths).reduce((prev, [key, values]) => {
|
|
29
|
+
if (source === prev) {
|
|
30
|
+
return values.reduce((p, v) => {
|
|
31
|
+
if (prev === p) {
|
|
32
|
+
const regexp = new RegExp(`^${path.resolve(`${process.cwd()}/${v}`)}(.+)$`)
|
|
33
|
+
|
|
34
|
+
if (prev.match(regexp)) {
|
|
35
|
+
return p.replace(regexp, `${key}/$1`).replace(/(\/)+/g, '/')
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return p
|
|
40
|
+
}, prev)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return prev
|
|
44
|
+
}, source)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
meta: {
|
|
49
|
+
type: 'suggestion',
|
|
50
|
+
messages: {
|
|
51
|
+
'require-barrel-import': '{{ message }}',
|
|
52
|
+
},
|
|
53
|
+
schema: [],
|
|
54
|
+
},
|
|
55
|
+
create(context) {
|
|
56
|
+
const filename = context.getFilename()
|
|
57
|
+
|
|
58
|
+
// HINT: indexファイルがある == barrelであるとする
|
|
59
|
+
if (filename.match(/\/index\.(js|ts)x?$/)) {
|
|
60
|
+
return {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const dir = (() => {
|
|
64
|
+
const d = filename.split('/')
|
|
65
|
+
d.pop()
|
|
66
|
+
|
|
67
|
+
return d.join('/')
|
|
68
|
+
})()
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
ImportDeclaration: (node) => {
|
|
72
|
+
let sourceValue = node.source.value
|
|
73
|
+
|
|
74
|
+
if (sourceValue[0] === '.') {
|
|
75
|
+
sourceValue = path.resolve(`${dir}/${sourceValue}`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
sourceValue = calculateAbsoluteImportPath(sourceValue)
|
|
79
|
+
|
|
80
|
+
if (sourceValue[0] !== '/') {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const sources = sourceValue.split('/')
|
|
85
|
+
let joinedSources = sourceValue
|
|
86
|
+
let ext = undefined
|
|
87
|
+
|
|
88
|
+
while (sources.length > 0) {
|
|
89
|
+
// HINT: 以下の場合は即終了
|
|
90
|
+
// - import元以下のimportだった場合
|
|
91
|
+
// - rootまで捜索した場合
|
|
92
|
+
if (dir === joinedSources || dir === rootPath) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
ext = ['ts', 'tsx', 'js', 'jsx'].find((e) => fs.existsSync(`${sources.join('/')}/index.${e}`))
|
|
97
|
+
|
|
98
|
+
if (ext) {
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
sources.pop()
|
|
103
|
+
joinedSources = sources.join('/')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
joinedSources &&
|
|
108
|
+
sourceValue !== joinedSources &&
|
|
109
|
+
!dir.match(new RegExp(`^${joinedSources}/`))
|
|
110
|
+
) {
|
|
111
|
+
const replacedSources = calculateReplacedImportPath(joinedSources)
|
|
112
|
+
|
|
113
|
+
context.report({
|
|
114
|
+
node,
|
|
115
|
+
messageId: 'require-barrel-import',
|
|
116
|
+
data: {
|
|
117
|
+
message: `${replacedSources}からimportするか、${replacedSources}/index.${ext}を削除してください`,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
module.exports.schema = []
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const SCHEMA = [{
|
|
3
|
+
type: 'object',
|
|
4
|
+
patternProperties: {
|
|
5
|
+
'.+': {
|
|
6
|
+
type: 'object',
|
|
7
|
+
patternProperties: {
|
|
8
|
+
'.+': {
|
|
9
|
+
type: 'object',
|
|
10
|
+
required: [
|
|
11
|
+
'imported',
|
|
12
|
+
],
|
|
13
|
+
properties: {
|
|
14
|
+
imported: {
|
|
15
|
+
type: ['boolean', 'array'],
|
|
16
|
+
items: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
reportMessage: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
additionalProperties: true,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
additionalProperties: true,
|
|
31
|
+
}]
|
|
32
|
+
|
|
33
|
+
const defaultReportMessage = (moduleName, exportName) => `${moduleName}${typeof exportName == 'string' ? `/${exportName}`: ''} をimportしてください`
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
meta: {
|
|
37
|
+
type: 'suggestion',
|
|
38
|
+
messages: {
|
|
39
|
+
'require_import': '{{ message }}',
|
|
40
|
+
},
|
|
41
|
+
schema: SCHEMA,
|
|
42
|
+
},
|
|
43
|
+
create(context) {
|
|
44
|
+
const options = context.options[0]
|
|
45
|
+
const filename = context.getFilename()
|
|
46
|
+
const targetPathRegexs = Object.keys(options)
|
|
47
|
+
const targetRequires = targetPathRegexs.filter((regex) => !!filename.match(new RegExp(regex)))
|
|
48
|
+
|
|
49
|
+
if (targetRequires.length === 0) {
|
|
50
|
+
return {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
Program: (node) => {
|
|
55
|
+
const importDeclarations = node.body.filter((item) => item.type === 'ImportDeclaration')
|
|
56
|
+
const parentDir = (() => {
|
|
57
|
+
const dir = filename.match(/^(.+?)\..+?$/)[1].split('/')
|
|
58
|
+
dir.pop()
|
|
59
|
+
|
|
60
|
+
return dir.join('/')
|
|
61
|
+
})()
|
|
62
|
+
|
|
63
|
+
targetRequires.forEach((requireKey) => {
|
|
64
|
+
const option = options[requireKey]
|
|
65
|
+
|
|
66
|
+
Object.keys(option).forEach((targetModule) => {
|
|
67
|
+
const { imported, reportMessage, targetRegex } = Object.assign({imported: true}, option[targetModule])
|
|
68
|
+
|
|
69
|
+
if (targetRegex && !filename.match(new RegExp(targetRegex))) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const actualTarget = targetModule[0] !== '.' ? targetModule : path.resolve(`${process.cwd()}/${targetModule}`)
|
|
74
|
+
const importDeclaration = importDeclarations.find(
|
|
75
|
+
actualTarget[0] !== '/' ? (
|
|
76
|
+
(id) => id.source.value === actualTarget
|
|
77
|
+
) : (
|
|
78
|
+
(id) => path.resolve(`${parentDir}/${id.source.value}`) === actualTarget
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
const reporter = (item) => {
|
|
82
|
+
context.report({
|
|
83
|
+
node,
|
|
84
|
+
messageId: 'require_import',
|
|
85
|
+
data: {
|
|
86
|
+
message: reportMessage ? reportMessage.replace('{{module}}', actualTarget).replace('{{export}}', item) : defaultReportMessage(actualTarget, item)
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!importDeclaration) {
|
|
92
|
+
if (Array.isArray(imported)) {
|
|
93
|
+
imported.forEach((i) => {
|
|
94
|
+
reporter(i)
|
|
95
|
+
})
|
|
96
|
+
} else if (imported) {
|
|
97
|
+
reporter()
|
|
98
|
+
}
|
|
99
|
+
} else if (Array.isArray(imported)) {
|
|
100
|
+
imported.forEach((i) => {
|
|
101
|
+
if (!importDeclaration.specifiers.find((s) => s.imported && s.imported.name === i)) {
|
|
102
|
+
reporter(i)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports.schema = SCHEMA
|