eslint-plugin-stratified-design 0.12.4 → 0.12.6

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.
@@ -142,7 +142,7 @@ The default is as follows:
142
142
  }
143
143
  ```
144
144
 
145
- Imported modules can be excluded from the rule (`lower-level-imports`) using the `excludeImports` option:
145
+ Imported modules are excluded from the rule (`lower-level-imports`) using the `excludeImports` option:
146
146
 
147
147
  ```json
148
148
  {
@@ -153,6 +153,18 @@ Imported modules can be excluded from the rule (`lower-level-imports`) using the
153
153
  }
154
154
  ```
155
155
 
156
+ Explicitly imported types are excluded using `ignoreType` option:
157
+
158
+ ```json
159
+ {
160
+ "stratified-design/stratified-imports": ["error", { "ignoreType": true }]
161
+ }
162
+ ```
163
+
164
+ ```js
165
+ import type { SomeType } from "./type"; // Explicitly imported types are excluded
166
+ ```
167
+
156
168
  ## Rule Details
157
169
 
158
170
  If a file structure is as follows:
@@ -87,7 +87,7 @@ The default configuration is as follows:
87
87
  }
88
88
  ```
89
89
 
90
- Imported modules can be excluded from the rule (`stratified-imports`) using the `excludeImports` option:
90
+ Imported modules are excluded from the rule (`stratified-imports`) using the `excludeImports` option:
91
91
 
92
92
  ```json
93
93
  {
@@ -98,6 +98,18 @@ Imported modules can be excluded from the rule (`stratified-imports`) using the
98
98
  }
99
99
  ```
100
100
 
101
+ Explicitly imported types are excluded using `ignoreType` option:
102
+
103
+ ```json
104
+ {
105
+ "stratified-design/stratified-imports": ["error", { "ignoreType": true }]
106
+ }
107
+ ```
108
+
109
+ ```js
110
+ import type { SomeType } from "./type"; // Explicitly imported types are excluded
111
+ ```
112
+
101
113
  ## Rule Details
102
114
 
103
115
  Consider the following folder structure:
@@ -98,6 +98,9 @@ module.exports = {
98
98
  isIndexHighest: {
99
99
  type: "boolean",
100
100
  },
101
+ ignoreType: {
102
+ type: "boolean",
103
+ },
101
104
  },
102
105
  additionalProperties: false,
103
106
  },
@@ -124,6 +127,7 @@ module.exports = {
124
127
  aliases: {},
125
128
  useLevelNumber: false,
126
129
  isIndexHighest: false,
130
+ ignoreType: false,
127
131
  ...(context.options[0] || {}),
128
132
  };
129
133
 
@@ -167,7 +171,7 @@ module.exports = {
167
171
  const isFileIndex = isFileIndexOfModule(options, fileDir, filePath);
168
172
 
169
173
  const report = (node) => {
170
- if (node.importKind === "type") return;
174
+ if (options.ignoreType && node.importKind === "type") return;
171
175
 
172
176
  if (isExcludedFile) return;
173
177
 
@@ -20,36 +20,97 @@ const { fromCwd, match } = require("../helpers/common");
20
20
  * @typedef {({[name: string]: number})} Levels
21
21
  */
22
22
 
23
+ const hasArrFunction = (elements, levels) => {
24
+ return elements.reduce((hasFunc, element) => {
25
+ if (hasFunc) return true;
26
+ const type = element.type;
27
+ if (type === "FunctionExpression" || type === "ArrowFunctionExpression")
28
+ return true;
29
+ if (type === "ObjectExpression")
30
+ return hasObjFunction(element.properties, levels);
31
+ if (type === "ArrayExpression")
32
+ return hasArrFunction(element.elements, levels);
33
+ if ("name" in element && element.name in levels) return true;
34
+ return false;
35
+ }, false);
36
+ };
37
+
38
+ const hasObjFunction = (properties, levels) => {
39
+ return properties.reduce((hasFunc, property) => {
40
+ if (hasFunc) return true;
41
+ if (!("value" in property)) return false;
42
+ const type = property.value.type;
43
+ if (type === "FunctionExpression" || type === "ArrowFunctionExpression")
44
+ return true;
45
+ if (type === "ObjectExpression")
46
+ return hasObjFunction(property.value.properties, levels);
47
+ if (type === "ArrayExpression")
48
+ return hasArrFunction(property.value.elements, levels);
49
+ if ("name" in property.value && property.value.name in levels) return true;
50
+ return false;
51
+ }, false);
52
+ };
53
+
54
+ /**
55
+ * @param {Node | Token} nodeOrToken
56
+ */
57
+ const deriveDeclaration = (nodeOrToken) => {
58
+ return (nodeOrToken.type === "ExportNamedDeclaration" ||
59
+ nodeOrToken.type === "ExportDefaultDeclaration") &&
60
+ Boolean(nodeOrToken.declaration)
61
+ ? nodeOrToken.declaration
62
+ : nodeOrToken;
63
+ };
64
+
23
65
  /**
24
66
  * @param {Node | Token} nodeOrToken
25
67
  * @returns {string | null}
26
68
  */
27
69
  const deriveFuncName = (nodeOrToken) => {
28
- const declaration =
29
- (nodeOrToken.type === "ExportNamedDeclaration" ||
30
- nodeOrToken.type === "ExportDefaultDeclaration") &&
31
- Boolean(nodeOrToken.declaration)
32
- ? nodeOrToken.declaration
33
- : nodeOrToken;
34
- const isFuncDeclaration = declaration.type === "FunctionDeclaration";
35
- const isVarDeclaration =
36
- declaration.type === "VariableDeclaration" &&
70
+ const declaration = deriveDeclaration(nodeOrToken);
71
+
72
+ if (declaration.type === "FunctionDeclaration") return declaration.id.name;
73
+ if (declaration.type !== "VariableDeclaration") return null;
74
+
75
+ const { init, id } = declaration.declarations[0];
76
+ if (
37
77
  [
38
78
  "ArrowFunctionExpression",
39
79
  "FunctionExpression",
40
80
  "CallExpression",
41
81
  "TaggedTemplateExpression",
42
- ].includes(declaration.declarations[0].init.type);
43
-
44
- if (isFuncDeclaration || isVarDeclaration) {
45
- const name = isFuncDeclaration
46
- ? declaration.id.name
47
- : declaration.declarations[0].id.name;
48
- return name;
82
+ ].includes(init.type)
83
+ ) {
84
+ return id.name;
49
85
  }
86
+
50
87
  return null;
51
88
  };
52
89
 
90
+ /**
91
+ * @param {Node | Token} nodeOrToken
92
+ * @param {Levels} levels
93
+ * @returns {string | null}
94
+ */
95
+ const deriveVarName = (nodeOrToken, levels) => {
96
+ const declaration = deriveDeclaration(nodeOrToken);
97
+ if (declaration.type !== "VariableDeclaration") return null;
98
+ const { init, id } = declaration.declarations[0];
99
+ if (
100
+ init.type === "ObjectExpression" &&
101
+ hasObjFunction(init.properties, levels)
102
+ ) {
103
+ return id.name;
104
+ } else if (
105
+ init.type === "ArrayExpression" &&
106
+ hasArrFunction(init.elements, levels)
107
+ ) {
108
+ return id.name;
109
+ } else {
110
+ return null;
111
+ }
112
+ };
113
+
53
114
  /**
54
115
  * @param {SourceCode} sourceCode
55
116
  * @param {Node | Token} nodeOrToken
@@ -89,16 +150,24 @@ const isCalleeLowerLevel = (sourceCode, levels) => {
89
150
  if (calleeLevel === undefined) return true; // 호출되는 함수가 존재하지 않으면 true 리턴. 함수 내의 함수의 경우에는 항상 calleeLevel 이 undefined 이다.
90
151
 
91
152
  const caller = traceAncestor(sourceCode, node);
92
- const callerLevel = deriveLevel(sourceCode, caller);
93
- if (callerLevel < calleeLevel) return true;
153
+ const callerName = deriveFuncName(caller) || deriveVarName(caller, levels);
154
+ const callerLevel = levels[callerName];
94
155
 
95
- const callerName = deriveFuncName(caller);
156
+ if (callerLevel === undefined) return true;
157
+ if (callerLevel < calleeLevel) return true;
96
158
  if (callerName === calleeName) return true; // recursive function
97
159
 
98
160
  return false;
99
161
  };
100
162
  };
101
163
 
164
+ const deriveCalleeName = (callee) => {
165
+ if (callee.type === "Identifier") return callee.name;
166
+ if (callee.type === "MemberExpression")
167
+ return deriveCalleeName(callee.object);
168
+ return null;
169
+ };
170
+
102
171
  /** @type {import('eslint').Rule.RuleModule} */
103
172
  module.exports = {
104
173
  meta: {
@@ -175,10 +244,11 @@ module.exports = {
175
244
  Program(node) {
176
245
  node.body.forEach((token) => {
177
246
  const name = deriveFuncName(token);
178
- if (name) {
179
- const level = deriveLevel(sourceCode, token);
180
- levels[name] = level;
181
- }
247
+ if (name !== null) levels[name] = deriveLevel(sourceCode, token);
248
+ });
249
+ node.body.forEach((token) => {
250
+ const name = deriveVarName(token, levels);
251
+ if (name !== null) levels[name] = deriveLevel(sourceCode, token);
182
252
  });
183
253
  },
184
254
  CallExpression(node) {
@@ -186,7 +256,7 @@ module.exports = {
186
256
  const isReported = report(node, name);
187
257
  if (isReported) return;
188
258
  }
189
- report(node, node.callee.name);
259
+ report(node, deriveCalleeName(node.callee));
190
260
  },
191
261
  JSXOpeningElement(node) {
192
262
  report(node, node.name.name);
@@ -16,6 +16,16 @@ const helper = require("../helpers/stratifiedImports");
16
16
  // Rule Definition
17
17
  //------------------------------------------------------------------------------
18
18
 
19
+ /**
20
+ * @typedef {{
21
+ * aliases: Record<string, string>,
22
+ * exclude: string[],
23
+ * include: string[],
24
+ * excludeImports: string[],
25
+ * ignoreType: boolean,
26
+ * }} Options
27
+ */
28
+
19
29
  /** @type {import('eslint').Rule.RuleModule} */
20
30
  module.exports = {
21
31
  meta: {
@@ -49,6 +59,9 @@ module.exports = {
49
59
  type: "array",
50
60
  items: [{ type: "string" }],
51
61
  },
62
+ ignoreType: {
63
+ type: "boolean",
64
+ },
52
65
  },
53
66
  additionalProperties: false,
54
67
  },
@@ -64,10 +77,14 @@ module.exports = {
64
77
  },
65
78
  },
66
79
  create(context) {
80
+ /**
81
+ * @type {Options}
82
+ */
67
83
  const options = {
68
84
  exclude: ["**/*.{test,spec}.{js,ts,jsx,tsx}"],
69
85
  include: ["**/*.{js,ts,jsx,tsx}"],
70
86
  excludeImports: [],
87
+ ignoreType: false,
71
88
  aliases: {},
72
89
  ...(context.options[0] || {}),
73
90
  };
@@ -124,7 +141,7 @@ module.exports = {
124
141
  * @returns
125
142
  */
126
143
  const report = (node) => {
127
- if (node.importKind === "type") return;
144
+ if (options.ignoreType && node.importKind === "type") return;
128
145
 
129
146
  if (!isStructureValid) {
130
147
  reportError(node, "invalid-json");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-stratified-design",
3
- "version": "0.12.4",
3
+ "version": "0.12.6",
4
4
  "description": "ESlint rules for stratified design",
5
5
  "keywords": [
6
6
  "eslint",
@@ -220,7 +220,7 @@ ruleTester.run("lower-level-imports", rule, {
220
220
  ],
221
221
  },
222
222
  {
223
- code: "import { func } from '@/other/file.js'",
223
+ code: "import { func } from '@/other/file'",
224
224
  filename: "./src/other/index.js",
225
225
  options: [
226
226
  {
@@ -239,7 +239,14 @@ ruleTester.run("lower-level-imports", rule, {
239
239
  {
240
240
  code: "import type { SomeType } from '@/layer2/subLayer2'",
241
241
  filename: "./src/layer1/subLayer2.js",
242
- options: [{ structure, root: "./src", aliases: { "@/": "./src/" } }],
242
+ options: [
243
+ {
244
+ structure,
245
+ root: "./src",
246
+ aliases: { "@/": "./src/" },
247
+ ignoreType: true,
248
+ },
249
+ ],
243
250
  },
244
251
  ],
245
252
  invalid: [
@@ -415,7 +422,7 @@ ruleTester.run("lower-level-imports", rule, {
415
422
  ],
416
423
  },
417
424
  {
418
- code: "import { func } from '@/other/file.js'",
425
+ code: "import { func } from '@/other/file'",
419
426
  filename: "./src/other/index.js",
420
427
  options: [
421
428
  {
@@ -427,7 +434,24 @@ ruleTester.run("lower-level-imports", rule, {
427
434
  errors: [
428
435
  {
429
436
  messageId: "not-registered:file",
430
- data: { module: "./other/file.js", file: "./other/index" },
437
+ data: { module: "./other/file", file: "./other/index" },
438
+ },
439
+ ],
440
+ },
441
+ {
442
+ code: "import type { SomeType } from '@/layer2/subLayer2'",
443
+ filename: "./src/layer1/subLayer2.js",
444
+ options: [
445
+ {
446
+ structure,
447
+ root: "./src",
448
+ aliases: { "@/": "./src/" },
449
+ },
450
+ ],
451
+ errors: [
452
+ {
453
+ messageId: "barrier",
454
+ data: { module: "./layer2/subLayer2", file: "./layer1/subLayer2" },
431
455
  },
432
456
  ],
433
457
  },
@@ -158,6 +158,66 @@ ruleTester.run("no-same-level-funcs", rule, {
158
158
  code: "/*@level 2*/const func = () => 'a'; /*@level 1*/const obj = { func: () => { func() } } ",
159
159
  filename: "./src/foo.js",
160
160
  },
161
+
162
+ {
163
+ code: "const func = () => 'a'; const obj = { a: func() } ",
164
+ filename: "./src/foo.js",
165
+ },
166
+ {
167
+ code: "/*@level 2*/const func = () => 'a'; /*@level 1*/const obj = { func: () => { func() } } ",
168
+ filename: "./src/foo.js",
169
+ errors: [{ messageId: "no-same-level-funcs", data: { func: "obj" } }],
170
+ },
171
+ {
172
+ code: "/*@level 2*/const obj = { func2: () => {} }; /*@level 1*/const func1 = () => { obj.func2() } ",
173
+ filename: "./src/foo.js",
174
+ },
175
+ {
176
+ code: "/*@level 2*/const obj = { func2(){} }; /*@level 1*/const func1 = () => { obj.func2() } ",
177
+ filename: "./src/foo.js",
178
+ },
179
+ {
180
+ code: "/*@level 3*/const func3 = () => 'a'; /*@level 2*/const obj = { func2: func3 }; /*@level 1*/const func1 = () => { obj.func2() } ",
181
+ filename: "./src/foo.js",
182
+ errors: [{ messageId: "no-same-level-funcs", data: { func: "obj" } }],
183
+ },
184
+
185
+ {
186
+ code: "const func = () => 'a'; const arr = [func()];",
187
+ filename: "./src/foo.js",
188
+ },
189
+ {
190
+ code: "/*@level 2*/const func = () => 'a'; /*@level 1*/const arr = [() => { func() }];",
191
+ filename: "./src/foo.js",
192
+ },
193
+ {
194
+ code: "/*@level 2*/const arr = [() => {}]; /*@level 1*/const func1 = () => { arr[0]() } ",
195
+ filename: "./src/foo.js",
196
+ },
197
+ {
198
+ code: "/*@level 3*/const func3 = () => 'a'; /*@level 2*/const arr = [func3]; /*@level 1*/const func1 = () => { arr[0]() } ",
199
+ filename: "./src/foo.js",
200
+ },
201
+ {
202
+ code: "import { func } from './module'; const arr = [{a: {b: {c: [func]}}}];",
203
+ filename: "./src/foo.js",
204
+ },
205
+ {
206
+ code: "const func = () => {}; const arr = [{a: {b: {c: [func]}}}];",
207
+ filename: "./src/foo.js",
208
+ },
209
+ {
210
+ code: "/*@level 3*/const func3 = () => {}; /*@level 2*/const arr = [{a: {b: {c: [func3]}}}]; /*@level 1*/const func1 = () => arr[0].a.b.c[0]();",
211
+ filename: "./src/foo.js",
212
+ },
213
+ {
214
+ code: "const obj2 = {}; const obj1 = {...obj2};",
215
+ filename: "./src/foo.js",
216
+ },
217
+ {
218
+ code: "const arr2 = []; const arr1 = [...arr2];",
219
+ filename: "./src/foo.js",
220
+ },
161
221
  ],
162
222
  invalid: [
163
223
  {
@@ -175,13 +235,11 @@ ruleTester.run("no-same-level-funcs", rule, {
175
235
  filename: "./src/foo.js",
176
236
  errors: [{ messageId: "no-same-level-funcs", data: { func: "func1" } }],
177
237
  },
178
-
179
238
  {
180
239
  code: "function func2(){ func1(); }; function func1(){}",
181
240
  filename: "./src/foo.js",
182
241
  errors: [{ messageId: "no-same-level-funcs", data: { func: "func1" } }],
183
242
  },
184
-
185
243
  {
186
244
  code: "export function func2(){ func1(); }; function func1(){}",
187
245
  filename: "./src/foo.js",
@@ -192,7 +250,6 @@ ruleTester.run("no-same-level-funcs", rule, {
192
250
  filename: "./src/foo.js",
193
251
  errors: [{ messageId: "no-same-level-funcs", data: { func: "func1" } }],
194
252
  },
195
-
196
253
  {
197
254
  code: "const func1 = () => {}; const func2 = () => { func1(); }",
198
255
  filename: "./src/foo.js",
@@ -203,7 +260,6 @@ ruleTester.run("no-same-level-funcs", rule, {
203
260
  filename: "./src/foo.js",
204
261
  errors: [{ messageId: "no-same-level-funcs", data: { func: "func1" } }],
205
262
  },
206
-
207
263
  {
208
264
  code: "const func1 = function(){}; const func2 = function(){ func1(); }",
209
265
  filename: "./src/foo.js",
@@ -214,7 +270,6 @@ ruleTester.run("no-same-level-funcs", rule, {
214
270
  filename: "./src/foo.js",
215
271
  errors: [{ messageId: "no-same-level-funcs", data: { func: "func1" } }],
216
272
  },
217
-
218
273
  {
219
274
  code: "const func1 = function func1(){}; const func2 = function func2(){ func1(); }",
220
275
  filename: "./src/foo.js",
@@ -225,7 +280,6 @@ ruleTester.run("no-same-level-funcs", rule, {
225
280
  filename: "./src/foo.js",
226
281
  errors: [{ messageId: "no-same-level-funcs", data: { func: "func1" } }],
227
282
  },
228
-
229
283
  {
230
284
  code: "const fn = () => 1; const value = fn()",
231
285
  filename: "./src/foo.js",
@@ -276,7 +330,6 @@ ruleTester.run("no-same-level-funcs", rule, {
276
330
  filename: "./src/foo.js",
277
331
  errors: [{ messageId: "no-same-level-funcs", data: { func: "func1" } }],
278
332
  },
279
-
280
333
  {
281
334
  code: "function func2(){};\n// @level 1\nfunction func1(){ func2(); }",
282
335
  filename: "./src/foo.js",
@@ -284,14 +337,45 @@ ruleTester.run("no-same-level-funcs", rule, {
284
337
  },
285
338
 
286
339
  {
287
- code: "const func = () => 'a'; const obj = { a: func() } ",
340
+ code: "const func = () => 'a'; const obj = { func: () => { func() } };",
288
341
  filename: "./src/foo.js",
289
342
  errors: [{ messageId: "no-same-level-funcs", data: { func: "func" } }],
290
343
  },
291
344
  {
292
- code: "const func = () => 'a'; const obj = { func: () => { func() } } ",
345
+ code: "const obj = { func2: () => {} }; const func1 = () => { obj.func2() } ",
346
+ filename: "./src/foo.js",
347
+ errors: [{ messageId: "no-same-level-funcs", data: { func: "obj" } }],
348
+ },
349
+ {
350
+ code: "const obj = { func2(){} }; const func1 = () => { obj.func2() } ",
351
+ filename: "./src/foo.js",
352
+ errors: [{ messageId: "no-same-level-funcs", data: { func: "obj" } }],
353
+ },
354
+ {
355
+ code: "const func3 = () => 'a'; const obj = { func2: func3 }; const func1 = () => { obj.func2() } ",
356
+ filename: "./src/foo.js",
357
+ errors: [{ messageId: "no-same-level-funcs", data: { func: "obj" } }],
358
+ },
359
+
360
+ {
361
+ code: "const func = () => 'a'; const arr = [() => { func() }];",
293
362
  filename: "./src/foo.js",
294
363
  errors: [{ messageId: "no-same-level-funcs", data: { func: "func" } }],
295
364
  },
365
+ {
366
+ code: "const arr = [() => {}]; const func1 = () => { arr[0]() } ",
367
+ filename: "./src/foo.js",
368
+ errors: [{ messageId: "no-same-level-funcs", data: { func: "arr" } }],
369
+ },
370
+ {
371
+ code: "const func3 = () => 'a'; const arr = [func3]; const func1 = () => { arr[0]() } ",
372
+ filename: "./src/foo.js",
373
+ errors: [{ messageId: "no-same-level-funcs", data: { func: "arr" } }],
374
+ },
375
+ {
376
+ code: "const func3 = () => {}; const arr = [{a: {b: {c: [func3]}}}]; const func1 = () => arr[0].a.b.c[0]();",
377
+ filename: "./src/foo.js",
378
+ errors: [{ messageId: "no-same-level-funcs", data: { func: "arr" } }],
379
+ },
296
380
  ],
297
381
  });
@@ -169,7 +169,7 @@ ruleTester.run("stratified-imports", rule, {
169
169
  {
170
170
  code: "import type { SomeType } from './layerA'",
171
171
  filename: "./mocked/stratified-imports/layerB.js",
172
- options: [],
172
+ options: [{ ignoreType: true }],
173
173
  },
174
174
  ],
175
175
  invalid: [
@@ -316,5 +316,15 @@ ruleTester.run("stratified-imports", rule, {
316
316
  },
317
317
  ],
318
318
  },
319
+ {
320
+ code: "import type { SomeType } from './layerA'",
321
+ filename: "./mocked/stratified-imports/layerB.js",
322
+ errors: [
323
+ {
324
+ messageId: "not-lower-level",
325
+ data: { module: "layerA", file: "layerB" },
326
+ },
327
+ ],
328
+ },
319
329
  ],
320
330
  });