@almadar/eslint-plugin 2.1.1 → 2.3.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/index.js +47 -3
- package/package.json +1 -1
- package/rules/no-entity-string-type.js +84 -0
- package/rules/no-use-navigate.js +57 -0
- package/rules/require-closed-circuit-props.js +1 -1
- package/rules/service-barrel-complete.js +129 -0
- package/rules/service-external-declared.js +172 -0
- package/rules/service-no-inline-queries.js +117 -0
- package/rules/service-no-workspace-refs.js +76 -0
- package/rules/service-subpath-match-tsup.js +93 -0
- package/rules/service-use-eventbus.js +144 -0
package/index.js
CHANGED
|
@@ -8,7 +8,9 @@ const templateNoIteration = require("./rules/template-no-iteration");
|
|
|
8
8
|
const templateSerializableProps = require("./rules/template-serializable-props");
|
|
9
9
|
const noTypeProp = require("./rules/no-type-prop");
|
|
10
10
|
const noAsAny = require("./rules/no-as-any");
|
|
11
|
+
const noEntityStringType = require("./rules/no-entity-string-type");
|
|
11
12
|
const noImportGenerated = require("./rules/no-import-generated");
|
|
13
|
+
const noUseNavigate = require("./rules/no-use-navigate");
|
|
12
14
|
const requireEventBus = require("./rules/require-event-bus");
|
|
13
15
|
const requireDisplayName = require("./rules/require-display-name");
|
|
14
16
|
const requireClosedCircuitProps = require("./rules/require-closed-circuit-props");
|
|
@@ -22,7 +24,7 @@ const organismNoCallbackProps = require("./rules/organism-no-callback-props");
|
|
|
22
24
|
const organismExtendsEntityDisplay = require("./rules/organism-extends-entity-display");
|
|
23
25
|
const organismRenderingStateOnly = require("./rules/organism-rendering-state-only");
|
|
24
26
|
|
|
25
|
-
// Service rules
|
|
27
|
+
// Service rules - Phase 1-3 (Implemented)
|
|
26
28
|
const serviceLazySingleton = require("./rules/service-lazy-singleton");
|
|
27
29
|
const serviceSingletonResettable = require("./rules/service-singleton-resettable");
|
|
28
30
|
const serviceNoModuleSideEffects = require("./rules/service-no-module-side-effects");
|
|
@@ -31,6 +33,16 @@ const serviceEventNameScreaming = require("./rules/service-event-name-screaming"
|
|
|
31
33
|
const serviceIntegrationExtendsBase = require("./rules/service-integration-extends-base");
|
|
32
34
|
const serviceStoreIsolation = require("./rules/service-store-isolation");
|
|
33
35
|
|
|
36
|
+
// Service rules - Phase 4 (New)
|
|
37
|
+
const serviceBarrelComplete = require("./rules/service-barrel-complete");
|
|
38
|
+
const serviceSubpathMatchTsup = require("./rules/service-subpath-match-tsup");
|
|
39
|
+
const serviceNoWorkspaceRefs = require("./rules/service-no-workspace-refs");
|
|
40
|
+
const serviceExternalDeclared = require("./rules/service-external-declared");
|
|
41
|
+
|
|
42
|
+
// Service rules - Additional (New)
|
|
43
|
+
const serviceUseEventbus = require("./rules/service-use-eventbus");
|
|
44
|
+
const serviceNoInlineQueries = require("./rules/service-no-inline-queries");
|
|
45
|
+
|
|
34
46
|
const plugin = {
|
|
35
47
|
meta: {
|
|
36
48
|
name: "@almadar/eslint-plugin",
|
|
@@ -46,7 +58,9 @@ const plugin = {
|
|
|
46
58
|
"template-serializable-props": templateSerializableProps,
|
|
47
59
|
"no-type-prop": noTypeProp,
|
|
48
60
|
"no-as-any": noAsAny,
|
|
61
|
+
"no-entity-string-type": noEntityStringType,
|
|
49
62
|
"no-import-generated": noImportGenerated,
|
|
63
|
+
"no-use-navigate": noUseNavigate,
|
|
50
64
|
"require-event-bus": requireEventBus,
|
|
51
65
|
"require-display-name": requireDisplayName,
|
|
52
66
|
"require-closed-circuit-props": requireClosedCircuitProps,
|
|
@@ -60,7 +74,7 @@ const plugin = {
|
|
|
60
74
|
"organism-extends-entity-display": organismExtendsEntityDisplay,
|
|
61
75
|
"organism-rendering-state-only": organismRenderingStateOnly,
|
|
62
76
|
|
|
63
|
-
// Service rules
|
|
77
|
+
// Service rules - Phase 1-3
|
|
64
78
|
"service-lazy-singleton": serviceLazySingleton,
|
|
65
79
|
"service-singleton-resettable": serviceSingletonResettable,
|
|
66
80
|
"service-no-module-side-effects": serviceNoModuleSideEffects,
|
|
@@ -68,6 +82,16 @@ const plugin = {
|
|
|
68
82
|
"service-event-name-screaming": serviceEventNameScreaming,
|
|
69
83
|
"service-integration-extends-base": serviceIntegrationExtendsBase,
|
|
70
84
|
"service-store-isolation": serviceStoreIsolation,
|
|
85
|
+
|
|
86
|
+
// Service rules - Phase 4
|
|
87
|
+
"service-barrel-complete": serviceBarrelComplete,
|
|
88
|
+
"service-subpath-match-tsup": serviceSubpathMatchTsup,
|
|
89
|
+
"service-no-workspace-refs": serviceNoWorkspaceRefs,
|
|
90
|
+
"service-external-declared": serviceExternalDeclared,
|
|
91
|
+
|
|
92
|
+
// Service rules - Additional
|
|
93
|
+
"service-use-eventbus": serviceUseEventbus,
|
|
94
|
+
"service-no-inline-queries": serviceNoInlineQueries,
|
|
71
95
|
},
|
|
72
96
|
|
|
73
97
|
configs: {},
|
|
@@ -88,7 +112,9 @@ plugin.configs.recommended = {
|
|
|
88
112
|
"almadar/template-serializable-props": "error",
|
|
89
113
|
"almadar/no-type-prop": "error",
|
|
90
114
|
"almadar/no-as-any": "error",
|
|
115
|
+
"almadar/no-entity-string-type": "error",
|
|
91
116
|
"almadar/no-import-generated": "error",
|
|
117
|
+
"almadar/no-use-navigate": "error",
|
|
92
118
|
|
|
93
119
|
// Warn-level — conventions that should be followed
|
|
94
120
|
"almadar/require-event-bus": "warn",
|
|
@@ -151,6 +177,7 @@ plugin.configs.organisms = {
|
|
|
151
177
|
plugin.configs.services = {
|
|
152
178
|
plugins: { almadar: plugin },
|
|
153
179
|
rules: {
|
|
180
|
+
// Phase 1-3
|
|
154
181
|
"almadar/service-lazy-singleton": "error",
|
|
155
182
|
"almadar/service-singleton-resettable": "error",
|
|
156
183
|
"almadar/service-no-module-side-effects": "error",
|
|
@@ -158,13 +185,22 @@ plugin.configs.services = {
|
|
|
158
185
|
"almadar/service-event-name-screaming": "error",
|
|
159
186
|
"almadar/service-integration-extends-base": "error",
|
|
160
187
|
"almadar/service-store-isolation": "error",
|
|
188
|
+
// Phase 4
|
|
189
|
+
"almadar/service-barrel-complete": "warn",
|
|
190
|
+
"almadar/service-subpath-match-tsup": "warn",
|
|
191
|
+
"almadar/service-no-workspace-refs": "error",
|
|
192
|
+
"almadar/service-external-declared": "error",
|
|
193
|
+
// Additional
|
|
194
|
+
"almadar/service-use-eventbus": "warn",
|
|
195
|
+
"almadar/service-no-inline-queries": "error",
|
|
161
196
|
},
|
|
162
197
|
};
|
|
163
198
|
|
|
164
|
-
// Services strict config — all service rules as error
|
|
199
|
+
// Services strict config — all service rules as error
|
|
165
200
|
plugin.configs["services-strict"] = {
|
|
166
201
|
plugins: { almadar: plugin },
|
|
167
202
|
rules: {
|
|
203
|
+
// Phase 1-3
|
|
168
204
|
"almadar/service-lazy-singleton": "error",
|
|
169
205
|
"almadar/service-singleton-resettable": "error",
|
|
170
206
|
"almadar/service-no-module-side-effects": "error",
|
|
@@ -172,6 +208,14 @@ plugin.configs["services-strict"] = {
|
|
|
172
208
|
"almadar/service-event-name-screaming": "error",
|
|
173
209
|
"almadar/service-integration-extends-base": "error",
|
|
174
210
|
"almadar/service-store-isolation": "error",
|
|
211
|
+
// Phase 4
|
|
212
|
+
"almadar/service-barrel-complete": "error",
|
|
213
|
+
"almadar/service-subpath-match-tsup": "error",
|
|
214
|
+
"almadar/service-no-workspace-refs": "error",
|
|
215
|
+
"almadar/service-external-declared": "error",
|
|
216
|
+
// Additional
|
|
217
|
+
"almadar/service-use-eventbus": "error",
|
|
218
|
+
"almadar/service-no-inline-queries": "error",
|
|
175
219
|
},
|
|
176
220
|
};
|
|
177
221
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rule: no-entity-string-type
|
|
5
|
+
*
|
|
6
|
+
* Prevents typing the `entity` prop as `string` in TypeScript interfaces and type aliases.
|
|
7
|
+
* Entity props must carry actual data (objects, arrays, or domain types), never string identifiers.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description:
|
|
15
|
+
"Disallow typing the 'entity' prop as 'string' — entity carries data, not a type name",
|
|
16
|
+
category: "Almadar Architecture",
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
noEntityString:
|
|
20
|
+
"The 'entity' prop must not be typed as 'string'. Use a proper entity type (object, array, or domain interface) instead.",
|
|
21
|
+
},
|
|
22
|
+
schema: [],
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
create(context) {
|
|
26
|
+
/**
|
|
27
|
+
* Check if a type annotation contains 'string' at any level
|
|
28
|
+
* (direct TSStringKeyword, or inside a union/intersection).
|
|
29
|
+
*/
|
|
30
|
+
function typeIncludesString(typeNode) {
|
|
31
|
+
if (!typeNode) return false;
|
|
32
|
+
|
|
33
|
+
// Direct string keyword
|
|
34
|
+
if (typeNode.type === "TSStringKeyword") return true;
|
|
35
|
+
|
|
36
|
+
// Union: string | Foo
|
|
37
|
+
if (typeNode.type === "TSUnionType") {
|
|
38
|
+
return typeNode.types.some(typeIncludesString);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Intersection: string & Foo (unlikely but defensive)
|
|
42
|
+
if (typeNode.type === "TSIntersectionType") {
|
|
43
|
+
return typeNode.types.some(typeIncludesString);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check a property signature (interface member or type literal member)
|
|
51
|
+
*/
|
|
52
|
+
function checkPropertySignature(node) {
|
|
53
|
+
// Only check properties named "entity"
|
|
54
|
+
const key = node.key;
|
|
55
|
+
if (!key) return;
|
|
56
|
+
|
|
57
|
+
const propName =
|
|
58
|
+
key.type === "Identifier"
|
|
59
|
+
? key.name
|
|
60
|
+
: key.type === "Literal"
|
|
61
|
+
? String(key.value)
|
|
62
|
+
: null;
|
|
63
|
+
|
|
64
|
+
if (propName !== "entity") return;
|
|
65
|
+
|
|
66
|
+
// Check the type annotation
|
|
67
|
+
const typeAnnotation = node.typeAnnotation;
|
|
68
|
+
if (!typeAnnotation) return;
|
|
69
|
+
|
|
70
|
+
const typeNode =
|
|
71
|
+
typeAnnotation.type === "TSTypeAnnotation"
|
|
72
|
+
? typeAnnotation.typeAnnotation
|
|
73
|
+
: typeAnnotation;
|
|
74
|
+
|
|
75
|
+
if (typeIncludesString(typeNode)) {
|
|
76
|
+
context.report({ node, messageId: "noEntityString" });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
TSPropertySignature: checkPropertySignature,
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "problem",
|
|
6
|
+
docs: {
|
|
7
|
+
description:
|
|
8
|
+
"Disallow useNavigate from react-router-dom. Use useEventBus and emit UI:NAVIGATE instead.",
|
|
9
|
+
category: "Almadar Architecture",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
noUseNavigate:
|
|
13
|
+
"Do not use useNavigate() from react-router-dom. Emit 'UI:NAVIGATE' via useEventBus instead for closed circuit compliance.",
|
|
14
|
+
noReactRouterNavigate:
|
|
15
|
+
"Do not import useNavigate from react-router-dom. Use useEventBus().emit('UI:NAVIGATE', { url }) instead.",
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
create(context) {
|
|
21
|
+
return {
|
|
22
|
+
// Catch: import { useNavigate } from 'react-router-dom'
|
|
23
|
+
ImportDeclaration(node) {
|
|
24
|
+
if (
|
|
25
|
+
node.source &&
|
|
26
|
+
node.source.value === "react-router-dom" &&
|
|
27
|
+
node.specifiers
|
|
28
|
+
) {
|
|
29
|
+
for (const specifier of node.specifiers) {
|
|
30
|
+
if (
|
|
31
|
+
specifier.type === "ImportSpecifier" &&
|
|
32
|
+
specifier.imported &&
|
|
33
|
+
specifier.imported.name === "useNavigate"
|
|
34
|
+
) {
|
|
35
|
+
context.report({
|
|
36
|
+
node: specifier,
|
|
37
|
+
messageId: "noReactRouterNavigate",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
// Catch: useNavigate() call expressions
|
|
44
|
+
CallExpression(node) {
|
|
45
|
+
if (
|
|
46
|
+
node.callee.type === "Identifier" &&
|
|
47
|
+
node.callee.name === "useNavigate"
|
|
48
|
+
) {
|
|
49
|
+
context.report({
|
|
50
|
+
node,
|
|
51
|
+
messageId: "noUseNavigate",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { isOrganism } = require("../utils/helpers");
|
|
4
4
|
|
|
5
5
|
const REQUIRED_PROPS = ["className"];
|
|
6
|
-
const RECOMMENDED_PROPS = ["isLoading", "error", "entity", "
|
|
6
|
+
const RECOMMENDED_PROPS = ["isLoading", "error", "entity", "sortBy", "sortDirection", "page", "pageSize", "totalCount", "searchValue"];
|
|
7
7
|
|
|
8
8
|
module.exports = {
|
|
9
9
|
meta: {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Rule to ensure all public functions are exported from barrel files (index.ts)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Ensure all public functions are exported from barrel files (index.ts)",
|
|
15
|
+
category: "Almadar Services",
|
|
16
|
+
recommended: true,
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
missingFromBarrel: "Public export '{{name}}' from '{{source}}' is not re-exported in '{{barrel}}'.",
|
|
20
|
+
notExported: "Function '{{name}}' is declared but not exported. Add export keyword or include in index.ts.",
|
|
21
|
+
},
|
|
22
|
+
schema: [
|
|
23
|
+
{
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
barrelFile: {
|
|
27
|
+
type: "string",
|
|
28
|
+
default: "index.ts",
|
|
29
|
+
},
|
|
30
|
+
ignorePrivate: {
|
|
31
|
+
type: "boolean",
|
|
32
|
+
default: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
create(context) {
|
|
41
|
+
const options = context.options[0] || {};
|
|
42
|
+
const barrelFileName = options.barrelFile || "index.ts";
|
|
43
|
+
const ignorePrivate = options.ignorePrivate !== false;
|
|
44
|
+
|
|
45
|
+
const filename = context.filename;
|
|
46
|
+
const dir = path.dirname(filename);
|
|
47
|
+
const barrelPath = path.join(dir, barrelFileName);
|
|
48
|
+
|
|
49
|
+
// Skip if this IS the barrel file itself
|
|
50
|
+
if (path.basename(filename) === barrelFileName) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Skip if no barrel file exists
|
|
55
|
+
if (!fs.existsSync(barrelPath)) {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Read barrel file content
|
|
60
|
+
let barrelContent = "";
|
|
61
|
+
try {
|
|
62
|
+
barrelContent = fs.readFileSync(barrelPath, "utf-8");
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Track exports in this file
|
|
68
|
+
const fileExports = new Set();
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
// Export declarations: export function foo() {}
|
|
72
|
+
ExportNamedDeclaration(node) {
|
|
73
|
+
if (node.declaration) {
|
|
74
|
+
if (node.declaration.type === "FunctionDeclaration" && node.declaration.id) {
|
|
75
|
+
fileExports.add(node.declaration.id.name);
|
|
76
|
+
} else if (node.declaration.type === "VariableDeclaration") {
|
|
77
|
+
node.declaration.declarations.forEach((decl) => {
|
|
78
|
+
if (decl.id && decl.id.type === "Identifier") {
|
|
79
|
+
fileExports.add(decl.id.name);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
} else if (node.declaration.type === "ClassDeclaration" && node.declaration.id) {
|
|
83
|
+
fileExports.add(node.declaration.id.name);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Handle: export { foo, bar }
|
|
87
|
+
if (node.specifiers) {
|
|
88
|
+
node.specifiers.forEach((spec) => {
|
|
89
|
+
if (spec.exported && spec.exported.type === "Identifier") {
|
|
90
|
+
fileExports.add(spec.exported.name);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// Export default: export default foo
|
|
97
|
+
ExportDefaultDeclaration(node) {
|
|
98
|
+
if (node.declaration && node.declaration.type === "Identifier") {
|
|
99
|
+
fileExports.add(node.declaration.name);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// Check function declarations at end of file
|
|
104
|
+
"Program:exit"() {
|
|
105
|
+
fileExports.forEach((name) => {
|
|
106
|
+
// Check if this export is in the barrel file
|
|
107
|
+
// Look for: export { name } or export * from './this-file'
|
|
108
|
+
const exportPattern = new RegExp(`export\\s*\\{[^}]*\\b${name}\\b`, "i");
|
|
109
|
+
const starExportPattern = new RegExp(`export\\s*\\*\\s*from\\s*['"]\\.\\/${path.basename(filename, path.extname(filename))}['"]`, "i");
|
|
110
|
+
|
|
111
|
+
const isInBarrel = exportPattern.test(barrelContent) ||
|
|
112
|
+
starExportPattern.test(barrelContent);
|
|
113
|
+
|
|
114
|
+
if (!isInBarrel) {
|
|
115
|
+
context.report({
|
|
116
|
+
node: context.getSourceCode().ast,
|
|
117
|
+
messageId: "missingFromBarrel",
|
|
118
|
+
data: {
|
|
119
|
+
name,
|
|
120
|
+
source: path.basename(filename),
|
|
121
|
+
barrel: barrelFileName,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Rule to ensure all imports are declared in package.json dependencies
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Ensure all imports are declared in package.json dependencies",
|
|
15
|
+
category: "Almadar Services",
|
|
16
|
+
recommended: true,
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
undeclaredImport: "Import '{{package}}' is not declared in package.json dependencies. Add to dependencies, devDependencies, or peerDependencies.",
|
|
20
|
+
missingPackageJson: "Could not find package.json to validate imports.",
|
|
21
|
+
},
|
|
22
|
+
schema: [
|
|
23
|
+
{
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
allowBuiltin: {
|
|
27
|
+
type: "boolean",
|
|
28
|
+
default: true,
|
|
29
|
+
},
|
|
30
|
+
allowInternal: {
|
|
31
|
+
type: "boolean",
|
|
32
|
+
default: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
create(context) {
|
|
41
|
+
const options = context.options[0] || {};
|
|
42
|
+
const allowBuiltin = options.allowBuiltin !== false;
|
|
43
|
+
const allowInternal = options.allowInternal !== false;
|
|
44
|
+
|
|
45
|
+
// Find package.json
|
|
46
|
+
let dir = path.dirname(context.filename);
|
|
47
|
+
let pkgPath = null;
|
|
48
|
+
let pkg = null;
|
|
49
|
+
|
|
50
|
+
while (dir !== path.dirname(dir)) {
|
|
51
|
+
const candidate = path.join(dir, "package.json");
|
|
52
|
+
if (fs.existsSync(candidate)) {
|
|
53
|
+
pkgPath = candidate;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
dir = path.dirname(dir);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!pkgPath) {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
65
|
+
} catch {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Collect all declared dependencies
|
|
70
|
+
const declaredDeps = new Set();
|
|
71
|
+
["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"].forEach((field) => {
|
|
72
|
+
if (pkg[field]) {
|
|
73
|
+
Object.keys(pkg[field]).forEach((dep) => declaredDeps.add(dep));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Node.js built-in modules
|
|
78
|
+
const builtinModules = new Set([
|
|
79
|
+
"assert", "buffer", "child_process", "cluster", "console", "constants",
|
|
80
|
+
"crypto", "dgram", "dns", "domain", "events", "fs", "http", "https",
|
|
81
|
+
"module", "net", "os", "path", "punycode", "querystring", "readline",
|
|
82
|
+
"repl", "stream", "string_decoder", "sys", "timers", "tls", "tty",
|
|
83
|
+
"url", "util", "v8", "vm", "zlib", "worker_threads", "perf_hooks",
|
|
84
|
+
"async_hooks", "http2", "diagnostics_channel", "stream/web",
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
ImportDeclaration(node) {
|
|
89
|
+
const source = node.source.value;
|
|
90
|
+
|
|
91
|
+
// Skip relative imports
|
|
92
|
+
if (source.startsWith(".") || source.startsWith("/")) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Skip internal package imports (e.g., @almadar/* in monorepo)
|
|
97
|
+
if (allowInternal && source.startsWith("@almadar/")) {
|
|
98
|
+
// Only check if it's actually in the monorepo
|
|
99
|
+
// For now, assume internal packages are allowed
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Extract package name (handle @scope/pkg and scoped packages with subpaths)
|
|
104
|
+
let packageName = source;
|
|
105
|
+
if (source.startsWith("@")) {
|
|
106
|
+
// @scope/pkg or @scope/pkg/subpath
|
|
107
|
+
const parts = source.split("/");
|
|
108
|
+
packageName = `${parts[0]}/${parts[1]}`;
|
|
109
|
+
} else {
|
|
110
|
+
// pkg or pkg/subpath
|
|
111
|
+
packageName = source.split("/")[0];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Skip built-in modules
|
|
115
|
+
if (allowBuiltin && builtinModules.has(packageName)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Skip Node: prefixed imports
|
|
120
|
+
if (packageName.startsWith("node:")) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check if declared
|
|
125
|
+
if (!declaredDeps.has(packageName)) {
|
|
126
|
+
context.report({
|
|
127
|
+
node,
|
|
128
|
+
messageId: "undeclaredImport",
|
|
129
|
+
data: { package: packageName },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// Handle require() calls
|
|
135
|
+
CallExpression(node) {
|
|
136
|
+
if (
|
|
137
|
+
node.callee.type === "Identifier" &&
|
|
138
|
+
node.callee.name === "require" &&
|
|
139
|
+
node.arguments.length > 0 &&
|
|
140
|
+
node.arguments[0].type === "Literal" &&
|
|
141
|
+
typeof node.arguments[0].value === "string"
|
|
142
|
+
) {
|
|
143
|
+
const source = node.arguments[0].value;
|
|
144
|
+
|
|
145
|
+
if (source.startsWith(".") || source.startsWith("/")) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (allowInternal && source.startsWith("@almadar/")) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let packageName = source;
|
|
154
|
+
if (source.startsWith("@")) {
|
|
155
|
+
const parts = source.split("/");
|
|
156
|
+
packageName = `${parts[0]}/${parts[1]}`;
|
|
157
|
+
} else {
|
|
158
|
+
packageName = source.split("/")[0];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!declaredDeps.has(packageName)) {
|
|
162
|
+
context.report({
|
|
163
|
+
node,
|
|
164
|
+
messageId: "undeclaredImport",
|
|
165
|
+
data: { package: packageName },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Rule to prevent inline Firestore queries in route handlers
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Prevent inline Firestore queries embedded in route handlers",
|
|
12
|
+
category: "Almadar Services",
|
|
13
|
+
recommended: true,
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
inlineFirestore: "Inline Firestore query detected. Extract to DataService or use StoreContract abstraction.",
|
|
17
|
+
directDbAccess: "Direct database access in route handler. Use service layer (DataService) instead.",
|
|
18
|
+
firestoreImport: "Firestore imports should not be used directly in route files. Use service abstraction.",
|
|
19
|
+
},
|
|
20
|
+
schema: [
|
|
21
|
+
{
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {
|
|
24
|
+
routePatterns: {
|
|
25
|
+
type: "array",
|
|
26
|
+
items: { type: "string" },
|
|
27
|
+
default: ["routes/", "src/routes/", "api/"],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
create(context) {
|
|
36
|
+
const options = context.options[0] || {};
|
|
37
|
+
const routePatterns = options.routePatterns || ["routes/", "src/routes/", "api/"];
|
|
38
|
+
|
|
39
|
+
const filename = context.filename;
|
|
40
|
+
|
|
41
|
+
// Check if this is a route file
|
|
42
|
+
const isRouteFile = routePatterns.some((pattern) => filename.includes(pattern));
|
|
43
|
+
|
|
44
|
+
// Also check if it looks like a route handler (contains route handler patterns)
|
|
45
|
+
let hasRouteHandlerPattern = false;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
// Check for Firestore imports
|
|
49
|
+
ImportDeclaration(node) {
|
|
50
|
+
const source = node.source.value;
|
|
51
|
+
|
|
52
|
+
if (source.includes("firebase-admin") || source.includes("@google-cloud/firestore")) {
|
|
53
|
+
if (isRouteFile) {
|
|
54
|
+
context.report({
|
|
55
|
+
node,
|
|
56
|
+
messageId: "firestoreImport",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Check for Express route handler patterns
|
|
63
|
+
"CallExpression[callee.object.name='app'][callee.property.name=/^(get|post|put|delete|patch)$/]"(node) {
|
|
64
|
+
hasRouteHandlerPattern = true;
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
"CallExpression[callee.object.name='router'][callee.property.name=/^(get|post|put|delete|patch)$/]"(node) {
|
|
68
|
+
hasRouteHandlerPattern = true;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// Check for Firestore method calls
|
|
72
|
+
"CallExpression[callee.type='MemberExpression']"(node) {
|
|
73
|
+
const callee = node.callee;
|
|
74
|
+
|
|
75
|
+
// Check for db.collection(), doc.get(), etc.
|
|
76
|
+
if (callee.property && callee.property.type === "Identifier") {
|
|
77
|
+
const methodName = callee.property.name;
|
|
78
|
+
|
|
79
|
+
const firestoreMethods = [
|
|
80
|
+
"collection", "doc", "get", "set", "update", "delete",
|
|
81
|
+
"where", "orderBy", "limit", "startAfter", "endBefore",
|
|
82
|
+
"add", "batch", "runTransaction"
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
if (firestoreMethods.includes(methodName)) {
|
|
86
|
+
// Check if this is being called on a Firestore-like object
|
|
87
|
+
const objectName = callee.object.name ||
|
|
88
|
+
(callee.object.property && callee.object.property.name);
|
|
89
|
+
|
|
90
|
+
const firestoreObjectNames = ["db", "firestore", "admin", "firestoreDb", "firebase"];
|
|
91
|
+
|
|
92
|
+
if (objectName && firestoreObjectNames.some(name =>
|
|
93
|
+
objectName.toLowerCase().includes(name)
|
|
94
|
+
)) {
|
|
95
|
+
if (isRouteFile || hasRouteHandlerPattern) {
|
|
96
|
+
context.report({
|
|
97
|
+
node,
|
|
98
|
+
messageId: "inlineFirestore",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// Check for direct db access patterns
|
|
107
|
+
"MemberExpression[object.name='db']"(node) {
|
|
108
|
+
if (isRouteFile || hasRouteHandlerPattern) {
|
|
109
|
+
context.report({
|
|
110
|
+
node,
|
|
111
|
+
messageId: "directDbAccess",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Rule to prevent workspace:* references in published packages
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Prevent workspace:* references in published package.json dependencies",
|
|
12
|
+
category: "Almadar Services",
|
|
13
|
+
recommended: true,
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
workspaceRef: "Workspace reference '{{ref}}' found in '{{field}}'. Use explicit version for published packages.",
|
|
17
|
+
workspaceProtocol: "workspace: protocol found in '{{field}}'. This will fail when package is published.",
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
create(context) {
|
|
23
|
+
// Only check package.json files
|
|
24
|
+
if (!context.filename.endsWith("package.json")) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
Program(node) {
|
|
30
|
+
const sourceCode = context.getSourceCode();
|
|
31
|
+
const text = sourceCode.getText();
|
|
32
|
+
|
|
33
|
+
let pkg;
|
|
34
|
+
try {
|
|
35
|
+
pkg = JSON.parse(text);
|
|
36
|
+
} catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if this is a private package
|
|
41
|
+
if (pkg.private === true) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check all dependency fields
|
|
46
|
+
const depFields = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
47
|
+
|
|
48
|
+
for (const field of depFields) {
|
|
49
|
+
const deps = pkg[field];
|
|
50
|
+
if (!deps || typeof deps !== "object") {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
55
|
+
// Check for workspace:*
|
|
56
|
+
if (version === "workspace:*" || version === "workspace:") {
|
|
57
|
+
context.report({
|
|
58
|
+
node,
|
|
59
|
+
messageId: "workspaceRef",
|
|
60
|
+
data: { ref: version, field },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Check for any workspace: protocol
|
|
64
|
+
if (typeof version === "string" && version.startsWith("workspace:")) {
|
|
65
|
+
context.report({
|
|
66
|
+
node,
|
|
67
|
+
messageId: "workspaceProtocol",
|
|
68
|
+
data: { field },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Rule to ensure package.json exports match tsup build configuration
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Ensure package.json exports match tsup build entries",
|
|
15
|
+
category: "Almadar Services",
|
|
16
|
+
recommended: true,
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
exportMismatch: "package.json export '{{exportPath}}' does not match any tsup entry.",
|
|
20
|
+
missingExport: "tsup entry '{{entry}}' is not exported in package.json.",
|
|
21
|
+
invalidPattern: "Export pattern '{{pattern}}' does not match expected format (./dist/*.js).",
|
|
22
|
+
},
|
|
23
|
+
schema: [],
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
create(context) {
|
|
27
|
+
// Only check package.json files
|
|
28
|
+
if (!context.filename.endsWith("package.json")) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const dir = path.dirname(context.filename);
|
|
33
|
+
const tsupConfigPath = path.join(dir, "tsup.config.ts");
|
|
34
|
+
|
|
35
|
+
// Skip if no tsup config
|
|
36
|
+
if (!fs.existsSync(tsupConfigPath)) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
Program(node) {
|
|
42
|
+
const sourceCode = context.getSourceCode();
|
|
43
|
+
const text = sourceCode.getText();
|
|
44
|
+
|
|
45
|
+
let pkg;
|
|
46
|
+
try {
|
|
47
|
+
pkg = JSON.parse(text);
|
|
48
|
+
} catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Read tsup config (simplified - just check if entries exist)
|
|
53
|
+
let tsupContent = "";
|
|
54
|
+
try {
|
|
55
|
+
tsupContent = fs.readFileSync(tsupConfigPath, "utf-8");
|
|
56
|
+
} catch {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Extract entry points from tsup config
|
|
61
|
+
// Look for: entry: ['src/index.ts'] or entry: { main: 'src/index.ts' }
|
|
62
|
+
const entryMatch = tsupContent.match(/entry\s*:\s*(\[[^\]]+\]|\{[^}]+\})/);
|
|
63
|
+
if (!entryMatch) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const exports = pkg.exports || {};
|
|
68
|
+
|
|
69
|
+
// Check each export has a corresponding tsup entry
|
|
70
|
+
for (const [exportPath, exportConfig] of Object.entries(exports)) {
|
|
71
|
+
if (exportPath.startsWith("./")) {
|
|
72
|
+
const expectedPattern = exportPath === "."
|
|
73
|
+
? "index.js"
|
|
74
|
+
: `${exportPath.replace(/^\.\//, "/")}.js`;
|
|
75
|
+
|
|
76
|
+
// Check if there's a matching entry in tsup
|
|
77
|
+
const hasMatchingEntry = tsupContent.includes(exportPath.replace(/^\.\//, "src/")) ||
|
|
78
|
+
tsupContent.includes(exportPath.replace(/^\.\//, "")) ||
|
|
79
|
+
(exportPath === "." && tsupContent.includes("src/index"));
|
|
80
|
+
|
|
81
|
+
if (!hasMatchingEntry) {
|
|
82
|
+
context.report({
|
|
83
|
+
node,
|
|
84
|
+
messageId: "exportMismatch",
|
|
85
|
+
data: { exportPath },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Rule to enforce EventBus usage over direct function calls between modules
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "suggestion",
|
|
12
|
+
docs: {
|
|
13
|
+
description: "Enforce EventBus usage over direct function calls between modules",
|
|
14
|
+
category: "Almadar Services",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
messages: {
|
|
18
|
+
directCall: "Cross-module function call to '{{function}}' from '{{module}}'. Consider using EventBus for loose coupling.",
|
|
19
|
+
directImport: "Direct import from service module '{{module}}'. Use EventBus or service factory instead.",
|
|
20
|
+
useEventBus: "Use getGlobalEventBus() or eventBus.emit() for cross-module communication instead of direct function calls.",
|
|
21
|
+
},
|
|
22
|
+
schema: [
|
|
23
|
+
{
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
servicePaths: {
|
|
27
|
+
type: "array",
|
|
28
|
+
items: { type: "string" },
|
|
29
|
+
default: ["src/services/", "src/lib/"],
|
|
30
|
+
},
|
|
31
|
+
allowSameDirectory: {
|
|
32
|
+
type: "boolean",
|
|
33
|
+
default: true,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
create(context) {
|
|
42
|
+
const options = context.options[0] || {};
|
|
43
|
+
const servicePaths = options.servicePaths || ["src/services/", "src/lib/"];
|
|
44
|
+
const allowSameDirectory = options.allowSameDirectory !== false;
|
|
45
|
+
|
|
46
|
+
const filename = context.filename;
|
|
47
|
+
const currentDir = path.dirname(filename);
|
|
48
|
+
|
|
49
|
+
// Track imports from service directories
|
|
50
|
+
const serviceImports = new Map();
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
ImportDeclaration(node) {
|
|
54
|
+
const source = node.source.value;
|
|
55
|
+
|
|
56
|
+
// Check if importing from a service path
|
|
57
|
+
const isServiceImport = servicePaths.some(sp =>
|
|
58
|
+
source.includes(sp) || source.includes("/services/")
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (!isServiceImport) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Allow same directory imports
|
|
66
|
+
if (allowSameDirectory) {
|
|
67
|
+
const importDir = path.dirname(path.resolve(currentDir, source));
|
|
68
|
+
if (importDir === currentDir) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Track the imported bindings
|
|
74
|
+
node.specifiers.forEach((spec) => {
|
|
75
|
+
if (spec.type === "ImportSpecifier" && spec.imported) {
|
|
76
|
+
serviceImports.set(spec.local.name, {
|
|
77
|
+
source,
|
|
78
|
+
imported: spec.imported.name,
|
|
79
|
+
});
|
|
80
|
+
} else if (spec.type === "ImportDefaultSpecifier") {
|
|
81
|
+
serviceImports.set(spec.local.name, {
|
|
82
|
+
source,
|
|
83
|
+
imported: "default",
|
|
84
|
+
});
|
|
85
|
+
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
86
|
+
serviceImports.set(spec.local.name, {
|
|
87
|
+
source,
|
|
88
|
+
imported: "*",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Report direct service imports
|
|
94
|
+
if (source.includes("/services/") && !source.startsWith(".")) {
|
|
95
|
+
context.report({
|
|
96
|
+
node,
|
|
97
|
+
messageId: "directImport",
|
|
98
|
+
data: { module: source },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// Check for direct function calls to imported service functions
|
|
104
|
+
CallExpression(node) {
|
|
105
|
+
if (node.callee.type === "Identifier") {
|
|
106
|
+
const name = node.callee.name;
|
|
107
|
+
const importInfo = serviceImports.get(name);
|
|
108
|
+
|
|
109
|
+
if (importInfo && importInfo.source.includes("/services/")) {
|
|
110
|
+
context.report({
|
|
111
|
+
node,
|
|
112
|
+
messageId: "directCall",
|
|
113
|
+
data: {
|
|
114
|
+
function: name,
|
|
115
|
+
module: importInfo.source,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check for member expressions: Service.method()
|
|
122
|
+
if (
|
|
123
|
+
node.callee.type === "MemberExpression" &&
|
|
124
|
+
node.callee.object.type === "Identifier"
|
|
125
|
+
) {
|
|
126
|
+
const objectName = node.callee.object.name;
|
|
127
|
+
const importInfo = serviceImports.get(objectName);
|
|
128
|
+
|
|
129
|
+
if (importInfo && importInfo.source.includes("/services/")) {
|
|
130
|
+
const methodName = node.callee.property.name || node.callee.property.value;
|
|
131
|
+
context.report({
|
|
132
|
+
node,
|
|
133
|
+
messageId: "directCall",
|
|
134
|
+
data: {
|
|
135
|
+
function: `${objectName}.${methodName}`,
|
|
136
|
+
module: importInfo.source,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
};
|