@almadar/eslint-plugin 2.1.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/LICENSE +21 -0
- package/README.md +25 -0
- package/index.js +178 -0
- package/package.json +22 -0
- package/rules/event-naming-convention.js +66 -0
- package/rules/no-as-any.js +30 -0
- package/rules/no-import-generated.js +57 -0
- package/rules/no-raw-dom-elements.js +88 -0
- package/rules/no-type-prop.js +60 -0
- package/rules/organism-extends-entity-display.js +63 -0
- package/rules/organism-no-auto-fetch.js +47 -0
- package/rules/organism-no-callback-props.js +57 -0
- package/rules/organism-no-data-state.js +50 -0
- package/rules/organism-rendering-state-only.js +47 -0
- package/rules/require-closed-circuit-props.js +70 -0
- package/rules/require-display-name.js +83 -0
- package/rules/require-event-bus.js +64 -0
- package/rules/require-stories.js +58 -0
- package/rules/require-translate.js +106 -0
- package/rules/service-event-name-screaming.js +73 -0
- package/rules/service-eventbus-singleton.js +53 -0
- package/rules/service-integration-extends-base.js +48 -0
- package/rules/service-lazy-singleton.js +86 -0
- package/rules/service-no-module-side-effects.js +81 -0
- package/rules/service-singleton-resettable.js +136 -0
- package/rules/service-store-isolation.js +70 -0
- package/rules/template-extends-base.js +55 -0
- package/rules/template-no-callbacks.js +46 -0
- package/rules/template-no-hooks.js +57 -0
- package/rules/template-no-iteration.js +58 -0
- package/rules/template-no-local-variables.js +112 -0
- package/rules/template-serializable-props.js +77 -0
- package/utils/helpers.js +133 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "problem",
|
|
6
|
+
docs: {
|
|
7
|
+
description:
|
|
8
|
+
"Integration classes must extend BaseIntegration for the .orb call-integration contract",
|
|
9
|
+
category: "Almadar Services",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
missingBase:
|
|
13
|
+
"Class '{{name}}' ends with 'Integration' but doesn't extend BaseIntegration. The .orb schema's call-integration effect expects the BaseIntegration contract (execute, validateParams, handleError, retry).",
|
|
14
|
+
},
|
|
15
|
+
schema: [],
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
create(context) {
|
|
19
|
+
const filename = context.filename;
|
|
20
|
+
|
|
21
|
+
// Only apply in integration directories
|
|
22
|
+
if (!/\/integrations\//.test(filename)) return {};
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
ClassDeclaration(node) {
|
|
26
|
+
if (!node.id || !node.id.name) return;
|
|
27
|
+
|
|
28
|
+
const name = node.id.name;
|
|
29
|
+
if (!name.endsWith("Integration")) return;
|
|
30
|
+
|
|
31
|
+
// Skip BaseIntegration itself
|
|
32
|
+
if (name === "BaseIntegration") return;
|
|
33
|
+
|
|
34
|
+
const superClass = node.superClass;
|
|
35
|
+
if (
|
|
36
|
+
!superClass ||
|
|
37
|
+
(superClass.type === "Identifier" && superClass.name !== "BaseIntegration")
|
|
38
|
+
) {
|
|
39
|
+
context.report({
|
|
40
|
+
node,
|
|
41
|
+
messageId: "missingBase",
|
|
42
|
+
data: { name },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { isServiceFile } = require("../utils/helpers");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Built-in classes that are safe to construct at module scope.
|
|
7
|
+
* These are lightweight, deterministic, and side-effect free.
|
|
8
|
+
*/
|
|
9
|
+
const SAFE_CONSTRUCTORS = new Set([
|
|
10
|
+
"Set",
|
|
11
|
+
"Map",
|
|
12
|
+
"WeakMap",
|
|
13
|
+
"WeakSet",
|
|
14
|
+
"WeakRef",
|
|
15
|
+
"Proxy",
|
|
16
|
+
"RegExp",
|
|
17
|
+
"URL",
|
|
18
|
+
"URLSearchParams",
|
|
19
|
+
"Date",
|
|
20
|
+
"Error",
|
|
21
|
+
"TypeError",
|
|
22
|
+
"RangeError",
|
|
23
|
+
"TextEncoder",
|
|
24
|
+
"TextDecoder",
|
|
25
|
+
"AbortController",
|
|
26
|
+
"Headers",
|
|
27
|
+
"FormData",
|
|
28
|
+
"Blob",
|
|
29
|
+
"Int8Array",
|
|
30
|
+
"Uint8Array",
|
|
31
|
+
"Float32Array",
|
|
32
|
+
"Float64Array",
|
|
33
|
+
"ArrayBuffer",
|
|
34
|
+
"SharedArrayBuffer",
|
|
35
|
+
"DataView",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
meta: {
|
|
40
|
+
type: "problem",
|
|
41
|
+
docs: {
|
|
42
|
+
description:
|
|
43
|
+
"Prevent eager singleton construction at module scope — use lazy get*()/reset*() pattern",
|
|
44
|
+
category: "Almadar Services",
|
|
45
|
+
},
|
|
46
|
+
messages: {
|
|
47
|
+
eagerSingleton:
|
|
48
|
+
"Don't eagerly construct '{{name}}' at module scope. Use a lazy get*()/reset*() pattern so the .orb runtime controls initialization timing.",
|
|
49
|
+
},
|
|
50
|
+
schema: [],
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
create(context) {
|
|
54
|
+
const filename = context.filename;
|
|
55
|
+
if (!isServiceFile(filename)) return {};
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
ExportNamedDeclaration(node) {
|
|
59
|
+
if (!node.declaration || node.declaration.type !== "VariableDeclaration") return;
|
|
60
|
+
|
|
61
|
+
for (const declarator of node.declaration.declarations) {
|
|
62
|
+
if (
|
|
63
|
+
declarator.init &&
|
|
64
|
+
declarator.init.type === "NewExpression"
|
|
65
|
+
) {
|
|
66
|
+
const callee = declarator.init.callee;
|
|
67
|
+
const name =
|
|
68
|
+
callee.type === "Identifier"
|
|
69
|
+
? callee.name
|
|
70
|
+
: callee.type === "MemberExpression" && callee.property.type === "Identifier"
|
|
71
|
+
? callee.property.name
|
|
72
|
+
: null;
|
|
73
|
+
|
|
74
|
+
if (name && !SAFE_CONSTRUCTORS.has(name)) {
|
|
75
|
+
context.report({
|
|
76
|
+
node: declarator,
|
|
77
|
+
messageId: "eagerSingleton",
|
|
78
|
+
data: { name },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { isServiceFile } = require("../utils/helpers");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Function calls that are safe at module scope.
|
|
7
|
+
*/
|
|
8
|
+
const SAFE_CALLS = new Set([
|
|
9
|
+
"require",
|
|
10
|
+
"Object.freeze",
|
|
11
|
+
"Object.defineProperty",
|
|
12
|
+
"Object.assign",
|
|
13
|
+
"Object.create",
|
|
14
|
+
"Symbol",
|
|
15
|
+
"Symbol.for",
|
|
16
|
+
"Array.from",
|
|
17
|
+
"Array.of",
|
|
18
|
+
"createLazyService",
|
|
19
|
+
// Express route definitions at module scope are standard
|
|
20
|
+
"registerIntegration",
|
|
21
|
+
// dotenv config loading
|
|
22
|
+
"dotenv.config",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Callee patterns that are safe at module scope (prefix match on member expressions).
|
|
27
|
+
* Express router methods (router.get, router.post, etc.) and Commander (program.parse, etc.)
|
|
28
|
+
*/
|
|
29
|
+
const SAFE_MEMBER_PREFIXES = ["router.", "program."];
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
meta: {
|
|
33
|
+
type: "problem",
|
|
34
|
+
docs: {
|
|
35
|
+
description:
|
|
36
|
+
"Prevent top-level function calls at module scope — the .orb runtime controls when services start",
|
|
37
|
+
category: "Almadar Services",
|
|
38
|
+
},
|
|
39
|
+
messages: {
|
|
40
|
+
moduleSideEffect:
|
|
41
|
+
"Top-level call to '{{name}}' runs at import time, bypassing .orb runtime control. Move into a factory function or get*() getter.",
|
|
42
|
+
},
|
|
43
|
+
schema: [],
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
create(context) {
|
|
47
|
+
const filename = context.filename;
|
|
48
|
+
if (!isServiceFile(filename)) return {};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
"Program > ExpressionStatement > CallExpression"(node) {
|
|
52
|
+
const name = getCallName(node);
|
|
53
|
+
if (name && !SAFE_CALLS.has(name) && !SAFE_MEMBER_PREFIXES.some(p => name.startsWith(p))) {
|
|
54
|
+
context.report({
|
|
55
|
+
node,
|
|
56
|
+
messageId: "moduleSideEffect",
|
|
57
|
+
data: { name },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extract the human-readable name of a call expression.
|
|
67
|
+
*/
|
|
68
|
+
function getCallName(node) {
|
|
69
|
+
const callee = node.callee;
|
|
70
|
+
if (callee.type === "Identifier") {
|
|
71
|
+
return callee.name;
|
|
72
|
+
}
|
|
73
|
+
if (
|
|
74
|
+
callee.type === "MemberExpression" &&
|
|
75
|
+
callee.object.type === "Identifier" &&
|
|
76
|
+
callee.property.type === "Identifier"
|
|
77
|
+
) {
|
|
78
|
+
return callee.object.name + "." + callee.property.name;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { isServiceFile } = require("../utils/helpers");
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description:
|
|
10
|
+
"Every exported get*() singleton getter must have a matching reset*() export for test isolation",
|
|
11
|
+
category: "Almadar Services",
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
missingReset:
|
|
15
|
+
"Exported getter '{{getter}}' has no matching '{{expectedReset}}' export. Add a reset function for test isolation.",
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
create(context) {
|
|
21
|
+
const filename = context.filename;
|
|
22
|
+
if (!isServiceFile(filename)) return {};
|
|
23
|
+
|
|
24
|
+
const exportedFunctions = new Map(); // name → node
|
|
25
|
+
const getterNodes = []; // { name, expectedReset, node }
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
// Collect exported function declarations
|
|
29
|
+
ExportNamedDeclaration(node) {
|
|
30
|
+
if (node.declaration) {
|
|
31
|
+
if (node.declaration.type === "FunctionDeclaration" && node.declaration.id) {
|
|
32
|
+
const name = node.declaration.id.name;
|
|
33
|
+
exportedFunctions.set(name, node);
|
|
34
|
+
|
|
35
|
+
// Check if it's a get* function that caches in a module-level variable (singleton pattern)
|
|
36
|
+
if (/^get[A-Z]/.test(name) && isSingletonGetter(node.declaration.body, context)) {
|
|
37
|
+
const suffix = name.slice(3); // getMemoryManager → MemoryManager
|
|
38
|
+
getterNodes.push({
|
|
39
|
+
name,
|
|
40
|
+
expectedReset: "reset" + suffix,
|
|
41
|
+
node,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// Also collect from export specifiers (export { getFoo })
|
|
49
|
+
"ExportNamedDeclaration > ExportSpecifier"(node) {
|
|
50
|
+
const name =
|
|
51
|
+
node.exported.type === "Identifier"
|
|
52
|
+
? node.exported.name
|
|
53
|
+
: node.exported.value;
|
|
54
|
+
exportedFunctions.set(name, node);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
"Program:exit"() {
|
|
58
|
+
for (const getter of getterNodes) {
|
|
59
|
+
if (!exportedFunctions.has(getter.expectedReset)) {
|
|
60
|
+
context.report({
|
|
61
|
+
node: getter.node,
|
|
62
|
+
messageId: "missingReset",
|
|
63
|
+
data: {
|
|
64
|
+
getter: getter.name,
|
|
65
|
+
expectedReset: getter.expectedReset,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Detect if a get*() function is a singleton getter — i.e., it checks a module-level
|
|
77
|
+
* variable and assigns it if null/undefined (the lazy singleton pattern).
|
|
78
|
+
*
|
|
79
|
+
* Pattern we're looking for:
|
|
80
|
+
* if (!_instance) { _instance = new Foo(); }
|
|
81
|
+
* return _instance;
|
|
82
|
+
*
|
|
83
|
+
* We detect this by checking if the function body assigns to a variable that exists
|
|
84
|
+
* at module scope (not a local variable).
|
|
85
|
+
*/
|
|
86
|
+
function isSingletonGetter(body, context) {
|
|
87
|
+
if (!body || !body.body) return false;
|
|
88
|
+
|
|
89
|
+
// Collect module-scope variable names
|
|
90
|
+
const moduleVars = new Set();
|
|
91
|
+
const program = context.sourceCode.ast;
|
|
92
|
+
for (const stmt of program.body) {
|
|
93
|
+
if (stmt.type === "VariableDeclaration") {
|
|
94
|
+
for (const decl of stmt.declarations) {
|
|
95
|
+
if (decl.id && decl.id.type === "Identifier") {
|
|
96
|
+
moduleVars.add(decl.id.name);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check if the function body assigns to a module-level variable
|
|
103
|
+
return hasAssignmentToModuleVar(body, moduleVars);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if an AST node contains an assignment to one of the given module-level variables.
|
|
108
|
+
*/
|
|
109
|
+
function hasAssignmentToModuleVar(node, moduleVars) {
|
|
110
|
+
if (!node) return false;
|
|
111
|
+
if (
|
|
112
|
+
node.type === "AssignmentExpression" &&
|
|
113
|
+
node.left &&
|
|
114
|
+
node.left.type === "Identifier" &&
|
|
115
|
+
moduleVars.has(node.left.name)
|
|
116
|
+
) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const key of Object.keys(node)) {
|
|
121
|
+
if (key === "parent") continue;
|
|
122
|
+
const child = node[key];
|
|
123
|
+
if (child && typeof child === "object") {
|
|
124
|
+
if (Array.isArray(child)) {
|
|
125
|
+
for (const item of child) {
|
|
126
|
+
if (item && typeof item.type === "string" && hasAssignmentToModuleVar(item, moduleVars)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} else if (typeof child.type === "string") {
|
|
131
|
+
if (hasAssignmentToModuleVar(child, moduleVars)) return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Database packages that should only be imported in store/db files.
|
|
5
|
+
*/
|
|
6
|
+
const DB_PACKAGES = [
|
|
7
|
+
"firebase-admin/firestore",
|
|
8
|
+
"@google-cloud/firestore",
|
|
9
|
+
"pg",
|
|
10
|
+
"mysql2",
|
|
11
|
+
"mongoose",
|
|
12
|
+
"redis",
|
|
13
|
+
"ioredis",
|
|
14
|
+
"@prisma/client",
|
|
15
|
+
"drizzle-orm",
|
|
16
|
+
"@planetscale/database",
|
|
17
|
+
"better-sqlite3",
|
|
18
|
+
"knex",
|
|
19
|
+
"typeorm",
|
|
20
|
+
"sequelize",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Paths where DB imports are allowed.
|
|
25
|
+
*/
|
|
26
|
+
function isAllowedPath(filename) {
|
|
27
|
+
return (
|
|
28
|
+
/\/stores\//.test(filename) ||
|
|
29
|
+
/\/lib\/db\./.test(filename) ||
|
|
30
|
+
/\/store\./.test(filename) ||
|
|
31
|
+
/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename) ||
|
|
32
|
+
/\/__tests__\//.test(filename) ||
|
|
33
|
+
/\/test\//.test(filename)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
meta: {
|
|
39
|
+
type: "problem",
|
|
40
|
+
docs: {
|
|
41
|
+
description:
|
|
42
|
+
"Database imports must be isolated to stores/ and lib/db.* files — use StoreContract<T> elsewhere",
|
|
43
|
+
category: "Almadar Services",
|
|
44
|
+
},
|
|
45
|
+
messages: {
|
|
46
|
+
dbOutsideStore:
|
|
47
|
+
"Import '{{source}}' is only allowed in stores/ or lib/db.* files. Use StoreContract<T> for database abstraction so databases remain swappable.",
|
|
48
|
+
},
|
|
49
|
+
schema: [],
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
create(context) {
|
|
53
|
+
const filename = context.filename;
|
|
54
|
+
|
|
55
|
+
if (isAllowedPath(filename)) return {};
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
ImportDeclaration(node) {
|
|
59
|
+
const source = node.source.value;
|
|
60
|
+
if (DB_PACKAGES.some((pkg) => source === pkg || source.startsWith(pkg + "/"))) {
|
|
61
|
+
context.report({
|
|
62
|
+
node,
|
|
63
|
+
messageId: "dbOutsideStore",
|
|
64
|
+
data: { source },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { isTemplateFile } = require("../utils/helpers");
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "suggestion",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Require template prop interfaces to extend TemplateProps<E>",
|
|
10
|
+
category: "Almadar Templates",
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
mustExtend:
|
|
14
|
+
"Template prop interface '{{name}}' should extend TemplateProps<YourEntity>. This ensures all data flows through the entity prop.",
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
create(context) {
|
|
20
|
+
const filename = context.filename;
|
|
21
|
+
if (!isTemplateFile(filename)) return {};
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
TSInterfaceDeclaration(node) {
|
|
25
|
+
// Only check interfaces ending in Props or TemplateProps
|
|
26
|
+
if (!node.id || !/Props$/.test(node.id.name)) return;
|
|
27
|
+
|
|
28
|
+
// Check if it extends TemplateProps
|
|
29
|
+
const extendsTemplateProps =
|
|
30
|
+
node.extends &&
|
|
31
|
+
node.extends.some((ext) => {
|
|
32
|
+
if (ext.expression.type === "Identifier") {
|
|
33
|
+
return ext.expression.name === "TemplateProps";
|
|
34
|
+
}
|
|
35
|
+
// MemberExpression: SomeModule.TemplateProps
|
|
36
|
+
if (
|
|
37
|
+
ext.expression.type === "MemberExpression" &&
|
|
38
|
+
ext.expression.property.type === "Identifier"
|
|
39
|
+
) {
|
|
40
|
+
return ext.expression.property.name === "TemplateProps";
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!extendsTemplateProps) {
|
|
46
|
+
context.report({
|
|
47
|
+
node: node.id,
|
|
48
|
+
messageId: "mustExtend",
|
|
49
|
+
data: { name: node.id.name },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { isTemplateFile } = require("../utils/helpers");
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Disallow function/callback props in JSX inside template files",
|
|
10
|
+
category: "Almadar Templates",
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
noCallback:
|
|
14
|
+
"Callback prop '{{prop}}' cannot survive the flattener. Use a declarative event prop instead (e.g., action, closeEvent, submitEvent).",
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
create(context) {
|
|
20
|
+
const filename = context.filename;
|
|
21
|
+
if (!isTemplateFile(filename)) return {};
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
JSXAttribute(node) {
|
|
25
|
+
if (!node.value) return;
|
|
26
|
+
|
|
27
|
+
// Check JSXExpressionContainer wrapping a function
|
|
28
|
+
if (node.value.type !== "JSXExpressionContainer") return;
|
|
29
|
+
|
|
30
|
+
const expr = node.value.expression;
|
|
31
|
+
if (
|
|
32
|
+
expr.type === "ArrowFunctionExpression" ||
|
|
33
|
+
expr.type === "FunctionExpression"
|
|
34
|
+
) {
|
|
35
|
+
const propName =
|
|
36
|
+
node.name.type === "JSXIdentifier" ? node.name.name : "unknown";
|
|
37
|
+
context.report({
|
|
38
|
+
node,
|
|
39
|
+
messageId: "noCallback",
|
|
40
|
+
data: { prop: propName },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { isTemplateFile, BANNED_TEMPLATE_HOOKS, WARNED_TEMPLATE_HOOKS } = require("../utils/helpers");
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Disallow React hooks in template files — templates must be pure functions of entity props",
|
|
10
|
+
category: "Almadar Templates",
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
bannedHook:
|
|
14
|
+
"Hook '{{hook}}' is not allowed in template files. Move stateful logic to an organism component.",
|
|
15
|
+
warnedHook:
|
|
16
|
+
"Hook '{{hook}}' in templates is discouraged. Prefer declarative event props (action, closeEvent, etc.).",
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
create(context) {
|
|
22
|
+
const filename = context.filename;
|
|
23
|
+
if (!isTemplateFile(filename)) return {};
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
CallExpression(node) {
|
|
27
|
+
const callee = node.callee;
|
|
28
|
+
let hookName = null;
|
|
29
|
+
|
|
30
|
+
if (callee.type === "Identifier") {
|
|
31
|
+
hookName = callee.name;
|
|
32
|
+
} else if (
|
|
33
|
+
callee.type === "MemberExpression" &&
|
|
34
|
+
callee.property.type === "Identifier"
|
|
35
|
+
) {
|
|
36
|
+
hookName = callee.property.name;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!hookName) return;
|
|
40
|
+
|
|
41
|
+
if (BANNED_TEMPLATE_HOOKS.has(hookName)) {
|
|
42
|
+
context.report({
|
|
43
|
+
node,
|
|
44
|
+
messageId: "bannedHook",
|
|
45
|
+
data: { hook: hookName },
|
|
46
|
+
});
|
|
47
|
+
} else if (WARNED_TEMPLATE_HOOKS.has(hookName)) {
|
|
48
|
+
context.report({
|
|
49
|
+
node,
|
|
50
|
+
messageId: "warnedHook",
|
|
51
|
+
data: { hook: hookName },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { isTemplateFile } = require("../utils/helpers");
|
|
4
|
+
|
|
5
|
+
const ITERATION_METHODS = new Set(["map", "filter", "reduce", "forEach", "flatMap"]);
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Disallow array iteration methods in template files",
|
|
12
|
+
category: "Almadar Templates",
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
noIteration:
|
|
16
|
+
"'.{{method}}()' in templates creates loop variables that can't survive the flattener. Use a list/table pattern or an organism component instead.",
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
create(context) {
|
|
22
|
+
const filename = context.filename;
|
|
23
|
+
if (!isTemplateFile(filename)) return {};
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
CallExpression(node) {
|
|
27
|
+
const callee = node.callee;
|
|
28
|
+
if (
|
|
29
|
+
callee.type === "MemberExpression" &&
|
|
30
|
+
callee.property.type === "Identifier" &&
|
|
31
|
+
ITERATION_METHODS.has(callee.property.name)
|
|
32
|
+
) {
|
|
33
|
+
context.report({
|
|
34
|
+
node,
|
|
35
|
+
messageId: "noIteration",
|
|
36
|
+
data: { method: callee.property.name },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
ForInStatement(node) {
|
|
42
|
+
context.report({
|
|
43
|
+
node,
|
|
44
|
+
messageId: "noIteration",
|
|
45
|
+
data: { method: "for...in" },
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
ForOfStatement(node) {
|
|
50
|
+
context.report({
|
|
51
|
+
node,
|
|
52
|
+
messageId: "noIteration",
|
|
53
|
+
data: { method: "for...of" },
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|