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.
@@ -0,0 +1,61 @@
1
+ const p = require("path");
2
+
3
+ /**
4
+ * @param {string} from
5
+ * @param {string} to
6
+ * @returns the relative path from `from` to `to`
7
+ */
8
+ const toRelative = (from, to) => {
9
+ const rel = p.relative(from, to);
10
+ return `${to}/`.startsWith(`${from}/`) ? `./${rel}` : rel;
11
+ };
12
+
13
+ /**
14
+ * @param {string} path
15
+ * @returns path to segments
16
+ */
17
+ const toSegments = (path) => path.split("/");
18
+
19
+ /**
20
+ * @param {string[]} segments
21
+ * @returns segments to path
22
+ */
23
+ const toPath = (segments) => segments.join("/");
24
+
25
+ const joinPath = p.join;
26
+
27
+ const resolvePath = p.resolve;
28
+
29
+ const parsePath = p.parse;
30
+
31
+ /**
32
+ * @param {any[]} array
33
+ * @param {(item: any) => boolean} callback
34
+ * @returns {number}
35
+ */
36
+ const findLastIndex = (array, callback) => {
37
+ return array.reduce((lastIndex, _, index) => {
38
+ if (lastIndex >= 0) return lastIndex;
39
+ const trueIndex = array.length - 1 - index;
40
+ return callback(array[trueIndex]) ? trueIndex : -1;
41
+ }, -1);
42
+ };
43
+
44
+ /**
45
+ * @param {any[]} array1
46
+ * @param {any[]} array2
47
+ */
48
+ const equal = (array1, array2) => {
49
+ return array1.every((item, index) => item === array2[index]);
50
+ };
51
+
52
+ module.exports = {
53
+ toRelative,
54
+ toSegments,
55
+ toPath,
56
+ joinPath,
57
+ resolvePath,
58
+ parsePath,
59
+ findLastIndex,
60
+ equal,
61
+ };
@@ -0,0 +1,23 @@
1
+ const {
2
+ createRootDir,
3
+ reportHasProperLevel: reportHasProperLayerLevel,
4
+ reportInSubDirOfFileDir: reportInSubDirOf,
5
+ FINISHED,
6
+ reportHasProperLevelNumber: reportHasProperLayerLevelNumber,
7
+ parseFileSource,
8
+ createModulePath: createModulePathFromSource,
9
+ } = require("./1 layer");
10
+ const { findLevel, createStructure, createAliases } = require("./3 layer");
11
+
12
+ module.exports = {
13
+ FINISHED,
14
+ createRootDir,
15
+ parseFileSource,
16
+ reportHasProperLayerLevel,
17
+ reportInSubDirOf,
18
+ reportHasProperLayerLevelNumber,
19
+ createModulePathFromSource,
20
+ findLevel,
21
+ createStructure,
22
+ createAliases,
23
+ };
package/lib/index.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @fileoverview Stratified Design
3
+ * @author Hodoug Joung
4
+ */
5
+ "use strict";
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Requirements
9
+ //------------------------------------------------------------------------------
10
+
11
+ const requireIndex = require("requireindex");
12
+
13
+ //------------------------------------------------------------------------------
14
+ // Plugin Definition
15
+ //------------------------------------------------------------------------------
16
+
17
+
18
+ // import all rules in lib/rules
19
+ module.exports.rules = requireIndex(__dirname + "/rules");
20
+
21
+
22
+
@@ -0,0 +1,165 @@
1
+ /**
2
+ * @fileoverview Require that lower level modules be imported (lower-level-imports)
3
+ * @author Hodoug Joung
4
+ */
5
+
6
+ "use strict";
7
+
8
+ //------------------------------------------------------------------------------
9
+ // Requirements
10
+ //------------------------------------------------------------------------------
11
+
12
+ const {
13
+ reportHasProperLayerLevel,
14
+ reportInSubDirOf,
15
+ FINISHED,
16
+ reportHasProperLayerLevelNumber,
17
+ createModulePathFromSource,
18
+ findLevel,
19
+ createStructure,
20
+ createAliases,
21
+ parseFileSource,
22
+ createRootDir,
23
+ } = require("../helpers/lowerLevelImports/");
24
+
25
+ //------------------------------------------------------------------------------
26
+ // Rule Definition
27
+ //------------------------------------------------------------------------------
28
+
29
+ const layerNamePattern = "^[^/]+(/[^/]+)*$";
30
+
31
+ const layerSchema = {
32
+ oneOf: [
33
+ { type: "string", pattern: layerNamePattern },
34
+ {
35
+ type: "object",
36
+ properties: {
37
+ name: {
38
+ type: "string",
39
+ pattern: layerNamePattern,
40
+ },
41
+ interface: { type: "boolean" },
42
+ isNodeModule: { type: "boolean" },
43
+ },
44
+ required: ["name"],
45
+ additionalProperties: false,
46
+ },
47
+ ],
48
+ };
49
+
50
+ /** @type {import('eslint').Rule.RuleModule} */
51
+ module.exports = {
52
+ meta: {
53
+ type: "problem",
54
+ fixable: "code",
55
+ schema: {
56
+ type: "array",
57
+ items: [
58
+ {
59
+ type: "object",
60
+ properties: {
61
+ structure: {
62
+ type: "array",
63
+ items: layerSchema,
64
+ },
65
+ root: {
66
+ type: "string",
67
+ pattern: "^\\.{1,2}(/[^/]+)*$",
68
+ },
69
+ aliases: {
70
+ type: "object",
71
+ patternProperties: {
72
+ ["^.+$"]: {
73
+ type: "string",
74
+ pattern: "^\\.{1,2}(/[^/]+)*/?$",
75
+ },
76
+ },
77
+ additionalProperties: false,
78
+ },
79
+ exclude: {
80
+ type: "array",
81
+ items: [{ type: "string" }],
82
+ },
83
+ include: {
84
+ type: "array",
85
+ items: [{ type: "string" }],
86
+ },
87
+ useLevelNumber: {
88
+ type: "boolean",
89
+ },
90
+ },
91
+ additionalProperties: false,
92
+ },
93
+ ],
94
+ additionalItems: false,
95
+ },
96
+ messages: {
97
+ "not-lower-level": "'{{module}}' is NOT LOWER level than '{{file}}'",
98
+ interface: "An INTERFACE prevents '{{file}}' from importing '{{module}}'",
99
+ "not-registered:file":
100
+ "'{{file}}' does NOT registered at layer structure",
101
+ "not-registered:module":
102
+ "'{{module}}' does NOT registered at layer structure",
103
+ },
104
+ },
105
+ create(context) {
106
+ const options = {
107
+ root: "./",
108
+ structure: [],
109
+ exclude: ["**/*.{test,spec}.{js,ts,jsx,tsx}"],
110
+ include: ["**/*.{js,ts,jsx,tsx}"],
111
+ aliases: {},
112
+ useLevelNumber: false,
113
+ ...(context.options[0] || {}),
114
+ };
115
+
116
+ const cwd = context.getCwd();
117
+ const rootDir = createRootDir(cwd, options);
118
+
119
+ const { fileDir, filePath, isExcludedFile } = parseFileSource(
120
+ options,
121
+ context.getFilename()
122
+ );
123
+
124
+ const structure = createStructure(options, rootDir);
125
+
126
+ const createModulePath = createModulePathFromSource(
127
+ cwd,
128
+ fileDir,
129
+ createAliases(options)
130
+ );
131
+
132
+ const fileLevel = findLevel(structure)(filePath);
133
+
134
+ const reportHasProperLevelNumber = reportHasProperLayerLevelNumber(
135
+ context,
136
+ options,
137
+ rootDir,
138
+ filePath
139
+ );
140
+
141
+ const reportInSubDirOfFileDir = reportInSubDirOf(fileDir);
142
+
143
+ const reportHasProperLevel = reportHasProperLayerLevel(
144
+ context,
145
+ structure,
146
+ rootDir,
147
+ fileLevel,
148
+ filePath
149
+ );
150
+
151
+ return {
152
+ ImportDeclaration(node) {
153
+ if (isExcludedFile) return;
154
+
155
+ const modulePath = createModulePath(node.source.value);
156
+
157
+ if (reportHasProperLevelNumber(node, modulePath) === FINISHED) return;
158
+
159
+ if (reportInSubDirOfFileDir(node, modulePath) === FINISHED) return;
160
+
161
+ reportHasProperLevel(node, modulePath);
162
+ },
163
+ };
164
+ },
165
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @fileoverview Disallow calling functions in the same file.
3
+ * @author Hodoug Joung
4
+ */
5
+ "use strict";
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Rule Definition
9
+ //------------------------------------------------------------------------------
10
+
11
+ /** @type {import('eslint').Rule.RuleModule} */
12
+ module.exports = {
13
+ meta: {
14
+ type: "problem",
15
+ fixable: "code",
16
+ schema: [],
17
+ messages: {
18
+ "no-same-level-funcs": "Disallow calling {{func}} in the same file.",
19
+ },
20
+ },
21
+
22
+ create(context) {
23
+ let funcNames;
24
+ return {
25
+ Program(node) {
26
+ funcNames = node.body.reduce((names, { type, id, declarations }) => {
27
+ if (type === "FunctionDeclaration") {
28
+ names.push(id.name);
29
+ } else if (
30
+ type === "VariableDeclaration" &&
31
+ (declarations[0].init.type === "ArrowFunctionExpression" ||
32
+ declarations[0].init.type === "FunctionExpression")
33
+ ) {
34
+ names.push(declarations[0].id.name);
35
+ }
36
+ return names;
37
+ }, []);
38
+ },
39
+ CallExpression(node) {
40
+ if (funcNames.includes(node.callee.name)) {
41
+ context.report({
42
+ node,
43
+ messageId: "no-same-level-funcs",
44
+ data: { func: node.callee.name },
45
+ });
46
+ }
47
+ },
48
+ };
49
+ },
50
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "eslint-plugin-stratified-design",
3
+ "version": "0.4.0",
4
+ "description": "ESlint rules for stratified design",
5
+ "keywords": [
6
+ "eslint",
7
+ "eslintplugin",
8
+ "eslint-plugin",
9
+ "stratified",
10
+ "stratified design"
11
+ ],
12
+ "author": "Hodoug Joung",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/anisotropy/eslint-plugin-stratified-design"
16
+ },
17
+ "homepage": "https://github.com/anisotropy/eslint-plugin-stratified-design",
18
+ "main": "./lib/index.js",
19
+ "exports": "./lib/index.js",
20
+ "scripts": {
21
+ "lint": "eslint .",
22
+ "test": "mocha tests --recursive"
23
+ },
24
+ "dependencies": {
25
+ "minimatch": "^9.0.1",
26
+ "requireindex": "^1.2.0"
27
+ },
28
+ "devDependencies": {
29
+ "eslint": "^8.19.0",
30
+ "eslint-plugin-eslint-plugin": "^5.0.0",
31
+ "eslint-plugin-node": "^11.1.0",
32
+ "mocha": "^10.0.0"
33
+ },
34
+ "engines": {
35
+ "node": "^14.17.0 || ^16.0.0 || >= 18.0.0"
36
+ },
37
+ "peerDependencies": {
38
+ "eslint": ">=7"
39
+ },
40
+ "license": "ISC"
41
+ }
@@ -0,0 +1,314 @@
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/lower-level-imports"),
12
+ RuleTester = require("eslint").RuleTester;
13
+
14
+ //------------------------------------------------------------------------------
15
+ // Tests
16
+ //------------------------------------------------------------------------------
17
+
18
+ const structure = [
19
+ "layer1/subLayer1",
20
+ "layer1/subLayer2",
21
+ { name: "layer2/subLayer1", interface: true },
22
+ "layer2/subLayer2",
23
+ { name: "node-module", isNodeModule: true },
24
+ "layer3/subLayer1",
25
+ "layer3/subLayer2",
26
+ "layer3/subLayer3",
27
+ ];
28
+
29
+ const ruleTester = new RuleTester({
30
+ parserOptions: { ecmaVersion: 2022, sourceType: "module" },
31
+ });
32
+
33
+ ruleTester.run("lower-level-imports", rule, {
34
+ valid: [
35
+ {
36
+ code: "import { func } from './otherLayerB/otherSubLayer'",
37
+ filename: "./src/otherLayerA.js",
38
+ options: [{ structure, root: "./src" }],
39
+ },
40
+ {
41
+ code: "import { func } from '../layer1/subLayer2'",
42
+ filename: "./src/layer1/subLayer1.js",
43
+ options: [{ structure, root: "./src" }],
44
+ },
45
+ {
46
+ code: "import { func } from '@/layer1/subLayer2'",
47
+ filename: "./src/layer1/subLayer1.js",
48
+ options: [{ structure, root: "./src", aliases: { "@/": "./src/" } }],
49
+ },
50
+ {
51
+ code: "import { func } from '@/layer1/subLayer2/otherLayerA'",
52
+ filename: "./src/layer1/subLayer1.js",
53
+ options: [{ structure, root: "./src", aliases: { "@/": "./src/" } }],
54
+ },
55
+ {
56
+ code: "import { func } from '@/layer1/subLayer2/otherLayerB'",
57
+ filename: "./src/layer1/subLayer1/otherLayerA.js",
58
+ options: [{ structure, root: "./src", aliases: { "@/": "./src/" } }],
59
+ },
60
+ {
61
+ code: "import { func } from 'node-module'",
62
+ filename: "./src/otherLayerA.js",
63
+ options: [{ structure, root: "./src" }],
64
+ },
65
+ {
66
+ code: "import { func } from 'node-module'",
67
+ filename: "./src/layer2/subLayer1.js",
68
+ options: [{ structure, root: "./src" }],
69
+ },
70
+ {
71
+ code: "import { func } from '@/layer2/subLayer1'",
72
+ filename: "./src/layer1/subLayer2.js",
73
+ options: [{ structure, root: "./src", aliases: { "@/": "./src/" } }],
74
+ },
75
+ {
76
+ code: "import { func } from '@/layer3/subLayer1'",
77
+ filename: "./src/layer2/subLayer1.js",
78
+ options: [{ structure, root: "./src", aliases: { "@/": "./src/" } }],
79
+ },
80
+ {
81
+ code: "import { func } from './otherLayerB'",
82
+ filename: "./src/otherLayerA.test.js",
83
+ options: [
84
+ {
85
+ structure,
86
+ root: "./src",
87
+ exclude: ["**/*.{test,spec}.{js,ts,jsx,tsx}"],
88
+ },
89
+ ],
90
+ },
91
+ {
92
+ code: "import { func } from './otherLayerB'",
93
+ filename: "./src/otherLayerA.test.js",
94
+ options: [
95
+ { structure, root: "./src", include: ["**/*.{js,ts,jsx,tsx}"] },
96
+ ],
97
+ },
98
+ {
99
+ code: "import { func } from './otherLayerB'",
100
+ filename: "./src/otherLayerA.test.js",
101
+ options: [
102
+ {
103
+ structure,
104
+ root: "./src",
105
+ include: ["**/*.js"],
106
+ exclude: ["**/otherLayerA.test.js"],
107
+ },
108
+ ],
109
+ },
110
+ {
111
+ code: "import { func } from './2 otherLayerB'",
112
+ filename: "./src/1 otherLayerA.js",
113
+ options: [{ structure, root: "./src", useLevelNumber: true }],
114
+ },
115
+ {
116
+ code: "import { func } from './1 otherSubLayerB'",
117
+ filename: "./src/otherLayerA/index.js",
118
+ options: [{ structure, root: "./src", useLevelNumber: true }],
119
+ },
120
+ {
121
+ code: "import { func } from './1 otherSubLayerB'",
122
+ filename: "./src/otherLayerA/subLayer.js",
123
+ options: [{ structure, root: "./src", useLevelNumber: true }],
124
+ },
125
+ {
126
+ code: "import { func } from '@/component/99 library'",
127
+ filename: "./src/component/1 layer/CompA/index.ts",
128
+ options: [
129
+ {
130
+ structure,
131
+ root: "./src",
132
+ useLevelNumber: true,
133
+ aliases: { "@/": "./src/" },
134
+ },
135
+ ],
136
+ },
137
+ {
138
+ code: "import { func } from '@/component/99 library'",
139
+ filename: "./src/component/1 layer/CompA/ComponentA.ts",
140
+ options: [
141
+ {
142
+ structure,
143
+ root: "./src",
144
+ useLevelNumber: true,
145
+ aliases: { "@/": "./src/" },
146
+ },
147
+ ],
148
+ },
149
+ {
150
+ code: "import { func } from '@/component/99 library'",
151
+ filename: "./src/component/1 layer/CompA/1 style.tsx",
152
+ options: [
153
+ {
154
+ structure,
155
+ root: "./src",
156
+ useLevelNumber: true,
157
+ aliases: { "@/": "./src/" },
158
+ },
159
+ ],
160
+ },
161
+ {
162
+ code: "import { func } from '@/component/1 layer/2 style'",
163
+ filename: "./src/component/1 layer/1 style.tsx",
164
+ options: [
165
+ {
166
+ structure,
167
+ root: "./src",
168
+ useLevelNumber: true,
169
+ aliases: { "@/": "./src/" },
170
+ },
171
+ ],
172
+ },
173
+ ],
174
+ invalid: [
175
+ {
176
+ code: "import { func } from './otherLayerB'",
177
+ filename: "./src/otherLayerA.js",
178
+ options: [{ structure, root: "./src" }],
179
+ errors: [
180
+ {
181
+ messageId: "not-registered:file",
182
+ data: { module: "./otherLayerB", file: "./otherLayerA" },
183
+ },
184
+ ],
185
+ },
186
+ {
187
+ code: "import { func } from 'node-module'",
188
+ filename: "./src/layer3/subLayer1.js",
189
+ options: [{ structure, root: "./src" }],
190
+ errors: [
191
+ {
192
+ messageId: "not-lower-level",
193
+ data: { module: "node-module", file: "./layer3/subLayer1" },
194
+ },
195
+ ],
196
+ },
197
+ {
198
+ code: "import { func } from '@/layer2/subLayer2'",
199
+ filename: "./src/layer1/subLayer2.js",
200
+ options: [{ structure, root: "./src", aliases: { "@/": "./src/" } }],
201
+ errors: [
202
+ {
203
+ messageId: "interface",
204
+ data: { module: "./layer2/subLayer2", file: "./layer1/subLayer2" },
205
+ },
206
+ ],
207
+ },
208
+ {
209
+ code: "import { func } from '@/layer3/subLayer2/1 otherLayerA'",
210
+ filename: "./src/layer3/subLayer1.js",
211
+ options: [
212
+ {
213
+ structure,
214
+ useLevelNumber: true,
215
+ root: "./src",
216
+ aliases: { "@/": "./src/" },
217
+ },
218
+ ],
219
+ errors: [
220
+ {
221
+ messageId: "interface",
222
+ data: {
223
+ module: "./layer3/subLayer2/1 otherLayerA",
224
+ file: "./layer3/subLayer1",
225
+ },
226
+ },
227
+ ],
228
+ },
229
+ {
230
+ code: "import { func } from '@/layer3/subLayer2/1 otherLayerA'",
231
+ filename: "./src/layer3/subLayer1/1 otherLayerA.js",
232
+ options: [
233
+ {
234
+ structure,
235
+ useLevelNumber: true,
236
+ root: "./src",
237
+ aliases: { "@/": "./src/" },
238
+ },
239
+ ],
240
+ errors: [
241
+ {
242
+ messageId: "interface",
243
+ data: {
244
+ module: "./layer3/subLayer2/1 otherLayerA",
245
+ file: "./layer3/subLayer1/1 otherLayerA",
246
+ },
247
+ },
248
+ ],
249
+ },
250
+ {
251
+ code: "import { func } from '@/component/2 layer/CompA'",
252
+ filename: "./src/component/1 layer.ts",
253
+ options: [
254
+ {
255
+ structure,
256
+ root: "./src",
257
+ useLevelNumber: true,
258
+ aliases: { "@/": "./src/" },
259
+ },
260
+ ],
261
+ errors: [
262
+ {
263
+ messageId: "interface",
264
+ data: {
265
+ module: "./component/2 layer/CompA",
266
+ file: "./component/1 layer",
267
+ },
268
+ },
269
+ ],
270
+ },
271
+ {
272
+ code: "import { func } from '@/component/1 layer/1 style'",
273
+ filename: "./src/component/1 layer/2 style.ts",
274
+ options: [
275
+ {
276
+ structure,
277
+ root: "./src",
278
+ useLevelNumber: true,
279
+ aliases: { "@/": "./src/" },
280
+ },
281
+ ],
282
+ errors: [
283
+ {
284
+ messageId: "not-lower-level",
285
+ data: {
286
+ module: "./component/1 layer/1 style",
287
+ file: "./component/1 layer/2 style",
288
+ },
289
+ },
290
+ ],
291
+ },
292
+ {
293
+ code: "import { func } from '@/component/2 layer/1 style'",
294
+ filename: "./src/component/1 layer/1 style.ts",
295
+ options: [
296
+ {
297
+ structure,
298
+ root: "./src",
299
+ useLevelNumber: true,
300
+ aliases: { "@/": "./src/" },
301
+ },
302
+ ],
303
+ errors: [
304
+ {
305
+ messageId: "interface",
306
+ data: {
307
+ module: "./component/2 layer/1 style",
308
+ file: "./component/1 layer/1 style",
309
+ },
310
+ },
311
+ ],
312
+ },
313
+ ],
314
+ });