eslint-plugin-stratified-design 0.6.1 → 0.8.0-beta

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.
@@ -66,7 +66,7 @@ If you want to register the level of a layer by 'number,' set the option `useLev
66
66
 
67
67
  The options `structure` and `useLevelNumber` can be used together.
68
68
 
69
- An `index.xxx` file can be the highest level layer when the option `isIndexHighest` is set to `true`:
69
+ An `index.xxx` file can be the highest level layer of sibling files when the option `isIndexHighest` is set to `true`:
70
70
 
71
71
  ```json
72
72
  "lower-level-imports": ["error", { "isIndexHighest": true }]
@@ -95,6 +95,7 @@ If a file structure is as follows:
95
95
  src/
96
96
  ┣ layer1.js
97
97
  ┣ layer2/
98
+ ┃ ┣ index.js
98
99
  ┃ ┣ file.js
99
100
  ┃ ┣ otherFile.js
100
101
  ┃ ┗ subFolder/
@@ -189,6 +190,6 @@ import { func } from "../layer3/entry";
189
190
 
190
191
  ```js
191
192
  /* "lower-level-imports": ["error", { "isIndexHighest": true }] */
192
- // ./src/layer/index.js
193
- import { func } from "./anything";
193
+ // ./src/layer2/index.js
194
+ import { func } from "./file";
194
195
  ```
@@ -36,6 +36,18 @@ function func3(...) {
36
36
  }
37
37
  ```
38
38
 
39
+ ```js
40
+ // @level 1
41
+ const funcA = (...) => { ... }
42
+
43
+ // @level 2
44
+ const funcB = (...) => {
45
+ ...
46
+ funcA(...)
47
+ ...
48
+ }
49
+ ```
50
+
39
51
  Examples of **correct** code for this rule:
40
52
 
41
53
  ```js
@@ -53,3 +65,15 @@ function func1(...) {
53
65
  func2(...);
54
66
  }
55
67
  ```
68
+
69
+ ```js
70
+ // @level 2
71
+ const funcA = (...) => { ... }
72
+
73
+ // @level 1
74
+ const funcB = (...) => {
75
+ ...
76
+ funcA(...)
77
+ ...
78
+ }
79
+ ```
@@ -29,8 +29,9 @@ const resolvePath = p.resolve;
29
29
  const parsePath = p.parse;
30
30
 
31
31
  /**
32
- * @param {any[]} array
33
- * @param {(item: any) => boolean} callback
32
+ * @template T
33
+ * @param {T[]} array
34
+ * @param {(item: T) => boolean} callback
34
35
  * @returns {number}
35
36
  */
36
37
  const findLastIndex = (array, callback) => {
@@ -49,6 +50,16 @@ const equal = (array1, array2) => {
49
50
  return array1.every((item, index) => item === array2[index]);
50
51
  };
51
52
 
53
+ /**
54
+ * @template T
55
+ * @param {T[]} array
56
+ * @param {number} index
57
+ * @returns {T}
58
+ */
59
+ const readArray = (array, index) => {
60
+ return index >= 0 ? array[index] : array[array.length + index];
61
+ };
62
+
52
63
  module.exports = {
53
64
  toRelative,
54
65
  toSegments,
@@ -58,4 +69,5 @@ module.exports = {
58
69
  parsePath,
59
70
  findLastIndex,
60
71
  equal,
72
+ readArray,
61
73
  };
@@ -3,7 +3,7 @@ const { report: reportError } = require("./2 layer");
3
3
  const {
4
4
  isNodeModule,
5
5
  findLevel: findLayerLevel,
6
- hasInterface: hasInterfaceBetween,
6
+ hasBarrier: hasBarrierBetween,
7
7
  removeAlias: removeAliasFromModuleSource,
8
8
  } = require("./3 layer");
9
9
  const {
@@ -12,7 +12,7 @@ const {
12
12
  toPath,
13
13
  resolvePath,
14
14
  parsePath,
15
- } = require("./4 layer");
15
+ } = require("../common");
16
16
 
17
17
  const FINISHED = "finished";
18
18
 
@@ -191,10 +191,10 @@ const reportHasProperLevel = (
191
191
  filePath
192
192
  ) => {
193
193
  const findLevel = findLayerLevel(structure);
194
- const hasInterface = hasInterfaceBetween(structure, fileLevel);
194
+ const hasBarrier = hasBarrierBetween(structure, fileLevel);
195
195
 
196
196
  /**
197
- * @param {import('eslint').Rule.NodeParentExtension} node
197
+ * @param {import('../type').Node} node
198
198
  * @param {string} modulePath
199
199
  */
200
200
  return (node, modulePath) => {
@@ -222,7 +222,7 @@ const reportHasProperLevel = (
222
222
  return FINISHED;
223
223
  }
224
224
 
225
- if (hasInterface(moduleLevel)) {
225
+ if (hasBarrier(moduleLevel)) {
226
226
  report("barrier");
227
227
  return FINISHED;
228
228
  }
@@ -1,15 +1,15 @@
1
1
  const { isNodeModule } = require("./3 layer");
2
- const { toRelative } = require("./4 layer");
2
+ const { toRelative } = require("../common");
3
3
 
4
4
  /**
5
5
  * Report eslint error
6
- * @param {import('eslint').Rule.RuleContext} context
6
+ * @param {import('../type.js').Context} context
7
7
  * @param {string} rootDir
8
- * * @param {string} filePath
8
+ * @param {string} filePath
9
9
  */
10
10
  const report = (context, rootDir, filePath) => {
11
11
  /**
12
- * @param {import('eslint').Rule.NodeParentExtension} node
12
+ * @param {import('../type').Node} node
13
13
  * @param {string} messageId
14
14
  * @param {string} modulePath
15
15
  */
@@ -4,11 +4,36 @@ const {
4
4
  joinPath,
5
5
  resolvePath,
6
6
  toRelative,
7
- } = require("./4 layer");
7
+ } = require("../common");
8
8
 
9
9
  /**
10
- * @param {*} options
10
+ * @typedef {{
11
+ * structure: Array<
12
+ * string | { name: string, barrier?: boolean, interface?: boolean, nodeModule?: boolean, isNodeModule?: boolean }
13
+ * >,
14
+ * root: string,
15
+ * aliases: Record<string, string>,
16
+ * exclude: string[],
17
+ * include: string[],
18
+ * useLevelNumber: boolean
19
+ * isIndexHighest: boolean
20
+ * }} Options
21
+ */
22
+
23
+ /**
24
+ * @typedef {{
25
+ * name: string;
26
+ * barrier?: boolean | undefined;
27
+ * interface?: boolean | undefined;
28
+ * nodeModule?: boolean | undefined;
29
+ * isNodeModule?: boolean | undefined;
30
+ * }[]} Structure
31
+ */
32
+
33
+ /**
34
+ * @param {Options} options
11
35
  * @param {string} rootDir
36
+ * @returns {Structure}
12
37
  */
13
38
  const createStructure = (options, rootDir) => {
14
39
  return options.structure.map((layer) => {
@@ -20,7 +45,7 @@ const createStructure = (options, rootDir) => {
20
45
  };
21
46
 
22
47
  /**
23
- * @param {*} options
48
+ * @param {Options} options
24
49
  */
25
50
  const createAliases = (options) => {
26
51
  return Object.keys(options.aliases)
@@ -63,7 +88,7 @@ const isNodeModule = (rootDir) => {
63
88
 
64
89
  /**
65
90
  * Find the layer level for a module
66
- * @param {{[string]: {name: string; nodeModule: boolean; barrier: boolean; isNodeModule: boolean, interface: boolean}}} structure
91
+ * @param {Structure} structure
67
92
  * @returns the level of the module with `path`
68
93
  */
69
94
  const findLevel = (structure) => {
@@ -83,25 +108,25 @@ const findLevel = (structure) => {
83
108
 
84
109
  /**
85
110
  * Check if there is an interface between file layer and module layer
86
- * @param {{[string]: {name: string; nodeModule: boolean; barrier: boolean; isNodeModule: boolean, interface: boolean}}} structure
111
+ * @param {Structure} structure
87
112
  * @param {number} fileLevel
88
113
  */
89
- const hasInterface = (structure, fileLevel) => {
114
+ const hasBarrier = (structure, fileLevel) => {
90
115
  /**
91
116
  * @param {number} moduleLevel
92
117
  */
93
118
  return (moduleLevel) => {
94
- const layerInterface = structure
119
+ const layerBarrier = structure
95
120
  .slice(fileLevel + 1, moduleLevel)
96
121
  .find((layer) => layer.barrier || layer.interface);
97
- return Boolean(layerInterface);
122
+ return Boolean(layerBarrier);
98
123
  };
99
124
  };
100
125
 
101
126
  module.exports = {
102
127
  isNodeModule,
103
128
  findLevel,
104
- hasInterface,
129
+ hasBarrier,
105
130
  createStructure,
106
131
  createAliases,
107
132
  removeAlias,
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+
3
+ const {
4
+ resolvePath,
5
+ joinPath,
6
+ toSegments,
7
+ readArray,
8
+ findLastIndex,
9
+ } = require("../common");
10
+ const {
11
+ toStructure,
12
+ replaceAlias,
13
+ readRawStructure,
14
+ findLevel,
15
+ findLayerWithSimilarPath,
16
+ } = require("./2 layer");
17
+
18
+ /**
19
+ * @param {string} fileDir
20
+ */
21
+ const createStructure = (fileDir) => {
22
+ const parentFileDir = joinPath(fileDir, "..");
23
+
24
+ const rawStructure = readRawStructure(fileDir);
25
+ const parentRawStructure = readRawStructure(parentFileDir);
26
+
27
+ const fileDirname = `${readArray(toSegments(fileDir), -1)}/`;
28
+ const theIndex = findLastIndex(parentRawStructure, (rawLayers) => {
29
+ return Boolean(
30
+ rawLayers.find((rawLayer) => {
31
+ if (typeof rawLayer === "string")
32
+ return `${rawLayer}/`.startsWith(fileDirname);
33
+ return `${rawLayer.name}/`.startsWith(fileDirname);
34
+ })
35
+ );
36
+ });
37
+
38
+ const structure = toStructure(rawStructure, fileDir);
39
+ const parentStructure = toStructure(parentRawStructure, parentFileDir);
40
+
41
+ for (let i = theIndex + 1; i <= parentRawStructure.length - 1; i++) {
42
+ structure.push(parentStructure[i]);
43
+ }
44
+
45
+ return structure;
46
+ };
47
+
48
+ /**
49
+ * @param {string} cwd
50
+ * @param {string} fileDir
51
+ * @param {import('./2 layer').Aliases} aliases
52
+ */
53
+ const createModulePath = (cwd, fileDir, aliases) => {
54
+ /**
55
+ * @param {string} moduleSourceWithAlias
56
+ */
57
+ return (moduleSourceWithAlias) => {
58
+ const moduleSource = replaceAlias(
59
+ cwd,
60
+ fileDir,
61
+ aliases
62
+ )(moduleSourceWithAlias);
63
+ const isNodeModule = moduleSource.startsWith(".") === false;
64
+ return isNodeModule ? moduleSource : resolvePath(fileDir, moduleSource);
65
+ };
66
+ };
67
+
68
+ /**
69
+ * @param {import('./2 layer').Structure} structure
70
+ */
71
+ const findLevelInChild = (structure) => {
72
+ /**
73
+ * @param {string} path
74
+ */
75
+ return (path) => {
76
+ const layer = findLayerWithSimilarPath(structure)(path);
77
+ if (!layer) return null;
78
+ const fileDir = layer.name;
79
+ const rawChildStructure = readRawStructure(fileDir);
80
+ const childStructure = toStructure(rawChildStructure, fileDir);
81
+ const childLevel = findLevel(childStructure)(path);
82
+ return childLevel !== null ? findLevel(structure)(fileDir) : null;
83
+ };
84
+ };
85
+
86
+ module.exports = { createStructure, createModulePath, findLevelInChild };
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+
3
+ const { readFileSync } = require("fs");
4
+ const {
5
+ resolvePath,
6
+ toRelative,
7
+ joinPath,
8
+ toSegments,
9
+ toPath,
10
+ } = require("../common");
11
+
12
+ /**
13
+ * @typedef {{
14
+ * name: string;
15
+ * barrier?: boolean;
16
+ * nodeModule?: boolean;
17
+ * }} Layer
18
+ */
19
+
20
+ /**
21
+ * @typedef {string|Layer} RawLayer
22
+ */
23
+
24
+ /**
25
+ * @typedef {Layer[][]} Structure
26
+ */
27
+
28
+ /**
29
+ * @typedef {RawLayer[][]} RawStructure
30
+ */
31
+
32
+ /**
33
+ * @typedef {{alias: string, path: string}[]} Aliases
34
+ */
35
+
36
+ /**
37
+ * @param {string} fileDir
38
+ * @returns {RawStructure}
39
+ */
40
+ const readRawStructure = (fileDir) => {
41
+ try {
42
+ return JSON.parse(
43
+ readFileSync(resolvePath(fileDir, "./.stratified.json"), "utf-8")
44
+ );
45
+ } catch (err) {
46
+ return [];
47
+ }
48
+ };
49
+
50
+ /**
51
+ * @param {RawStructure} rawStructure
52
+ * @param {string} fileDir
53
+ * @return {Structure}
54
+ */
55
+ const toStructure = (rawStructure, fileDir) => {
56
+ return rawStructure.map((rawLayers) => {
57
+ return rawLayers.map((rawLayer) => {
58
+ const layer =
59
+ typeof rawLayer === "string" ? { name: rawLayer } : rawLayer;
60
+ return layer.nodeModule
61
+ ? layer
62
+ : { ...layer, name: joinPath(fileDir, layer.name) };
63
+ });
64
+ });
65
+ };
66
+
67
+ /**
68
+ * @param {Record<string, string>} rawAliases
69
+ * @returns {Aliases}
70
+ */
71
+ const createAliases = (rawAliases) => {
72
+ return Object.keys(rawAliases)
73
+ .sort((a, b) => b.length - a.length)
74
+ .map((alias) => ({ alias, path: rawAliases[alias] }));
75
+ };
76
+
77
+ /**
78
+ * Replace an alias into the corresponding path
79
+ * @param {string} cwd
80
+ * @param {string} fileDir
81
+ * @param {Aliases} aliases
82
+ */
83
+ const replaceAlias = (cwd, fileDir, aliases) => {
84
+ /**
85
+ * @param {string} moduleSource
86
+ */
87
+ return (moduleSource) => {
88
+ const { alias, path } =
89
+ aliases.find(({ alias }) => moduleSource.startsWith(alias)) || {};
90
+ if (!alias) return moduleSource;
91
+ const modulePath = resolvePath(cwd, moduleSource.replace(alias, path));
92
+ return toRelative(fileDir, modulePath);
93
+ };
94
+ };
95
+
96
+ /**
97
+ * Find the layer level for a module
98
+ * @param {Structure} structure
99
+ * @returns the level of the module with `path`
100
+ */
101
+ const findLevel = (structure) => {
102
+ /**
103
+ * @param {string} path
104
+ */
105
+ return (path) => {
106
+ const level = structure.findIndex((layers) =>
107
+ Boolean(layers.find((layer) => layer.name === path))
108
+ );
109
+ return level >= 0 ? level : null;
110
+ };
111
+ };
112
+
113
+ /**
114
+ * @param {import('./2 layer').Structure} structure
115
+ */
116
+ const findLayerWithSimilarPath = (structure) => {
117
+ /**
118
+ * @param {string} path
119
+ * @return {Layer | null}
120
+ */
121
+ return (path) => {
122
+ return (
123
+ toSegments(path).reduce((theLayer, _, index, segments) => {
124
+ if (theLayer) return theLayer;
125
+ const similarPath = toPath(segments.slice(0, segments.length - index));
126
+ /**
127
+ * @type {Layer}
128
+ */
129
+ return structure.reduce((foundLayer, layers) => {
130
+ if (foundLayer) return foundLayer;
131
+ return layers.find((layer) => layer.name === similarPath);
132
+ }, undefined);
133
+ }, undefined) || null
134
+ );
135
+ };
136
+ };
137
+
138
+ /**
139
+ * @param {string} cwd
140
+ */
141
+ const isNotRegisteredNodeModule = (cwd) => {
142
+ /**
143
+ * @param {string} modulePath
144
+ */
145
+ return (modulePath) => modulePath.startsWith(cwd) === false;
146
+ };
147
+
148
+ module.exports = {
149
+ readRawStructure,
150
+ toStructure,
151
+ createAliases,
152
+ replaceAlias,
153
+ findLevel,
154
+ findLayerWithSimilarPath,
155
+ isNotRegisteredNodeModule,
156
+ };
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+
3
+ const {
4
+ createStructure,
5
+ createModulePath,
6
+ findLevelInChild,
7
+ } = require("./1 layer");
8
+ const {
9
+ createAliases,
10
+ findLevel,
11
+ isNotRegisteredNodeModule,
12
+ } = require("./2 layer");
13
+
14
+ const { parseFileSource } = require("../lowerLevelImports");
15
+
16
+ module.exports = {
17
+ createStructure,
18
+ createModulePath,
19
+ createAliases,
20
+ parseFileSource,
21
+ findLevel,
22
+ isNotRegisteredNodeModule,
23
+ findLevelInChild,
24
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @typedef {import('eslint').Rule.RuleContext} Context
3
+ * @typedef {import('eslint').Rule.Node} Node
4
+ * @typedef {import('eslint').AST.Token} Token
5
+ * @typedef {import('eslint').SourceCode} SourceCode
6
+ */
7
+
8
+ module.exports = {};
@@ -117,15 +117,16 @@ module.exports = {
117
117
  include: ["**/*.{js,ts,jsx,tsx}"],
118
118
  aliases: {},
119
119
  useLevelNumber: false,
120
+ isIndexHighest: false,
120
121
  ...(context.options[0] || {}),
121
122
  };
122
123
 
123
- const cwd = context.getCwd();
124
+ const cwd = context.cwd;
124
125
  const rootDir = createRootDir(cwd, options);
125
126
 
126
127
  const { fileDir, filePath, isExcludedFile } = parseFileSource(
127
128
  options,
128
- context.getFilename()
129
+ context.filename
129
130
  );
130
131
 
131
132
  const structure = createStructure(options, rootDir);
@@ -11,6 +11,37 @@ const path = require("path");
11
11
  // Rule Definition
12
12
  //------------------------------------------------------------------------------
13
13
 
14
+ /**
15
+ * @typedef {import('eslint').Rule.Node} Node
16
+ * @typedef {import('eslint').AST.Token} Token
17
+ * @typedef {import('eslint').SourceCode} SourceCode
18
+ */
19
+
20
+ /**
21
+ * @param {SourceCode} sourceCode
22
+ * @param {Node | Token} nodeOrToken
23
+ *
24
+ */
25
+ const deriveLevel = (sourceCode, nodeOrToken) => {
26
+ const comments = sourceCode.getCommentsBefore(nodeOrToken);
27
+ for (const { value } of comments) {
28
+ const levelInStr = value.replace(/^[^]*@level\s+?([0-9]+)[^0-9]*$/, "$1");
29
+ const levelInNum = Number(levelInStr);
30
+ if (levelInStr && !Number.isNaN(levelInNum)) {
31
+ return levelInNum;
32
+ }
33
+ }
34
+ return null;
35
+ };
36
+
37
+ /**
38
+ * @param {SourceCode} sourceCode
39
+ * @param {Node} node
40
+ */
41
+ const traceAncestor = (sourceCode, node) => {
42
+ return sourceCode.getAncestors(node)[1];
43
+ };
44
+
14
45
  /** @type {import('eslint').Rule.RuleModule} */
15
46
  module.exports = {
16
47
  meta: {
@@ -36,10 +67,9 @@ module.exports = {
36
67
  additionalItems: false,
37
68
  },
38
69
  messages: {
39
- "no-same-level-funcs": "Disallow calling {{func}} in the same file.",
70
+ "no-same-level-funcs": "{{func}} is NOT lower level",
40
71
  },
41
72
  },
42
-
43
73
  create(context) {
44
74
  const options = {
45
75
  exclude: ["**/*.{test,spec}.{js,ts,jsx,tsx}"],
@@ -47,7 +77,7 @@ module.exports = {
47
77
  ...(context.options[0] || {}),
48
78
  };
49
79
 
50
- const fileSource = path.resolve(context.getFilename());
80
+ const fileSource = path.resolve(context.filename);
51
81
 
52
82
  const isIncludedFile = options.include.find((pattern) =>
53
83
  minimatch(fileSource, pattern)
@@ -59,31 +89,47 @@ module.exports = {
59
89
 
60
90
  if (isExcludedFile) return {};
61
91
 
62
- let funcNames;
92
+ /**
93
+ * @type {({[name: string]: number | null})}
94
+ */
95
+ const levels = {};
96
+
97
+ const sourceCode = context.sourceCode;
63
98
 
64
99
  return {
65
100
  Program(node) {
66
- funcNames = node.body.reduce((names, { type, id, declarations }) => {
67
- if (type === "FunctionDeclaration") {
68
- names.push(id.name);
69
- } else if (
70
- type === "VariableDeclaration" &&
71
- (declarations[0].init.type === "ArrowFunctionExpression" ||
72
- declarations[0].init.type === "FunctionExpression")
73
- ) {
74
- names.push(declarations[0].id.name);
101
+ node.body.forEach((token) => {
102
+ const isFuncDeclaration = token.type === "FunctionDeclaration";
103
+ const isVarDeclaration =
104
+ token.type === "VariableDeclaration" &&
105
+ [
106
+ "ArrowFunctionExpression",
107
+ "FunctionExpression",
108
+ "CallExpression",
109
+ ].includes(token.declarations[0].init.type);
110
+
111
+ if (isFuncDeclaration || isVarDeclaration) {
112
+ const level = deriveLevel(sourceCode, token);
113
+ const name = isFuncDeclaration
114
+ ? token.id.name
115
+ : token.declarations[0].id.name;
116
+ levels[name] = level;
75
117
  }
76
- return names;
77
- }, []);
118
+ });
78
119
  },
79
120
  CallExpression(node) {
80
- if (funcNames.includes(node.callee.name)) {
81
- context.report({
82
- node,
83
- messageId: "no-same-level-funcs",
84
- data: { func: node.callee.name },
85
- });
121
+ const calleeLevel = levels[node.callee.name];
122
+ if (calleeLevel === undefined) return;
123
+ if (calleeLevel !== null) {
124
+ const ancestor = traceAncestor(sourceCode, node);
125
+ const ancestorLevel = deriveLevel(sourceCode, ancestor);
126
+ if (ancestorLevel !== null && ancestorLevel < calleeLevel) return;
86
127
  }
128
+ context.report({
129
+ node,
130
+ messageId: "no-same-level-funcs",
131
+ data: { func: node.callee.name },
132
+ });
87
133
  },
88
134
  };
89
135
  },
@@ -0,0 +1,172 @@
1
+ /**
2
+ * @fileoverview Require that lower level modules be imported (stratified-imports)
3
+ * @author Hodoug Joung
4
+ */
5
+
6
+ "use strict";
7
+
8
+ const { parsePath } = require("../helpers/common");
9
+ //------------------------------------------------------------------------------
10
+ // Requirements
11
+ //------------------------------------------------------------------------------
12
+
13
+ const helper = require("../helpers/stratifiedImports");
14
+
15
+ //------------------------------------------------------------------------------
16
+ // Rule Definition
17
+ //------------------------------------------------------------------------------
18
+
19
+ /** @type {import('eslint').Rule.RuleModule} */
20
+ module.exports = {
21
+ meta: {
22
+ type: "problem",
23
+ fixable: "code",
24
+ schema: {
25
+ type: "array",
26
+ items: [
27
+ {
28
+ type: "object",
29
+ properties: {
30
+ aliases: {
31
+ type: "object",
32
+ patternProperties: {
33
+ ["^.+$"]: {
34
+ type: "string",
35
+ pattern: "^\\.{1,2}(/[^/]+)*/?$",
36
+ },
37
+ },
38
+ additionalProperties: false,
39
+ },
40
+ exclude: {
41
+ type: "array",
42
+ items: [{ type: "string" }],
43
+ },
44
+ include: {
45
+ type: "array",
46
+ items: [{ type: "string" }],
47
+ },
48
+ },
49
+ additionalProperties: false,
50
+ },
51
+ ],
52
+ additionalItems: false,
53
+ },
54
+ messages: {
55
+ "not-lower-level": "'{{module}}' is NOT LOWER level than '{{file}}'",
56
+ barrier:
57
+ "An ABSTRACT BARRIER prevents '{{file}}' from importing '{{module}}'",
58
+ "not-registered": "'{{file}}' does NOT registered at .stratified.json",
59
+ },
60
+ },
61
+ create(context) {
62
+ const options = {
63
+ exclude: ["**/*.{test,spec}.{js,ts,jsx,tsx}"],
64
+ include: ["**/*.{js,ts,jsx,tsx}"],
65
+ aliases: {},
66
+ ...(context.options[0] || {}),
67
+ };
68
+
69
+ const { fileDir, filePath, isExcludedFile } = helper.parseFileSource(
70
+ options,
71
+ context.filename
72
+ );
73
+
74
+ if (isExcludedFile) return {};
75
+
76
+ const structure = helper.createStructure(fileDir);
77
+
78
+ const fileLevel = helper.findLevel(structure)(filePath);
79
+
80
+ const createModulePath = helper.createModulePath(
81
+ context.cwd,
82
+ fileDir,
83
+ helper.createAliases(options.aliases)
84
+ );
85
+
86
+ const findLevel = helper.findLevel(structure);
87
+
88
+ const findLevelInChild = helper.findLevelInChild(structure);
89
+
90
+ const isNotRegisteredNodeModule = helper.isNotRegisteredNodeModule(
91
+ context.cwd
92
+ );
93
+
94
+ /**
95
+ * @param {number} moduleLevel
96
+ */
97
+ const hasBarrier = (moduleLevel) => {
98
+ const layerBarrier = structure
99
+ .slice(fileLevel + 1, moduleLevel)
100
+ .find((layers) => layers.find((layer) => layer.barrier));
101
+ return Boolean(layerBarrier);
102
+ };
103
+
104
+ /**
105
+ * @param {import('../helpers/type').Node} node
106
+ * @param {'not-lower-level'|'barrier'|'not-registered'} messageId
107
+ * @param {string | undefined} modulePath
108
+ */
109
+ const reportError = (node, messageId, modulePath) => {
110
+ context.report({
111
+ node,
112
+ messageId,
113
+ data: {
114
+ file: parsePath(filePath).name,
115
+ ...(modulePath ? { module: parsePath(modulePath).name } : {}),
116
+ },
117
+ });
118
+ };
119
+
120
+ /**
121
+ * @param {import('../helpers/type').Node} node
122
+ * @returns
123
+ */
124
+ const report = (node) => {
125
+ // TODO: invalid .stratified.json
126
+
127
+ if (fileLevel === null) {
128
+ reportError(node, "not-registered");
129
+ return;
130
+ }
131
+
132
+ const modulePath = createModulePath(node.source.value);
133
+ const moduleLevel = (() => {
134
+ const level = findLevel(modulePath);
135
+ return level !== null
136
+ ? level
137
+ : isNotRegisteredNodeModule(modulePath)
138
+ ? "notRegisteredNodeModule"
139
+ : findLevelInChild(modulePath);
140
+ })();
141
+
142
+ if (moduleLevel === "notRegisteredNodeModule") return;
143
+
144
+ if (moduleLevel === null) {
145
+ reportError(node, "not-lower-level", modulePath);
146
+ return;
147
+ }
148
+
149
+ if (hasBarrier(moduleLevel)) {
150
+ reportError(node, "barrier", modulePath);
151
+ return;
152
+ }
153
+
154
+ if (moduleLevel <= fileLevel) {
155
+ reportError(node, "not-lower-level", modulePath);
156
+ return;
157
+ }
158
+ };
159
+
160
+ return {
161
+ ImportDeclaration(node) {
162
+ report(node);
163
+ },
164
+ ExportNamedDeclaration(node) {
165
+ if (node.source) report(node);
166
+ },
167
+ ExportAllDeclaration(node) {
168
+ if (node.source) report(node);
169
+ },
170
+ };
171
+ },
172
+ };
@@ -0,0 +1,12 @@
1
+ [
2
+ ["layerA"],
3
+ ["layerB"],
4
+ [{ "name": "layerC", "barrier": true }],
5
+ ["layerD"],
6
+ [{ "name": "nodeModuleE", "nodeModule": "true" }],
7
+ ["layerF"],
8
+ ["layerG", "layerH"],
9
+ ["layerI"],
10
+ ["layerJ"],
11
+ ["layerK"]
12
+ ]
@@ -0,0 +1 @@
1
+ [["layerBA"], ["layerBB"]]
@@ -0,0 +1 @@
1
+ [["entryJA", "entryJB"], ["layerJC"]]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-stratified-design",
3
- "version": "0.6.1",
3
+ "version": "0.8.0-beta",
4
4
  "description": "ESlint rules for stratified design",
5
5
  "keywords": [
6
6
  "eslint",
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @fileoverview test for helpers/stratified-imports
3
+ * @author Hodoug Joung
4
+ */
5
+ "use strict";
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Requirements
9
+ //------------------------------------------------------------------------------
10
+
11
+ const assert = require("assert");
12
+ const {
13
+ findLevel,
14
+ findLayerWithSimilarPath,
15
+ toStructure,
16
+ createAliases,
17
+ replaceAlias,
18
+ } = require("../../../lib/helpers/stratifiedImports/2 layer");
19
+ const {
20
+ createModulePath,
21
+ } = require("../../../lib/helpers/stratifiedImports/1 layer");
22
+
23
+ //------------------------------------------------------------------------------
24
+ // Tests
25
+ //------------------------------------------------------------------------------
26
+
27
+ describe("helpers/stratified-imports", () => {
28
+ describe("toStructure()", () => {
29
+ const fileDir = "/src";
30
+ const testCases = [
31
+ { rawStructure: [["layer"]], structure: [[{ name: "/src/layer" }]] },
32
+ {
33
+ rawStructure: [[{ name: "layer" }]],
34
+ structure: [[{ name: "/src/layer" }]],
35
+ },
36
+ {
37
+ rawStructure: [[{ name: "layer", barrier: true }]],
38
+ structure: [[{ name: "/src/layer", barrier: true }]],
39
+ },
40
+ ];
41
+ testCases.forEach(({ rawStructure, structure }) => {
42
+ it(`${rawStructure} -> ${structure}`, () => {
43
+ assert.deepEqual(toStructure(rawStructure, fileDir), structure);
44
+ });
45
+ });
46
+ });
47
+
48
+ describe("createAliases()", () => {
49
+ const testCases = [
50
+ {
51
+ rawAliases: { "@/": "./src/" },
52
+ aliases: [{ alias: "@/", path: "./src/" }],
53
+ },
54
+ {
55
+ rawAliases: { "@/": "./src/", "@layer/": "./src/layer/" },
56
+ aliases: [
57
+ { alias: "@layer/", path: "./src/layer/" },
58
+ { alias: "@/", path: "./src/" },
59
+ ],
60
+ },
61
+ ];
62
+ testCases.forEach(({ rawAliases, aliases }) => {
63
+ it(`${rawAliases} -> ${aliases}`, () => {
64
+ assert.deepEqual(createAliases(rawAliases), aliases);
65
+ });
66
+ });
67
+ });
68
+
69
+ describe("replaceAlias()", () => {
70
+ const cwd = "/proj";
71
+ const fileDir = "/proj/src/layerA";
72
+ const aliases = [{ alias: "@/", path: "./src/" }];
73
+ const testCases = [
74
+ { moduleSource: "@/layerA/layerAA", relPath: "./layerAA" },
75
+ { moduleSource: "nodeModule", relPath: "nodeModule" },
76
+ ];
77
+ testCases.forEach(({ moduleSource, relPath }) => {
78
+ it(`${moduleSource} -> ${relPath}`, () => {
79
+ assert.equal(
80
+ replaceAlias(cwd, fileDir, aliases)(moduleSource),
81
+ relPath
82
+ );
83
+ });
84
+ });
85
+ });
86
+
87
+ describe("findLevel()", () => {
88
+ const structure = [[{ name: "/src/layerA" }], [{ name: "/src/layerB" }]];
89
+ const testCases = [
90
+ { path: "/src/layerA", level: 0 },
91
+ { path: "/src/layerB", level: 1 },
92
+ { path: "/src/layerA/entry", level: null },
93
+ ];
94
+ testCases.forEach(({ path, level }) => {
95
+ it(`The level of ${path} is ${level}`, () => {
96
+ assert.equal(findLevel(structure)(path), level);
97
+ });
98
+ });
99
+ });
100
+
101
+ describe("findLayerWithSimilarPath()", () => {
102
+ const structure = [[{ name: "/src/layerA" }], [{ name: "/src/layerB" }]];
103
+ const testCases = [
104
+ { path: "/src/layerA", layer: structure[0][0] },
105
+ { path: "/src/layerB", layer: structure[1][0] },
106
+ { path: "/src/layerA/entry", layer: structure[0][0] },
107
+ ];
108
+ testCases.forEach(({ path, layer }) => {
109
+ it(`The level of ${path} is ${JSON.stringify(layer)}`, () => {
110
+ assert.equal(findLayerWithSimilarPath(structure)(path), layer);
111
+ });
112
+ });
113
+ });
114
+
115
+ describe("createModulePath()", () => {
116
+ const cwd = "/proj";
117
+ const fileDir = "/proj/src/layerA";
118
+ const aliases = [{ alias: "@/", path: "./src/" }];
119
+ const testCases = [
120
+ {
121
+ moduleSource: "@/layerA/layerAA",
122
+ modulePath: "/proj/src/layerA/layerAA",
123
+ },
124
+ {
125
+ moduleSource: "nodeModule",
126
+ modulePath: "nodeModule",
127
+ },
128
+ ];
129
+ testCases.forEach(({ moduleSource, modulePath }) => {
130
+ it(`${moduleSource} -> ${modulePath}`, () => {
131
+ assert.equal(
132
+ createModulePath(cwd, fileDir, aliases)(moduleSource),
133
+ modulePath
134
+ );
135
+ });
136
+ });
137
+ });
138
+ });
@@ -44,6 +44,30 @@ ruleTester.run("no-same-level-funcs", rule, {
44
44
  filename: "./src/foo.js",
45
45
  options: [{ include: ["**/src/**/*.*"], exclude: ["**/foo.js"] }],
46
46
  },
47
+ {
48
+ code: "// @level 2\nfunction func2(){};\n// @level 1\nfunction func1(){ func2(); }",
49
+ filename: "./src/foo.js",
50
+ },
51
+ {
52
+ code: "// @level 2\nconst func2 = () => {};\n// @level 1\nfunction func1(){ func2(); }",
53
+ filename: "./src/foo.js",
54
+ },
55
+ {
56
+ code: "// @level 2\nconst func2 = () => {};\n// @level 1\nconst func1 = () => func2();",
57
+ filename: "./src/foo.js",
58
+ },
59
+ {
60
+ code: "/*@level 2*/\nconst func2 = () => {};\n/*@level 1*/\nconst func1 = () => func2();",
61
+ filename: "./src/foo.js",
62
+ },
63
+ {
64
+ code: "// @level 2 something\nconst func2 = () => {};\n// @level 1 something\nconst func1 = () => func2();",
65
+ filename: "./src/foo.js",
66
+ },
67
+ {
68
+ code: "/*\n@level 2\nsomething\n*/\nconst func2 = () => {};\n/*something\n@level 1\n*/\nconst func1 = () => func2();",
69
+ filename: "./src/foo.js",
70
+ },
47
71
  ],
48
72
  invalid: [
49
73
  {
@@ -71,5 +95,35 @@ ruleTester.run("no-same-level-funcs", rule, {
71
95
  filename: "./src/foo.js",
72
96
  errors: [{ messageId: "no-same-level-funcs", data: { func: "func1" } }],
73
97
  },
98
+ {
99
+ code: "const fn = () => 1; const value = fn()",
100
+ filename: "./src/foo.js",
101
+ errors: [{ messageId: "no-same-level-funcs", data: { func: "fn" } }],
102
+ },
103
+ {
104
+ code: "const fnByHof = hof(() => 1); const value = fnByHof()",
105
+ filename: "./src/foo.js",
106
+ errors: [{ messageId: "no-same-level-funcs", data: { func: "fnByHof" } }],
107
+ },
108
+ {
109
+ code: "const fnByHof = hof(() => 1); const fn = fnByHof(() => 1)",
110
+ filename: "./src/foo.js",
111
+ errors: [{ messageId: "no-same-level-funcs", data: { func: "fnByHof" } }],
112
+ },
113
+ {
114
+ code: "// @level 1\nfunction func1(){};\n// @level 2\nfunction func2(){ func1(); }",
115
+ filename: "./src/foo.js",
116
+ errors: [{ messageId: "no-same-level-funcs", data: { func: "func1" } }],
117
+ },
118
+ {
119
+ code: "// @level 2\nfunction func2(){};\nfunction func1(){ func2(); }",
120
+ filename: "./src/foo.js",
121
+ errors: [{ messageId: "no-same-level-funcs", data: { func: "func2" } }],
122
+ },
123
+ {
124
+ code: "function func2(){};\n// @level 1\nfunction func1(){ func2(); }",
125
+ filename: "./src/foo.js",
126
+ errors: [{ messageId: "no-same-level-funcs", data: { func: "func2" } }],
127
+ },
74
128
  ],
75
129
  });
@@ -0,0 +1,228 @@
1
+ /**
2
+ * @fileoverview test for lower-level-imports
3
+ * @author Hodoug Joung
4
+ */
5
+ "use strict";
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Requirements
9
+ //------------------------------------------------------------------------------
10
+
11
+ const rule = require("../../../lib/rules/stratified-imports");
12
+ const RuleTester = require("eslint").RuleTester;
13
+
14
+ //------------------------------------------------------------------------------
15
+ // Tests
16
+ //------------------------------------------------------------------------------
17
+
18
+ /*
19
+ // .stratified.json
20
+ [
21
+ ["layerA"],
22
+ ["layerB"],
23
+ [{ "name": "layerC", "barrier": true }],
24
+ ["layerD"],
25
+ [{ "name": "nodeModuleE", "nodeModule": "true" }],
26
+ ["layerF"],
27
+ ["layerG", "layerH"],
28
+ ["layerI"],
29
+ ["layerJ"],
30
+ ["layerK"]
31
+ ]
32
+
33
+ // layerB/.stratified.json
34
+ [
35
+ ["layerBA"],
36
+ ["layerBB"]
37
+ ]
38
+
39
+ // layerJ/.stratified.json
40
+ [
41
+ ["entryJA", "entryJB"],
42
+ ["layerJC"]
43
+ ]
44
+ */
45
+
46
+ const ruleTester = new RuleTester({
47
+ parserOptions: { ecmaVersion: 2022, sourceType: "module" },
48
+ });
49
+
50
+ ruleTester.run("stratified-imports", rule, {
51
+ valid: [
52
+ {
53
+ code: "import { func } from './layerB'",
54
+ filename: "./mocked/stratified-imports/layerA.js",
55
+ options: [],
56
+ },
57
+ {
58
+ code: "import { func } from './layerC'",
59
+ filename: "./mocked/stratified-imports/layerA.js",
60
+ options: [],
61
+ },
62
+ {
63
+ code: "import { func } from 'notRegisteredNodeModule'",
64
+ filename: "./mocked/stratified-imports/layerA.js",
65
+ options: [],
66
+ },
67
+ {
68
+ code: "import { func } from 'nodeModuleE'",
69
+ filename: "./mocked/stratified-imports/layerD.js",
70
+ options: [],
71
+ },
72
+ {
73
+ code: "import { func } from './layerH'",
74
+ filename: "./mocked/stratified-imports/layerF.js",
75
+ options: [],
76
+ },
77
+ {
78
+ code: "import { func } from './layerI'",
79
+ filename: "./mocked/stratified-imports/layerH.js",
80
+ options: [],
81
+ },
82
+ {
83
+ code: "import { func } from './layerBB'",
84
+ filename: "./mocked/stratified-imports/layerB/layerBA.js",
85
+ options: [],
86
+ },
87
+ {
88
+ code: "import { func } from '../layerC'",
89
+ filename: "./mocked/stratified-imports/layerB/layerBA.js",
90
+ options: [],
91
+ },
92
+ {
93
+ code: "import { func } from './layerJC'",
94
+ filename: "./mocked/stratified-imports/layerJ/entryJA.js",
95
+ options: [],
96
+ },
97
+ {
98
+ code: "import { func } from './layerJC'",
99
+ filename: "./mocked/stratified-imports/layerJ/entryJB.js",
100
+ options: [],
101
+ },
102
+ {
103
+ code: "import { func } from '../layerK'",
104
+ filename: "./mocked/stratified-imports/layerJ/layerJC.js",
105
+ options: [],
106
+ },
107
+ {
108
+ code: "import { func } from '@/layerB'",
109
+ filename: "./mocked/stratified-imports/layerA.js",
110
+ options: [{ aliases: { "@/": "./mocked/stratified-imports/" } }],
111
+ },
112
+ {
113
+ code: "import { func } from './notRegisteredLayer'",
114
+ filename: "./mocked/stratified-imports/layerA.test.js",
115
+ options: [],
116
+ },
117
+ {
118
+ code: "import { func } from './notRegisteredLayer'",
119
+ filename: "./mocked/stratified-imports/layerA.js",
120
+ options: [{ include: ["**/*.ts"] }],
121
+ },
122
+ {
123
+ code: "import { func } from './notRegisteredLayer'",
124
+ filename: "./mocked/stratified-imports/layerA.js",
125
+ options: [{ exclude: ["**/*.js"] }],
126
+ },
127
+ {
128
+ code: "import { func } from './notRegisteredLayer'",
129
+ filename: "./mocked/stratified-imports/layerA.js",
130
+ options: [{ include: ["**/*.js"], exclude: ["**/layerA.js"] }],
131
+ },
132
+ {
133
+ code: "import { func } from './layerJ/entryJA'",
134
+ filename: "./mocked/stratified-imports/layerI.js",
135
+ options: [],
136
+ },
137
+ ],
138
+ invalid: [
139
+ {
140
+ code: "import { func } from './layerA'",
141
+ filename: "./mocked/stratified-imports/notRegisteredLayer.js",
142
+ options: [],
143
+ errors: [
144
+ {
145
+ messageId: "not-registered",
146
+ data: { file: "notRegisteredLayer" },
147
+ },
148
+ ],
149
+ },
150
+ {
151
+ code: "import { func } from './layerA'",
152
+ filename: "./mocked/stratified-imports/layerB.js",
153
+ options: [],
154
+ errors: [
155
+ {
156
+ messageId: "not-lower-level",
157
+ data: { module: "layerA", file: "layerB" },
158
+ },
159
+ ],
160
+ },
161
+ {
162
+ code: "import { func } from './layerD'",
163
+ filename: "./mocked/stratified-imports/layerB.js",
164
+ options: [],
165
+ errors: [
166
+ {
167
+ messageId: "barrier",
168
+ data: { module: "layerD", file: "layerB" },
169
+ },
170
+ ],
171
+ },
172
+ {
173
+ code: "import { func } from './layerBA'",
174
+ filename: "./mocked/stratified-imports/layerB/layerBB.js",
175
+ options: [],
176
+ errors: [
177
+ {
178
+ messageId: "not-lower-level",
179
+ data: { module: "layerBA", file: "layerBB" },
180
+ },
181
+ ],
182
+ },
183
+ {
184
+ code: "import { func } from '../layerA'",
185
+ filename: "./mocked/stratified-imports/layerB/layerBA.js",
186
+ options: [],
187
+ errors: [
188
+ {
189
+ messageId: "not-lower-level",
190
+ data: { module: "layerA", file: "layerBA" },
191
+ },
192
+ ],
193
+ },
194
+ {
195
+ code: "import { func } from './entryJA'",
196
+ filename: "./mocked/stratified-imports/layerJ/layerJC.js",
197
+ options: [],
198
+ errors: [
199
+ {
200
+ messageId: "not-lower-level",
201
+ data: { module: "entryJA", file: "layerJC" },
202
+ },
203
+ ],
204
+ },
205
+ {
206
+ code: "import { func } from '../layerI'",
207
+ filename: "./mocked/stratified-imports/layerJ/layerJC.js",
208
+ options: [],
209
+ errors: [
210
+ {
211
+ messageId: "not-lower-level",
212
+ data: { module: "layerI", file: "layerJC" },
213
+ },
214
+ ],
215
+ },
216
+ {
217
+ code: "import { func } from './layerJ/notRegisteredEntry'",
218
+ filename: "./mocked/stratified-imports/layerI.js",
219
+ options: [],
220
+ errors: [
221
+ {
222
+ messageId: "not-lower-level",
223
+ data: { module: "notRegisteredEntry", file: "layerI" },
224
+ },
225
+ ],
226
+ },
227
+ ],
228
+ });