eslint-plugin-stratified-design 0.7.0 → 0.8.0-beta.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/README.md CHANGED
@@ -39,4 +39,5 @@ Then configure the rules you wish to use under the rules section:
39
39
  ## Supported Rules
40
40
 
41
41
  - [lower-level-imports](https://github.com/anisotropy/eslint-plugin-stratified-design/blob/main/docs/rules/lower-level-imports.md): Requires lower-level modules to be imported.
42
+ - [lower-level-imports](https://github.com/anisotropy/eslint-plugin-stratified-design/blob/main/docs/rules/stratified-imports.md): Requires lower-level modules to be imported. The stratified structure is set by `.stratified.json`.
42
43
  - [no-same-level-funcs](https://github.com/anisotropy/eslint-plugin-stratified-design/blob/main/docs/rules/no-same-level-funcs.md): Disallows calling functions in the same file.
@@ -15,9 +15,14 @@ This rule works correctly on POSIX systems, where the path segment separator is
15
15
  The syntax to specify the level structure is as follows:
16
16
 
17
17
  ```json
18
- "lower-level-imports": ["error", {
19
- "structure": ["layer1", "layer2", "layer3"]
20
- }]
18
+ {
19
+ "stratified-design/lower-level-imports": [
20
+ "error",
21
+ {
22
+ "structure": ["layer1", "layer2", "layer3"]
23
+ }
24
+ ]
25
+ }
21
26
  ```
22
27
 
23
28
  In the folder array, the file or folder on the left is considered to be at a higher level than the one on the right.
@@ -25,9 +30,14 @@ In the folder array, the file or folder on the left is considered to be at a hig
25
30
  To designate a layer as an abstract barrier, set `barrier` to `true`:
26
31
 
27
32
  ```json
28
- "lower-level-imports": ["error", {
29
- "structure": ["layer1", { "name": "layer2", "barrier": true }, "layer3"],
30
- }]
33
+ {
34
+ "stratified-design/lower-level-imports": [
35
+ "error",
36
+ {
37
+ "structure": ["layer1", { "name": "layer2", "barrier": true }, "layer3"]
38
+ }
39
+ ]
40
+ }
31
41
  ```
32
42
 
33
43
  For the 'abstract barrier,' refer to "[Grokking Simplicity](https://grokkingsimplicity.com)."
@@ -35,33 +45,54 @@ For the 'abstract barrier,' refer to "[Grokking Simplicity](https://grokkingsimp
35
45
  To locate a node module in the structure, set `nodeModule` to `true`:
36
46
 
37
47
  ```json
38
- "lower-level-imports": ["error", {
39
- "structure": ["layer1", { "name": "nodeModule", "nodeModule": true }, "layer3"],
40
- }]
48
+ {
49
+ "stratified-design/lower-level-imports": [
50
+ "error",
51
+ {
52
+ "structure": [
53
+ "layer1",
54
+ { "name": "nodeModule", "nodeModule": true },
55
+ "layer3"
56
+ ]
57
+ }
58
+ ]
59
+ }
41
60
  ```
42
61
 
43
62
  The default root directory is the current working directory. To change the root directory, use the `root` option:
44
63
 
45
64
  ```json
46
- "lower-level-imports": ["error", {
47
- "structure": ["layer1", "layer2", "layer3"],
48
- "root": "./src"
49
- }]
65
+ {
66
+ "stratified-design/lower-level-imports": [
67
+ "error",
68
+ {
69
+ "structure": ["layer1", "layer2", "layer3"],
70
+ "root": "./src"
71
+ }
72
+ ]
73
+ }
50
74
  ```
51
75
 
52
76
  If the name of an imported module has an alias, register the alias using the `aliases` option:
53
77
 
54
78
  ```json
55
- "lower-level-imports": ["error", {
56
- "structure": ["layer1", "layer2", "layer3"],
57
- "aliases": { "@": "./src" }
58
- }]
79
+ {
80
+ "stratified-design/lower-level-imports": [
81
+ "error",
82
+ {
83
+ "structure": ["layer1", "layer2", "layer3"],
84
+ "aliases": { "@": "./src" }
85
+ }
86
+ ]
87
+ }
59
88
  ```
60
89
 
61
90
  If you want to register the level of a layer by 'number,' set the option `useLevelNumber` to `true`:
62
91
 
63
92
  ```json
64
- "lower-level-imports": ["error", { "useLevelNumber": true }]
93
+ {
94
+ "stratified-design/lower-level-imports": ["error", { "useLevelNumber": true }]
95
+ }
65
96
  ```
66
97
 
67
98
  The options `structure` and `useLevelNumber` can be used together.
@@ -69,13 +100,20 @@ The options `structure` and `useLevelNumber` can be used together.
69
100
  An `index.xxx` file can be the highest level layer of sibling files when the option `isIndexHighest` is set to `true`:
70
101
 
71
102
  ```json
72
- "lower-level-imports": ["error", { "isIndexHighest": true }]
103
+ {
104
+ "stratified-design/lower-level-imports": ["error", { "isIndexHighest": true }]
105
+ }
73
106
  ```
74
107
 
75
108
  You can register the files to apply the rule (`lower-level-imports`) using the `include` and `exclude` options:
76
109
 
77
110
  ```json
78
- "lower-level-imports": ["error", { "include": ["**/*.js"], "exclude": ["**/*.test.js"] }]
111
+ {
112
+ "stratified-design/lower-level-imports": [
113
+ "error",
114
+ { "include": ["**/*.js"], "exclude": ["**/*.test.js"] }
115
+ ]
116
+ }
79
117
  ```
80
118
 
81
119
  The default is as follows:
@@ -7,7 +7,12 @@ This rule prohibits calling functions at the same level in the same file.
7
7
  You can register the files to apply the rule (`no-same-level-funcs`) using the `include` and `exclude` options:
8
8
 
9
9
  ```json
10
- "no-same-level-funcs": ["error", { "include": ["**/*.js"], "exclude": ["**/*.test.js"] }]
10
+ {
11
+ "stratified-design/no-same-level-funcs": [
12
+ "error",
13
+ { "include": ["**/*.js"], "exclude": ["**/*.test.js"] }
14
+ ]
15
+ }
11
16
  ```
12
17
 
13
18
  The default is as follows:
@@ -0,0 +1,178 @@
1
+ # Require that lower-level modules be imported (stratified-imports)
2
+
3
+ (Note: This rule works correctly on POSIX systems, where the path segment separator is `/`. It will be updated to work well on Windows systems in the future.)
4
+
5
+ This rule enforces the requirement for importing lower-level modules. The hierarchy should be set by `.stratified.json` in **each folder** as follows:
6
+
7
+ ```json
8
+ [
9
+ ["layerA"],
10
+ [{ "name": "layerB", "barrier": true }],
11
+ [{ "name": "nodeModuleC", "nodeModule": true }],
12
+ ["layerD", "layerE"]
13
+ ]
14
+ ```
15
+
16
+ And consider that the folder structure is as follows:
17
+
18
+ ```
19
+ ┣ layerA
20
+ ┣ layerB
21
+ ┣ layerD
22
+ ┣ layerE
23
+ ┃ ┣ index.js
24
+ ┃ ┣ entry
25
+ ┃ ┣ layerEA
26
+ ┃ ┗ .stratified.json
27
+ ┗ .stratified.json
28
+ ```
29
+
30
+ The above JSON file indicates the following:
31
+
32
+ - The `layerA` file/folder at the highest level.
33
+ - The `layerB` file/folder is a lower-level layer than `layerA` and serves as an abstract barrier. (For the concept of 'abstract barrier,' refer to '[Grokking Simplicity](https://grokkingsimplicity.com).')
34
+ - `nodeModuleC` is an **installed module** (node module) and is at a lower level than `layerB`. (Unregistered node modules are considered to be the lowest layers.)
35
+ - The `layerD` file/folder and the `layerE` file/folder are at the same level and represent the lowest level layers.
36
+
37
+ Consider that the `.stratified.json` in the `layerE` folder is as follows:
38
+
39
+ ```json
40
+ [["index", "entry"], ["layerEA"]]
41
+ ```
42
+
43
+ Higher-level layers than `layerE` can import `./layerE` and `./layer/entry` as follows:
44
+
45
+ ```js
46
+ import { func } from "./layerE";
47
+ import { func } from "./layerE/entry";
48
+ ```
49
+
50
+ However, `./layer/layerEA` should not be imported.
51
+
52
+ ## Options
53
+
54
+ If an imported module has an alias, register the alias using the `aliases` option:
55
+
56
+ ```json
57
+ {
58
+ "stratified-design/stratified-imports": [
59
+ "error",
60
+ {
61
+ "aliases": { "@/": "./src/" }
62
+ }
63
+ ]
64
+ }
65
+ ```
66
+
67
+ You can register the files to which the rule (`stratified-imports`) should apply using the `include` and `exclude` options:
68
+
69
+ ```json
70
+ {
71
+ "stratified-design/lower-level-imports": [
72
+ "error",
73
+ { "include": ["**/*.js"], "exclude": ["**/*.test.js"] }
74
+ ]
75
+ }
76
+ ```
77
+
78
+ The default configuration is as follows:
79
+
80
+ ```json
81
+ {
82
+ "include": ["**/*.{js,ts,jsx,tsx}"],
83
+ "exclude": ["**/*.{spec,test}.{js,ts,jsx,tsx}"]
84
+ }
85
+ ```
86
+
87
+ ## Rule Details
88
+
89
+ Consider the following folder structure:
90
+
91
+ ```
92
+ src/
93
+ ┣ layerA.js
94
+ ┣ layerB.js
95
+ ┣ layerD.js
96
+ ┣ layerE/
97
+ ┃ ┣ index.js
98
+ ┃ ┣ entry
99
+ ┃ ┣ layerEA.js
100
+ ┃ ┗ .stratified.json
101
+ ┗ .stratified.json
102
+ ```
103
+
104
+ and the `.stratified.json` in `src/` is as follows:
105
+
106
+ ```json
107
+ [
108
+ ["layerA"],
109
+ [{ "name": "layerB", "barrier": true }],
110
+ [{ "name": "nodeModuleC", "nodeModule": true }],
111
+ ["layerD", "layerE"]
112
+ ]
113
+ ```
114
+
115
+ and the `.stratified.json` in `layerE/` is as follows:
116
+
117
+ ```json
118
+ [["index", "entry"], ["layerEA"]]
119
+ ```
120
+
121
+ Examples of **incorrect** code for this rule:
122
+
123
+ ```js
124
+ // ./layerB.js
125
+ import { func } from "./layerA";
126
+ ```
127
+
128
+ ```js
129
+ // ./layerA.js
130
+ import { func } from "./layerD";
131
+ ```
132
+
133
+ ```js
134
+ // ./layerD.js
135
+ import { func } from "nodeModuleC";
136
+ ```
137
+
138
+ ```js
139
+ // ./layerD.js
140
+ import { func } from "layerE";
141
+ ```
142
+
143
+ ```js
144
+ // ./layerB.js
145
+ import { func } from "layerE/layerEA";
146
+ ```
147
+
148
+ Examples of **correct** code for this rule:
149
+
150
+ ```js
151
+ // ./layerA.js
152
+ import { func } from "./layerB";
153
+ ```
154
+
155
+ ```js
156
+ // ./layerB.js
157
+ import { func } from "nodeModuleC";
158
+ ```
159
+
160
+ ```js
161
+ // ./layerD.js
162
+ import { func } from "some-node-module";
163
+ ```
164
+
165
+ ```js
166
+ // ./layerB.js
167
+ import { func } from "./layerD";
168
+ ```
169
+
170
+ ```js
171
+ // ./layerB.js
172
+ import { func } from "./layerE";
173
+ ```
174
+
175
+ ```js
176
+ // ./layerB.js
177
+ import { func } from "./layerE/entry";
178
+ ```
@@ -1,5 +1,6 @@
1
1
  const p = require("path");
2
2
 
3
+ // @level 2
3
4
  /**
4
5
  * @param {string} from
5
6
  * @param {string} to
@@ -10,27 +11,34 @@ const toRelative = (from, to) => {
10
11
  return `${to}/`.startsWith(`${from}/`) ? `./${rel}` : rel;
11
12
  };
12
13
 
14
+ // @level 2
13
15
  /**
14
16
  * @param {string} path
15
17
  * @returns path to segments
16
18
  */
17
19
  const toSegments = (path) => path.split("/");
18
20
 
21
+ // @level 2
19
22
  /**
20
23
  * @param {string[]} segments
21
24
  * @returns segments to path
22
25
  */
23
26
  const toPath = (segments) => segments.join("/");
24
27
 
28
+ // @level 2
25
29
  const joinPath = p.join;
26
30
 
31
+ // @level 2
27
32
  const resolvePath = p.resolve;
28
33
 
34
+ // @level 2
29
35
  const parsePath = p.parse;
30
36
 
37
+ // @level 2
31
38
  /**
32
- * @param {any[]} array
33
- * @param {(item: any) => boolean} callback
39
+ * @template T
40
+ * @param {T[]} array
41
+ * @param {(item: T) => boolean} callback
34
42
  * @returns {number}
35
43
  */
36
44
  const findLastIndex = (array, callback) => {
@@ -41,6 +49,7 @@ const findLastIndex = (array, callback) => {
41
49
  }, -1);
42
50
  };
43
51
 
52
+ // @level 2
44
53
  /**
45
54
  * @param {any[]} array1
46
55
  * @param {any[]} array2
@@ -49,6 +58,29 @@ const equal = (array1, array2) => {
49
58
  return array1.every((item, index) => item === array2[index]);
50
59
  };
51
60
 
61
+ // @level 2
62
+ /**
63
+ * @template T
64
+ * @param {T[]} array
65
+ * @param {number} index
66
+ * @returns {T}
67
+ */
68
+ const readArray = (array, index) => {
69
+ return index >= 0 ? array[index] : array[array.length + index];
70
+ };
71
+
72
+ // @level 1
73
+ /**
74
+ * @param {string} path
75
+ * @return {string[]}
76
+ */
77
+ const reducedPaths = (path) => {
78
+ return toSegments(path).reduce((paths, _, index, segments) => {
79
+ paths.push(toPath(segments.slice(0, segments.length - index)));
80
+ return paths;
81
+ }, []);
82
+ };
83
+
52
84
  module.exports = {
53
85
  toRelative,
54
86
  toSegments,
@@ -58,4 +90,6 @@ module.exports = {
58
90
  parsePath,
59
91
  findLastIndex,
60
92
  equal,
93
+ readArray,
94
+ reducedPaths,
61
95
  };
@@ -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,14 +12,14 @@ 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
 
19
19
  /**
20
20
  *
21
21
  * @param {string} cwd
22
- * @param {*} options
22
+ * @param {import("./3 layer").Options} options
23
23
  * @returns
24
24
  */
25
25
  const createRootDir = (cwd, options) => {
@@ -27,7 +27,7 @@ const createRootDir = (cwd, options) => {
27
27
  };
28
28
 
29
29
  /**
30
- * @param {*} options
30
+ * @param {import("./3 layer").Options} options
31
31
  * @param {string} contextFileSource
32
32
  */
33
33
  const parseFileSource = (options, contextFileSource) => {
@@ -50,10 +50,11 @@ const parseFileSource = (options, contextFileSource) => {
50
50
 
51
51
  /**
52
52
  * @param {string} cwd
53
+ * @param {string[]} excludeImports
53
54
  * @param {string} fileDir
54
55
  * @param {{alias: string, path: string}[]} aliases
55
56
  */
56
- const createModulePath = (cwd, fileDir, aliases) => {
57
+ const createModulePath = (cwd, excludeImports, fileDir, aliases) => {
57
58
  const removeAlias = removeAliasFromModuleSource(cwd, fileDir, aliases);
58
59
  /**
59
60
  * @param {string} moduleSourceWithAlias
@@ -61,12 +62,18 @@ const createModulePath = (cwd, fileDir, aliases) => {
61
62
  return (moduleSourceWithAlias) => {
62
63
  const moduleSource = removeAlias(moduleSourceWithAlias);
63
64
  const isNodeModule = moduleSource.startsWith(".") === false;
64
- return isNodeModule ? moduleSource : resolvePath(fileDir, moduleSource);
65
+ const modulePath = isNodeModule
66
+ ? moduleSource
67
+ : resolvePath(fileDir, moduleSource);
68
+ const isModuleExcluded = Boolean(
69
+ excludeImports.find((pattern) => minimatch(modulePath, pattern))
70
+ );
71
+ return { modulePath, isModuleExcluded };
65
72
  };
66
73
  };
67
74
 
68
75
  /**
69
- * @param {*} options
76
+ * @param {import("./3 layer").Options} options
70
77
  * @param {string} fileDor
71
78
  * @param {string} modulePath
72
79
  */
@@ -82,7 +89,7 @@ const isFileIndexOfModule = (options, fileDir, filePath) => (modulePath) => {
82
89
  /**
83
90
  * Report error about using `options.useLevelNumber`
84
91
  * @param {import('eslint').Rule.RuleContext} context
85
- * @param {*} options
92
+ * @param {import("./3 layer").Options} options
86
93
  * @param {string} rootDir
87
94
  * @param {string} filePath
88
95
  */
@@ -191,10 +198,10 @@ const reportHasProperLevel = (
191
198
  filePath
192
199
  ) => {
193
200
  const findLevel = findLayerLevel(structure);
194
- const hasInterface = hasInterfaceBetween(structure, fileLevel);
201
+ const hasBarrier = hasBarrierBetween(structure, fileLevel);
195
202
 
196
203
  /**
197
- * @param {import('eslint').Rule.NodeParentExtension} node
204
+ * @param {import('../type').Node} node
198
205
  * @param {string} modulePath
199
206
  */
200
207
  return (node, modulePath) => {
@@ -222,7 +229,7 @@ const reportHasProperLevel = (
222
229
  return FINISHED;
223
230
  }
224
231
 
225
- if (hasInterface(moduleLevel)) {
232
+ if (hasBarrier(moduleLevel)) {
226
233
  report("barrier");
227
234
  return FINISHED;
228
235
  }
@@ -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,