eslint-plugin-stratified-design 0.4.0

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/.eslintrc.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+
3
+ module.exports = {
4
+ root: true,
5
+ extends: [
6
+ "eslint:recommended",
7
+ "plugin:eslint-plugin/recommended",
8
+ "plugin:node/recommended",
9
+ ],
10
+ env: {
11
+ node: true,
12
+ },
13
+ overrides: [
14
+ {
15
+ files: ["tests/**/*.js"],
16
+ env: { mocha: true },
17
+ },
18
+ ],
19
+ };
@@ -0,0 +1,18 @@
1
+ name: release
2
+
3
+ on:
4
+ release:
5
+ types: [created]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v3
12
+ - uses: actions/setup-node@v3
13
+ with:
14
+ node-version: 16
15
+ registry-url: https://registry.npmjs.org/
16
+ - run: npm ci
17
+ - run: npm test
18
+ - run: npm publish
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ ISC License (ISCL)
2
+
3
+ Copyright (c) 2022 Hodoug Joung
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # eslint-plugin-stratified-design
2
+
3
+ ESLint rules for stratified design, inspired by "[Grokking Simplicity](https://grokkingsimplicity.com)" written by Erick Normand, for practicing stratified design.
4
+
5
+ ## Installation
6
+
7
+ First, ensure you have [ESLint](https://eslint.org/) installed:
8
+
9
+ ```sh
10
+ npm i eslint --save-dev
11
+ ```
12
+
13
+ Next, install `eslint-plugin-stratified-design`:
14
+
15
+ ```sh
16
+ npm install eslint-plugin-stratified-design --save-dev
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ Add `stratified-design` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix:
22
+
23
+ ```json
24
+ {
25
+ "plugins": ["stratified-design"]
26
+ }
27
+ ```
28
+
29
+ Then configure the rules you wish to use under the rules section:
30
+
31
+ ```json
32
+ {
33
+ "rules": {
34
+ "stratified-design/rule-name": ["error"]
35
+ }
36
+ }
37
+ ```
38
+
39
+ ## Supported Rules
40
+
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
+ - [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.
@@ -0,0 +1,182 @@
1
+ # Require that lower-level modules be imported (lower-level-imports)
2
+
3
+ This rule enforces the requirement for importing lower-level modules. Here's an explanation:
4
+
5
+ - Functions in the same file within the same folder are considered to be at the same level.
6
+ - Child folders are considered to be at a lower level than the parent folder.
7
+ - Installed modules (node modules) are considered to be at the lowest level.
8
+
9
+ However, you can modify this hierarchy using the `structure` option.
10
+
11
+ 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.
12
+
13
+ ### Options
14
+
15
+ The syntax to specify the level structure is as follows:
16
+
17
+ ```json
18
+ "lower-level-imports": ["error", {
19
+ "structure": ["layer1", "layer2", "layer3"]
20
+ }]
21
+ ```
22
+
23
+ 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.
24
+
25
+ To designate a layer as an interface layer, set `interface` to `true`:
26
+
27
+ ```json
28
+ "lower-level-imports": ["error", {
29
+ "structure": ["layer1", { "name": "layer2", "interface": true }, "layer3"],
30
+ }]
31
+ ```
32
+
33
+ For the 'interface layer,' refer to "[Grokking Simplicity](https://grokkingsimplicity.com)."
34
+
35
+ To locate a node module in the structure, set `isNodeModule` to `true`:
36
+
37
+ ```json
38
+ "lower-level-imports": ["error", {
39
+ "structure": ["layer1", { "name": "nodeModule", "isNodeModule": true }, "layer3"],
40
+ }]
41
+ ```
42
+
43
+ The default root directory is the current working directory. To change the root directory, use the `root` option:
44
+
45
+ ```json
46
+ "lower-level-imports": ["error", {
47
+ "structure": ["layer1", "layer2", "layer3"],
48
+ "root": "./src"
49
+ }]
50
+ ```
51
+
52
+ If the name of an imported module has an alias, register the alias using the `aliases` option:
53
+
54
+ ```json
55
+ "lower-level-imports": ["error", {
56
+ "structure": ["layer1", "layer2", "layer3"],
57
+ "aliases": { "@": "./src" }
58
+ }]
59
+ ```
60
+
61
+ If you want to register the level of a layer by 'number,' set the option `useLevelNumber` to `true`:
62
+
63
+ ```json
64
+ "lower-level-imports": ["error", { "useLevelNumber": true }]
65
+ ```
66
+
67
+ The options `structure` and `useLevelNumber` can be used together.
68
+
69
+ You can register the files to apply the rule (`lower-level-imports`) using the `include` and `exclude` options:
70
+
71
+ ```json
72
+ "lower-level-imports": ["error", { "include": ["**/*.js"], "exclude": ["**/*.test.js"] }]
73
+ ```
74
+
75
+ The default is as follows:
76
+
77
+ ```json
78
+ {
79
+ "include": ["**/*.{js,ts,jsx,tsx}"],
80
+ "exclude": ["**/*.{spec,test}.{js,ts,jsx,tsx}"]
81
+ }
82
+ ```
83
+
84
+ ## Rule Details
85
+
86
+ If a file structure is as follows:
87
+
88
+ ```
89
+ src/
90
+ ┣ layer1.js
91
+ ┣ layer2/
92
+ ┃ ┣ file.js
93
+ ┃ ┣ otherFile.js
94
+ ┃ ┗ subFolder/
95
+ ┗ layer3/
96
+ ┣ entry.js
97
+ ┣ 1 layer.js
98
+ ┗ 2 layer.js
99
+ ```
100
+
101
+ Examples of **incorrect** code for this rule:
102
+
103
+ ```js
104
+ /* "lower-level-imports": ["error"] */
105
+ // ./src/layer1.js
106
+ import { func } from "../layer2/file";
107
+ ```
108
+
109
+ ```js
110
+ /* "lower-level-imports": ["error"] */
111
+ // ./src/layer2/file.js
112
+ import { func } from "./otherFile";
113
+ ```
114
+
115
+ ```js
116
+ /* "lower-level-imports": ["error", { "structure": ["layer1", "layer2"] }] */
117
+ // ./src/layer2/file.js
118
+ import { func } from "../layer1";
119
+ ```
120
+
121
+ ```js
122
+ /* "lower-level-imports": ["error", {
123
+ "structure": ["layer1", { name: "layer2", interface: true }, "layer3"]
124
+ }] */
125
+ // ./src/layer1.js
126
+ import { func } from "../layer3/entry";
127
+ ```
128
+
129
+ ```js
130
+ /* "lower-level-imports": ["error", {
131
+ "structure": ["layer1", { "name": "nodeModule", "isNodeModule": true }, "layer3"],
132
+ }] */
133
+ // ./src/layer3/entry.js
134
+ import { func } from "nodeModule";
135
+ ```
136
+
137
+ ```js
138
+ /* "lower-level-imports": ["error", { "useLevelNumber": true }}] */
139
+ // ./src/layer1.js
140
+ import { func } from "layer3/1 layer";
141
+ ```
142
+
143
+ Examples of **correct** code for this rule:
144
+
145
+ ```js
146
+ /* "lower-level-imports": ["error"] */
147
+ // ./src/layer2/file.js
148
+ import { func } from "./subFolder/file";
149
+ ```
150
+
151
+ ```js
152
+ /* "lower-level-imports": ["error", { "structure": ["layer1", "layer2"] }] */
153
+ // ./src/layer1.js
154
+ import { func } from "../layer2/file";
155
+ ```
156
+
157
+ ```js
158
+ /* "lower-level-imports": ["error", {
159
+ "structure": ["layer1", "layer2"],
160
+ "alias": { "@/": "./src/" }
161
+ }] */
162
+ // ./src/layer1.js
163
+ import { func } from "@/layer2/file";
164
+ ```
165
+
166
+ ```js
167
+ /* "lower-level-imports": ["error", { "useLevelNumber": true }] */
168
+ // ./src/layer3/1 layer.js
169
+ import { func } from "./2 layer";
170
+ ```
171
+
172
+ ```js
173
+ /* "lower-level-imports": ["error", { "useLevelNumber": true }] */
174
+ // ./src/layer3/entry.js
175
+ import { func } from "./1 layer";
176
+ ```
177
+
178
+ ```js
179
+ /* "lower-level-imports": ["error", { "structure": ["layer1", "layer3"], "useLevelNumber": true }] */
180
+ // ./src/layer.js
181
+ import { func } from "../layer3/entry";
182
+ ```
@@ -0,0 +1,30 @@
1
+ # Disallow calling functions in the same file (no-same-level-funcs)
2
+
3
+ This rule prohibits calling functions at the same level in the same file.
4
+
5
+ ## Rule Details
6
+
7
+ Examples of **incorrect** code for this rule:
8
+
9
+ ```js
10
+ function func1(...) {
11
+ ...
12
+ }
13
+
14
+ const func2(...) => { ... }
15
+
16
+ function func3(...) {
17
+ func1(...);
18
+ func2(...);
19
+ }
20
+ ```
21
+
22
+ Examples of **correct** code for this rule:
23
+
24
+ ```js
25
+ function func1(...) {
26
+ const func2(...) => { ... };
27
+ ...
28
+ func2(...);
29
+ }
30
+ ```
@@ -0,0 +1,220 @@
1
+ const { minimatch } = require("minimatch");
2
+ const { report: reportError } = require("./2 layer");
3
+ const {
4
+ isNodeModule,
5
+ findLevel: findLayerLevel,
6
+ hasInterface: hasInterfaceBetween,
7
+ removeAlias: removeAliasFromModuleSource,
8
+ } = require("./3 layer");
9
+ const {
10
+ toRelative,
11
+ toSegments,
12
+ toPath,
13
+ resolvePath,
14
+ parsePath,
15
+ } = require("./4 layer");
16
+
17
+ const FINISHED = "finished";
18
+
19
+ /**
20
+ *
21
+ * @param {string} cwd
22
+ * @param {*} options
23
+ * @returns
24
+ */
25
+ const createRootDir = (cwd, options) => {
26
+ return resolvePath(cwd, options.root);
27
+ };
28
+
29
+ /**
30
+ * @param {*} options
31
+ * @param {string} contextFileSource
32
+ */
33
+ const parseFileSource = (options, contextFileSource) => {
34
+ const fileSource = resolvePath(contextFileSource);
35
+ const parsedFileSource = parsePath(fileSource);
36
+ const fileDir = resolvePath(parsedFileSource.dir);
37
+ const filePath = resolvePath(fileDir, parsedFileSource.name);
38
+ const isIncludedFile = options.include.find((pattern) =>
39
+ minimatch(fileSource, pattern)
40
+ );
41
+ const isExcludedFile = options.exclude.find((pattern) =>
42
+ minimatch(fileSource, pattern)
43
+ );
44
+ return {
45
+ fileDir,
46
+ filePath,
47
+ isExcludedFile: !isIncludedFile || isExcludedFile,
48
+ };
49
+ };
50
+
51
+ /**
52
+ * @param {string} cwd
53
+ * @param {string} fileDir
54
+ * @param {{alias: string, path: string}[]} aliases
55
+ */
56
+ const createModulePath = (cwd, fileDir, aliases) => {
57
+ const removeAlias = removeAliasFromModuleSource(cwd, fileDir, aliases);
58
+ /**
59
+ * @param {string} moduleSourceWithAlias
60
+ */
61
+ return (moduleSourceWithAlias) => {
62
+ const moduleSource = removeAlias(moduleSourceWithAlias);
63
+ const isNodeModule = moduleSource.startsWith(".") === false;
64
+ return isNodeModule ? moduleSource : resolvePath(fileDir, moduleSource);
65
+ };
66
+ };
67
+
68
+ /**
69
+ * Report error about using `options.useLevelNumber`
70
+ * @param {import('eslint').Rule.RuleContext} context
71
+ * @param {*} options
72
+ * @param {string} rootDir
73
+ * @param {string} filePath
74
+ */
75
+ const reportHasProperLevelNumber = (context, options, rootDir, filePath) => {
76
+ /**
77
+ * @param {import('eslint').Rule.NodeParentExtension} node
78
+ * @param {string} modulePath
79
+ */
80
+ return (node, modulePath) => {
81
+ if (!options.useLevelNumber) return;
82
+
83
+ const report = (messageId) =>
84
+ reportError(context, rootDir, filePath)(node, messageId, modulePath);
85
+
86
+ const extractLevel = (segment) => {
87
+ const level = segment.split(" ")[0];
88
+ return /^[\d]+$/.test(level) ? Number(level) : null;
89
+ };
90
+
91
+ const parentPath = (() => {
92
+ const moduleSegment = toSegments(modulePath);
93
+ const fileSegment = toSegments(filePath);
94
+ const parent = moduleSegment.reduce((parent, seg, index) => {
95
+ if (fileSegment[index] === seg) parent.push(seg);
96
+ return parent;
97
+ }, []);
98
+ return toPath(parent);
99
+ })();
100
+
101
+ const { moduleLevel, isInterfaceError } = (() => {
102
+ const segments = toSegments(toRelative(parentPath, modulePath));
103
+ const level = extractLevel(segments[1]);
104
+ return {
105
+ moduleLevel: level,
106
+ isInterfaceError: segments.length > 2,
107
+ };
108
+ })();
109
+
110
+ if (isInterfaceError) {
111
+ report("interface");
112
+ return FINISHED;
113
+ }
114
+
115
+ if (moduleLevel === null) return;
116
+
117
+ const fileLevel = (() => {
118
+ const segments = toSegments(toRelative(parentPath, filePath));
119
+ const level = extractLevel(segments[1]);
120
+ if (level === null && segments.length === 2 && segments[0] == ".")
121
+ return -1;
122
+ return level;
123
+ })();
124
+
125
+ if (fileLevel === null) {
126
+ report("interface");
127
+ return FINISHED;
128
+ }
129
+
130
+ if (fileLevel >= moduleLevel) {
131
+ report("not-lower-level");
132
+ return FINISHED;
133
+ }
134
+
135
+ return FINISHED;
136
+ };
137
+ };
138
+
139
+ /**
140
+ * @param {string} fileDir
141
+ * @return return `FINISHED` if the imported module(`modulePath`) is in a sub directory of the file directory(`fileDir`)
142
+ */
143
+ const reportInSubDirOfFileDir = (fileDir) => {
144
+ /**
145
+ * @param {import('eslint').Rule.NodeParentExtension} node
146
+ * @param {string} modulePath
147
+ */
148
+ return (node, modulePath) => {
149
+ const relModulePath = toRelative(fileDir, modulePath);
150
+ if (
151
+ relModulePath.startsWith("..") === false &&
152
+ toSegments(relModulePath).length >= 3
153
+ ) {
154
+ return FINISHED;
155
+ }
156
+ };
157
+ };
158
+
159
+ /**
160
+ * Report error about using `options.structure`
161
+ * @param {import('eslint').Rule.RuleContext} context
162
+ * @param {{[string]: string | {name: string, isNodeModule: boolean, interface: boolean}}} structure
163
+ * @param {string} rootDir
164
+ * @param {number | null} fileLevel
165
+ * @param {string} filePath
166
+ */
167
+ const reportHasProperLevel = (
168
+ context,
169
+ structure,
170
+ rootDir,
171
+ fileLevel,
172
+ filePath
173
+ ) => {
174
+ const findLevel = findLayerLevel(structure);
175
+ const hasInterface = hasInterfaceBetween(structure, fileLevel);
176
+
177
+ /**
178
+ * @param {import('eslint').Rule.NodeParentExtension} node
179
+ * @param {string} modulePath
180
+ */
181
+ return (node, modulePath) => {
182
+ const report = (messageId) =>
183
+ reportError(context, rootDir, filePath)(node, messageId, modulePath);
184
+
185
+ const isNodeModulePath = isNodeModule(rootDir)(modulePath);
186
+
187
+ if (fileLevel === null) {
188
+ if (!isNodeModulePath) report("not-registered:file");
189
+ return FINISHED;
190
+ }
191
+
192
+ const moduleLevel = findLevel(modulePath);
193
+ if (moduleLevel === null) {
194
+ if (!isNodeModulePath) report("not-registered:module");
195
+ return FINISHED;
196
+ }
197
+
198
+ if (fileLevel >= moduleLevel) {
199
+ report("not-lower-level");
200
+ return FINISHED;
201
+ }
202
+
203
+ if (hasInterface(moduleLevel)) {
204
+ report("interface");
205
+ return FINISHED;
206
+ }
207
+
208
+ return FINISHED;
209
+ };
210
+ };
211
+
212
+ module.exports = {
213
+ FINISHED,
214
+ createRootDir,
215
+ parseFileSource,
216
+ createModulePath,
217
+ reportHasProperLevelNumber,
218
+ reportInSubDirOfFileDir,
219
+ reportHasProperLevel,
220
+ };
@@ -0,0 +1,25 @@
1
+ const { isNodeModule } = require("./3 layer");
2
+ const { toRelative } = require("./4 layer");
3
+
4
+ /**
5
+ * Report eslint error
6
+ * @param {import('eslint').Rule.RuleContext} context
7
+ * @param {string} rootDir
8
+ * * @param {string} filePath
9
+ */
10
+ const report = (context, rootDir, filePath) => {
11
+ /**
12
+ * @param {import('eslint').Rule.NodeParentExtension} node
13
+ * @param {string} messageId
14
+ * @param {string} modulePath
15
+ */
16
+ return (node, messageId, modulePath) => {
17
+ const module = isNodeModule(rootDir)(modulePath)
18
+ ? modulePath
19
+ : toRelative(rootDir, modulePath);
20
+ const file = toRelative(rootDir, filePath);
21
+ context.report({ node, messageId, data: { module, file } });
22
+ };
23
+ };
24
+
25
+ module.exports = { report };
@@ -0,0 +1,108 @@
1
+ const {
2
+ toSegments,
3
+ toPath,
4
+ joinPath,
5
+ resolvePath,
6
+ toRelative,
7
+ } = require("./4 layer");
8
+
9
+ /**
10
+ * @param {*} options
11
+ * @param {string} rootDir
12
+ */
13
+ const createStructure = (options, rootDir) => {
14
+ return options.structure.map((layer) => {
15
+ const theLayer = typeof layer === "string" ? { name: layer } : layer;
16
+ return theLayer.isNodeModule
17
+ ? theLayer
18
+ : { ...theLayer, name: joinPath(rootDir, theLayer.name) };
19
+ });
20
+ };
21
+
22
+ /**
23
+ * @param {*} options
24
+ */
25
+ const createAliases = (options) => {
26
+ return Object.keys(options.aliases)
27
+ .sort((a, b) => b.length - a.length)
28
+ .map((alias) => ({ alias, path: options.aliases[alias] }));
29
+ };
30
+
31
+ /**
32
+ * Replace an alias into the corresponding path
33
+ * @param {string} cwd
34
+ * @param {string} fileDir
35
+ * @param {{alias: string, path: string}[]} aliases
36
+ */
37
+ const removeAlias = (cwd, fileDir, aliases) => {
38
+ /**
39
+ * @param {string} moduleSource
40
+ */
41
+ return (moduleSource) => {
42
+ const { alias, path } =
43
+ aliases.find(({ alias }) => moduleSource.startsWith(alias)) || {};
44
+ if (!alias) return moduleSource;
45
+ const modulePath = resolvePath(cwd, moduleSource.replace(alias, path));
46
+ return toRelative(fileDir, modulePath);
47
+ };
48
+ };
49
+
50
+ /**
51
+ * Check if a module is node module
52
+ * @param {string} rootDir
53
+ * @returns `true` if `modulePath` is node module path
54
+ */
55
+ const isNodeModule = (rootDir) => {
56
+ /**
57
+ * @param {string} modulePath
58
+ */
59
+ return (modulePath) => {
60
+ return modulePath.startsWith(rootDir) === false;
61
+ };
62
+ };
63
+
64
+ /**
65
+ * Find the layer level for a module
66
+ * @param {{[string]: string | {name: string, isNodeModule: boolean, interface: boolean}}} structure
67
+ * @returns the level of the module with `path`
68
+ */
69
+ const findLevel = (structure) => {
70
+ /**
71
+ * @param {string} path
72
+ */
73
+ return (path) => {
74
+ const segments = toSegments(path);
75
+ const level = segments.reduce((level, _, index) => {
76
+ if (level >= 0) return level;
77
+ const path = toPath(segments.slice(0, segments.length - index));
78
+ return structure.findIndex((layer) => layer.name === path);
79
+ }, -1);
80
+ return level >= 0 ? level : null;
81
+ };
82
+ };
83
+
84
+ /**
85
+ * Check if there is an interface between file layer and module layer
86
+ * @param {{[string]: string | {name: string, isNodeModule: boolean, interface: boolean}}} structure
87
+ * @param {number} fileLevel
88
+ */
89
+ const hasInterface = (structure, fileLevel) => {
90
+ /**
91
+ * @param {number} moduleLevel
92
+ */
93
+ return (moduleLevel) => {
94
+ const layerInterface = structure
95
+ .slice(fileLevel + 1, moduleLevel)
96
+ .find((layer) => layer.interface);
97
+ return Boolean(layerInterface);
98
+ };
99
+ };
100
+
101
+ module.exports = {
102
+ isNodeModule,
103
+ findLevel,
104
+ hasInterface,
105
+ createStructure,
106
+ createAliases,
107
+ removeAlias,
108
+ };