@mionjs/devtools 0.8.6 → 0.8.8
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/build/eslint/cjs/index.cjs.map +1 -1
- package/build/eslint/cjs/src/eslint/index.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/index.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/enforce-type-imports.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/enforce-type-imports.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/formatTypeNames.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/formatTypeNames.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-mixed-union-properties.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-mixed-union-properties.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-type-imports.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-type-imports.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-typeof-runtype.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-typeof-runtype.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-unreachable-union-types.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-unreachable-union-types.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-vite-client.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/no-vite-client.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/pure-functions.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/pure-functions.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/strong-typed-routes.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/strong-typed-routes.d.ts.map +1 -0
- package/build/eslint/cjs/src/eslint/rules/type-formats-imports.d.ts +1 -0
- package/build/eslint/cjs/src/eslint/rules/type-formats-imports.d.ts.map +1 -0
- package/build/eslint/cjs/src/pureFns/purityRules.d.ts +1 -0
- package/build/eslint/cjs/src/pureFns/purityRules.d.ts.map +1 -0
- package/build/eslint/esm/index.js.map +1 -1
- package/build/eslint/esm/src/eslint/index.d.ts +1 -0
- package/build/eslint/esm/src/eslint/index.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/enforce-type-imports.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/enforce-type-imports.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/formatTypeNames.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/formatTypeNames.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/no-mixed-union-properties.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/no-mixed-union-properties.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/no-type-imports.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/no-type-imports.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/no-typeof-runtype.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/no-typeof-runtype.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/no-unreachable-union-types.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/no-unreachable-union-types.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/no-vite-client.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/no-vite-client.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/pure-functions.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/pure-functions.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/strong-typed-routes.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/strong-typed-routes.d.ts.map +1 -0
- package/build/eslint/esm/src/eslint/rules/type-formats-imports.d.ts +1 -0
- package/build/eslint/esm/src/eslint/rules/type-formats-imports.d.ts.map +1 -0
- package/build/eslint/esm/src/pureFns/purityRules.d.ts +1 -0
- package/build/eslint/esm/src/pureFns/purityRules.d.ts.map +1 -0
- package/build/vite-plugin/cjs/index.cjs +0 -1
- package/build/vite-plugin/cjs/index.cjs.map +1 -1
- package/build/vite-plugin/cjs/src/pureFns/purityRules.d.ts +1 -0
- package/build/vite-plugin/cjs/src/pureFns/purityRules.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/aotCacheGenerator.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/aotCacheGenerator.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/aotDiskCache.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/aotDiskCache.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/cjsPackageJsonPlugin.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/cjsPackageJsonPlugin.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/constants.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/constants.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/extractPureFn.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/extractPureFn.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/index.cjs +0 -1
- package/build/vite-plugin/cjs/src/vite-plugin/index.cjs.map +1 -1
- package/build/vite-plugin/cjs/src/vite-plugin/index.d.ts +2 -1
- package/build/vite-plugin/cjs/src/vite-plugin/index.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/mionVitePlugin.cjs.map +1 -1
- package/build/vite-plugin/cjs/src/vite-plugin/mionVitePlugin.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/mionVitePlugin.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/resolveModule.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/resolveModule.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/transformers.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/transformers.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/types.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/types.d.ts.map +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/virtualModule.d.ts +1 -0
- package/build/vite-plugin/cjs/src/vite-plugin/virtualModule.d.ts.map +1 -0
- package/build/vite-plugin/esm/index.js +2 -3
- package/build/vite-plugin/esm/src/pureFns/purityRules.d.ts +1 -0
- package/build/vite-plugin/esm/src/pureFns/purityRules.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/aotCacheGenerator.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/aotCacheGenerator.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/aotDiskCache.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/aotDiskCache.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/cjsPackageJsonPlugin.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/cjsPackageJsonPlugin.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/constants.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/constants.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/extractPureFn.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/extractPureFn.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/index.d.ts +2 -1
- package/build/vite-plugin/esm/src/vite-plugin/index.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/index.js +2 -3
- package/build/vite-plugin/esm/src/vite-plugin/mionVitePlugin.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/mionVitePlugin.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/mionVitePlugin.js.map +1 -1
- package/build/vite-plugin/esm/src/vite-plugin/resolveModule.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/resolveModule.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/transformers.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/transformers.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/types.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/types.d.ts.map +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/virtualModule.d.ts +1 -0
- package/build/vite-plugin/esm/src/vite-plugin/virtualModule.d.ts.map +1 -0
- package/package.json +6 -4
- package/src/eslint/index.ts +53 -0
- package/src/eslint/rules/enforce-type-imports.ts +157 -0
- package/src/eslint/rules/formatTypeNames.ts +92 -0
- package/src/eslint/rules/no-mixed-union-properties.ts +276 -0
- package/src/eslint/rules/no-type-imports.ts +386 -0
- package/src/eslint/rules/no-typeof-runtype.ts +112 -0
- package/src/eslint/rules/no-unreachable-union-types.ts +357 -0
- package/src/eslint/rules/no-vite-client.ts +101 -0
- package/src/eslint/rules/pure-functions.ts +523 -0
- package/src/eslint/rules/strong-typed-routes.ts +537 -0
- package/src/eslint/rules/type-formats-imports.ts +73 -0
- package/src/pureFns/purityRules.ts +76 -0
- package/src/vite-plugin/README.md +399 -0
- package/src/vite-plugin/aotCacheGenerator.ts +397 -0
- package/src/vite-plugin/aotDiskCache.ts +193 -0
- package/src/vite-plugin/cjsPackageJsonPlugin.ts +21 -0
- package/src/vite-plugin/constants.ts +50 -0
- package/src/vite-plugin/extractPureFn.ts +845 -0
- package/src/vite-plugin/index.ts +27 -0
- package/src/vite-plugin/mionVitePlugin.ts +738 -0
- package/src/vite-plugin/resolveModule.ts +50 -0
- package/src/vite-plugin/transformers.ts +209 -0
- package/src/vite-plugin/types.ts +182 -0
- package/src/vite-plugin/virtualModule.ts +94 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/* ########
|
|
2
|
+
* 2026 mion
|
|
3
|
+
* Author: Ma-jerez
|
|
4
|
+
* License: MIT
|
|
5
|
+
* The software is provided "as is", without warranty of any kind.
|
|
6
|
+
* ######## */
|
|
7
|
+
|
|
8
|
+
import {TSESTree, TSESLint, AST_NODE_TYPES} from '@typescript-eslint/utils';
|
|
9
|
+
import {ALLOWED_GLOBALS, FORBIDDEN_IDENTIFIERS, PURE_FN_SOURCE_PACKAGES} from '../../pureFns/purityRules.ts';
|
|
10
|
+
|
|
11
|
+
type MessageIds =
|
|
12
|
+
| 'purityThis'
|
|
13
|
+
| 'purityAwait'
|
|
14
|
+
| 'purityYield'
|
|
15
|
+
| 'purityDynamicImport'
|
|
16
|
+
| 'purityForbiddenIdentifier'
|
|
17
|
+
| 'purityClosureVariable'
|
|
18
|
+
| 'importedArgument'
|
|
19
|
+
| 'unresolvedArgument';
|
|
20
|
+
|
|
21
|
+
/** Cache of pure function imports from @mionjs/core and @mionjs/client */
|
|
22
|
+
interface PureFnImports {
|
|
23
|
+
/** Maps local name -> imported name for pureServerFn/registerPureFnFactory/mapFrom */
|
|
24
|
+
pureFnNames: Map<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Builds a cache of pure function imports from @mionjs/core and @mionjs/client */
|
|
28
|
+
function buildPureFnImportCache(program: TSESTree.Program): PureFnImports {
|
|
29
|
+
const pureFnNames = new Map<string, string>();
|
|
30
|
+
|
|
31
|
+
for (const statement of program.body) {
|
|
32
|
+
if (statement.type !== AST_NODE_TYPES.ImportDeclaration) continue;
|
|
33
|
+
const source = statement.source.value;
|
|
34
|
+
if (!PURE_FN_SOURCE_PACKAGES.includes(source as (typeof PURE_FN_SOURCE_PACKAGES)[number])) continue;
|
|
35
|
+
|
|
36
|
+
for (const specifier of statement.specifiers) {
|
|
37
|
+
if (specifier.type === AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === AST_NODE_TYPES.Identifier) {
|
|
38
|
+
const importedName = specifier.imported.name;
|
|
39
|
+
if (importedName === 'pureServerFn' || importedName === 'registerPureFnFactory' || importedName === 'mapFrom') {
|
|
40
|
+
pureFnNames.set(specifier.local.name, importedName);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {pureFnNames};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Collects all binding names from a pattern (handles destructuring) */
|
|
50
|
+
function collectBindingNames(node: TSESTree.Node, scope: Set<string>): void {
|
|
51
|
+
switch (node.type) {
|
|
52
|
+
case AST_NODE_TYPES.Identifier:
|
|
53
|
+
scope.add(node.name);
|
|
54
|
+
break;
|
|
55
|
+
case AST_NODE_TYPES.ObjectPattern:
|
|
56
|
+
for (const prop of node.properties) {
|
|
57
|
+
if (prop.type === AST_NODE_TYPES.Property) {
|
|
58
|
+
collectBindingNames(prop.value, scope);
|
|
59
|
+
} else if (prop.type === AST_NODE_TYPES.RestElement) {
|
|
60
|
+
collectBindingNames(prop.argument, scope);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
case AST_NODE_TYPES.ArrayPattern:
|
|
65
|
+
for (const element of node.elements) {
|
|
66
|
+
if (element) collectBindingNames(element, scope);
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
case AST_NODE_TYPES.RestElement:
|
|
70
|
+
collectBindingNames(node.argument, scope);
|
|
71
|
+
break;
|
|
72
|
+
case AST_NODE_TYPES.AssignmentPattern:
|
|
73
|
+
collectBindingNames(node.left, scope);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Collects all locally declared variable names from a node tree */
|
|
79
|
+
function collectLocalDeclarations(node: TSESTree.Node, scope: Set<string>): void {
|
|
80
|
+
if (node.type === AST_NODE_TYPES.VariableDeclarator) {
|
|
81
|
+
collectBindingNames(node.id, scope);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (node.type === AST_NODE_TYPES.FunctionDeclaration) {
|
|
85
|
+
if (node.id) scope.add(node.id.name);
|
|
86
|
+
for (const param of node.params) collectBindingNames(param, scope);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (node.type === AST_NODE_TYPES.FunctionExpression) {
|
|
90
|
+
if (node.id) scope.add(node.id.name);
|
|
91
|
+
for (const param of node.params) collectBindingNames(param, scope);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
95
|
+
for (const param of node.params) collectBindingNames(param, scope);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
(node.type === AST_NODE_TYPES.ForOfStatement || node.type === AST_NODE_TYPES.ForInStatement) &&
|
|
100
|
+
node.left.type === AST_NODE_TYPES.VariableDeclaration
|
|
101
|
+
) {
|
|
102
|
+
for (const decl of node.left.declarations) {
|
|
103
|
+
collectBindingNames(decl.id, scope);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (node.type === AST_NODE_TYPES.CatchClause && node.param) {
|
|
108
|
+
collectBindingNames(node.param, scope);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Recurse into children
|
|
112
|
+
for (const key of Object.keys(node)) {
|
|
113
|
+
if (key === 'parent') continue;
|
|
114
|
+
const child = (node as unknown as Record<string, unknown>)[key];
|
|
115
|
+
if (child && typeof child === 'object') {
|
|
116
|
+
if (Array.isArray(child)) {
|
|
117
|
+
for (const item of child) {
|
|
118
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
119
|
+
collectLocalDeclarations(item as TSESTree.Node, scope);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else if ('type' in child) {
|
|
123
|
+
collectLocalDeclarations(child as TSESTree.Node, scope);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Collects the full local scope for a function node */
|
|
130
|
+
function collectLocalScope(fnNode: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression): Set<string> {
|
|
131
|
+
const scope = new Set<string>();
|
|
132
|
+
|
|
133
|
+
// Add parameter names
|
|
134
|
+
for (const param of fnNode.params) {
|
|
135
|
+
collectBindingNames(param, scope);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Add function name (for recursion)
|
|
139
|
+
if (fnNode.type === AST_NODE_TYPES.FunctionExpression && fnNode.id) {
|
|
140
|
+
scope.add(fnNode.id.name);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Walk body collecting declarations
|
|
144
|
+
collectLocalDeclarations(fnNode.body, scope);
|
|
145
|
+
|
|
146
|
+
return scope;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Checks purity violations by traversing the function body AST */
|
|
150
|
+
function checkPurityViolations(
|
|
151
|
+
body: TSESTree.Node,
|
|
152
|
+
localScope: Set<string>,
|
|
153
|
+
fnTypeLabel: string,
|
|
154
|
+
context: TSESLint.RuleContext<MessageIds, []>
|
|
155
|
+
): void {
|
|
156
|
+
const forbiddenSet = FORBIDDEN_IDENTIFIERS;
|
|
157
|
+
|
|
158
|
+
function visit(node: TSESTree.Node): void {
|
|
159
|
+
if (node.type === AST_NODE_TYPES.ThisExpression) {
|
|
160
|
+
context.report({node, messageId: 'purityThis', data: {fnType: fnTypeLabel}});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (node.type === AST_NODE_TYPES.AwaitExpression) {
|
|
164
|
+
context.report({node, messageId: 'purityAwait', data: {fnType: fnTypeLabel}});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (node.type === AST_NODE_TYPES.YieldExpression) {
|
|
168
|
+
context.report({node, messageId: 'purityYield', data: {fnType: fnTypeLabel}});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (node.type === AST_NODE_TYPES.ImportExpression) {
|
|
172
|
+
context.report({node, messageId: 'purityDynamicImport', data: {fnType: fnTypeLabel}});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (node.type === AST_NODE_TYPES.Identifier) {
|
|
176
|
+
const name = node.name;
|
|
177
|
+
|
|
178
|
+
// Skip non-computed property access (obj.prop — only check 'obj')
|
|
179
|
+
if (node.parent?.type === AST_NODE_TYPES.MemberExpression && node.parent.property === node && !node.parent.computed) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Skip non-computed, non-shorthand property keys in object literals
|
|
184
|
+
if (
|
|
185
|
+
node.parent?.type === AST_NODE_TYPES.Property &&
|
|
186
|
+
node.parent.key === node &&
|
|
187
|
+
!node.parent.computed &&
|
|
188
|
+
!node.parent.shorthand
|
|
189
|
+
) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check forbidden identifiers
|
|
194
|
+
if (forbiddenSet.has(name)) {
|
|
195
|
+
context.report({
|
|
196
|
+
node,
|
|
197
|
+
messageId: 'purityForbiddenIdentifier',
|
|
198
|
+
data: {name, fnType: fnTypeLabel},
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check for closure variables (not local, not allowed global)
|
|
204
|
+
if (!localScope.has(name) && !ALLOWED_GLOBALS.has(name)) {
|
|
205
|
+
context.report({
|
|
206
|
+
node,
|
|
207
|
+
messageId: 'purityClosureVariable',
|
|
208
|
+
data: {name, fnType: fnTypeLabel},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Recurse into children (skip type-only AST subtrees)
|
|
214
|
+
for (const key of Object.keys(node)) {
|
|
215
|
+
if (
|
|
216
|
+
key === 'parent' ||
|
|
217
|
+
key === 'typeAnnotation' ||
|
|
218
|
+
key === 'returnType' ||
|
|
219
|
+
key === 'typeParameters' ||
|
|
220
|
+
key === 'typeArguments'
|
|
221
|
+
)
|
|
222
|
+
continue;
|
|
223
|
+
const child = (node as unknown as Record<string, unknown>)[key];
|
|
224
|
+
if (child && typeof child === 'object') {
|
|
225
|
+
if (Array.isArray(child)) {
|
|
226
|
+
for (const item of child) {
|
|
227
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
228
|
+
visit(item as TSESTree.Node);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} else if ('type' in child) {
|
|
232
|
+
visit(child as TSESTree.Node);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
visit(body);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Resolves a variable identifier to its initializer expression within the same module (searches all scopes) */
|
|
242
|
+
function resolveVariableInitializer(name: string, program: TSESTree.Program): TSESTree.Expression | null {
|
|
243
|
+
function search(node: TSESTree.Node): TSESTree.Expression | null {
|
|
244
|
+
if (node.type === AST_NODE_TYPES.VariableDeclarator) {
|
|
245
|
+
if (node.id.type === AST_NODE_TYPES.Identifier && node.id.name === name && node.init) {
|
|
246
|
+
return node.init;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const key of Object.keys(node)) {
|
|
250
|
+
if (key === 'parent') continue;
|
|
251
|
+
const child = (node as unknown as Record<string, unknown>)[key];
|
|
252
|
+
if (child && typeof child === 'object') {
|
|
253
|
+
if (Array.isArray(child)) {
|
|
254
|
+
for (const item of child) {
|
|
255
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
256
|
+
const result = search(item as TSESTree.Node);
|
|
257
|
+
if (result) return result;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} else if ('type' in child) {
|
|
261
|
+
const result = search(child as TSESTree.Node);
|
|
262
|
+
if (result) return result;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
return search(program);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Extracts a function or object expression from a node, resolving variable references */
|
|
272
|
+
function resolveToExpression(node: TSESTree.Node, program: TSESTree.Program): TSESTree.Expression | null {
|
|
273
|
+
if (node.type === AST_NODE_TYPES.Identifier) {
|
|
274
|
+
return resolveVariableInitializer(node.name, program);
|
|
275
|
+
}
|
|
276
|
+
return node as TSESTree.Expression;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Checks if an identifier name is an import from any module */
|
|
280
|
+
function isImportedIdentifier(name: string, program: TSESTree.Program): boolean {
|
|
281
|
+
for (const statement of program.body) {
|
|
282
|
+
if (statement.type !== AST_NODE_TYPES.ImportDeclaration) continue;
|
|
283
|
+
for (const specifier of statement.specifiers) {
|
|
284
|
+
if (specifier.local.name === name) return true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Finds the unresolved identifier in a pure function argument */
|
|
291
|
+
function findUnresolvedIdentifier(arg: TSESTree.Node, program: TSESTree.Program): {name: string; node: TSESTree.Node} | null {
|
|
292
|
+
// Direct identifier: pureServerFn(myFn) or registerPureFnFactory('ns', 'id', myFn)
|
|
293
|
+
if (arg.type === AST_NODE_TYPES.Identifier) {
|
|
294
|
+
const resolved = resolveVariableInitializer(arg.name, program);
|
|
295
|
+
if (!resolved) return {name: arg.name, node: arg};
|
|
296
|
+
|
|
297
|
+
// Resolved to something but extraction still returned null — check object form
|
|
298
|
+
if (resolved.type === AST_NODE_TYPES.ObjectExpression) {
|
|
299
|
+
return findUnresolvedPureFnInObject(resolved, program);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Resolved to something unexpected (call expression, etc.)
|
|
303
|
+
return {name: arg.name, node: arg};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Inline object: pureServerFn({ pureFn: importedFn })
|
|
307
|
+
if (arg.type === AST_NODE_TYPES.ObjectExpression) {
|
|
308
|
+
return findUnresolvedPureFnInObject(arg, program);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Finds unresolved pureFn identifier inside an object expression */
|
|
315
|
+
function findUnresolvedPureFnInObject(
|
|
316
|
+
obj: TSESTree.ObjectExpression,
|
|
317
|
+
program: TSESTree.Program
|
|
318
|
+
): {name: string; node: TSESTree.Node} | null {
|
|
319
|
+
for (const prop of obj.properties) {
|
|
320
|
+
if (prop.type !== AST_NODE_TYPES.Property) continue;
|
|
321
|
+
if (prop.key.type !== AST_NODE_TYPES.Identifier || prop.key.name !== 'pureFn') continue;
|
|
322
|
+
if (prop.value.type === AST_NODE_TYPES.Identifier) {
|
|
323
|
+
const resolved = resolveToExpression(prop.value, program);
|
|
324
|
+
if (
|
|
325
|
+
!resolved ||
|
|
326
|
+
(resolved.type !== AST_NODE_TYPES.FunctionExpression && resolved.type !== AST_NODE_TYPES.ArrowFunctionExpression)
|
|
327
|
+
) {
|
|
328
|
+
return {name: (prop.value as TSESTree.Identifier).name, node: prop.value};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Reports an error when a pure function argument cannot be resolved */
|
|
336
|
+
function reportUnresolvedArgument(
|
|
337
|
+
node: TSESTree.CallExpression,
|
|
338
|
+
callee: string,
|
|
339
|
+
program: TSESTree.Program,
|
|
340
|
+
context: TSESLint.RuleContext<MessageIds, []>
|
|
341
|
+
): void {
|
|
342
|
+
const argIndex = callee === 'registerPureFnFactory' ? 2 : callee === 'mapFrom' ? 1 : 0;
|
|
343
|
+
const arg = node.arguments[argIndex];
|
|
344
|
+
if (!arg) return;
|
|
345
|
+
|
|
346
|
+
const identifierToCheck = findUnresolvedIdentifier(arg, program);
|
|
347
|
+
if (!identifierToCheck) return;
|
|
348
|
+
|
|
349
|
+
const name = identifierToCheck.name;
|
|
350
|
+
const reportNode = identifierToCheck.node;
|
|
351
|
+
|
|
352
|
+
if (isImportedIdentifier(name, program)) {
|
|
353
|
+
context.report({node: reportNode, messageId: 'importedArgument', data: {callee, name}});
|
|
354
|
+
} else {
|
|
355
|
+
context.report({node: reportNode, messageId: 'unresolvedArgument', data: {callee, name}});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Extracts function + isFactory from an object expression (PureFnDef) */
|
|
360
|
+
function extractFromObjectExpression(
|
|
361
|
+
obj: TSESTree.ObjectExpression,
|
|
362
|
+
program: TSESTree.Program
|
|
363
|
+
): {fnNode: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression; isFactory: boolean} | null {
|
|
364
|
+
let fnNode: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression | null = null;
|
|
365
|
+
let isFactory = false;
|
|
366
|
+
|
|
367
|
+
for (const prop of obj.properties) {
|
|
368
|
+
if (prop.type !== AST_NODE_TYPES.Property) continue;
|
|
369
|
+
if (prop.key.type !== AST_NODE_TYPES.Identifier) continue;
|
|
370
|
+
|
|
371
|
+
if (prop.key.name === 'pureFn') {
|
|
372
|
+
// pureFn value could be inline or a variable reference
|
|
373
|
+
const resolved = resolveToExpression(prop.value, program);
|
|
374
|
+
if (
|
|
375
|
+
resolved &&
|
|
376
|
+
(resolved.type === AST_NODE_TYPES.FunctionExpression || resolved.type === AST_NODE_TYPES.ArrowFunctionExpression)
|
|
377
|
+
) {
|
|
378
|
+
fnNode = resolved;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (prop.key.name === 'isFactory') {
|
|
383
|
+
if (prop.value.type === AST_NODE_TYPES.Literal && prop.value.value === true) {
|
|
384
|
+
isFactory = true;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (fnNode) return {fnNode, isFactory};
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Extracts the function node and isFactory flag from a pureServerFn() call */
|
|
394
|
+
function extractPureServerFnTarget(
|
|
395
|
+
node: TSESTree.CallExpression,
|
|
396
|
+
program: TSESTree.Program
|
|
397
|
+
): {fnNode: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression; isFactory: boolean} | null {
|
|
398
|
+
const arg = node.arguments[0];
|
|
399
|
+
if (!arg) return null;
|
|
400
|
+
|
|
401
|
+
// Resolve variable reference if needed
|
|
402
|
+
const resolved = resolveToExpression(arg, program);
|
|
403
|
+
if (!resolved) return null;
|
|
404
|
+
|
|
405
|
+
// Direct function: pureServerFn(function() {}) or pureServerFn(() => {})
|
|
406
|
+
if (resolved.type === AST_NODE_TYPES.FunctionExpression || resolved.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
407
|
+
return {fnNode: resolved, isFactory: false};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Object form: pureServerFn({ pureFn: fn, isFactory: bool })
|
|
411
|
+
if (resolved.type === AST_NODE_TYPES.ObjectExpression) {
|
|
412
|
+
return extractFromObjectExpression(resolved, program);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Extracts the factory function from a registerPureFnFactory() call */
|
|
419
|
+
function extractFactoryFnTarget(
|
|
420
|
+
node: TSESTree.CallExpression,
|
|
421
|
+
program: TSESTree.Program
|
|
422
|
+
): {fnNode: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression; isFactory: true} | null {
|
|
423
|
+
// registerPureFnFactory(namespace, functionID, factoryFn)
|
|
424
|
+
const fnArg = node.arguments[2];
|
|
425
|
+
if (!fnArg) return null;
|
|
426
|
+
|
|
427
|
+
// Resolve variable reference if needed
|
|
428
|
+
const resolved = resolveToExpression(fnArg, program);
|
|
429
|
+
if (!resolved) return null;
|
|
430
|
+
|
|
431
|
+
if (resolved.type === AST_NODE_TYPES.FunctionExpression || resolved.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
432
|
+
return {fnNode: resolved, isFactory: true};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Extracts the mapper function from a mapFrom(source, mapper) call */
|
|
439
|
+
function extractMapFromMapperTarget(
|
|
440
|
+
node: TSESTree.CallExpression,
|
|
441
|
+
program: TSESTree.Program
|
|
442
|
+
): {fnNode: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression; isFactory: boolean} | null {
|
|
443
|
+
// mapFrom(source, mapper) - mapper is the 2nd argument
|
|
444
|
+
const mapperArg = node.arguments[1];
|
|
445
|
+
if (!mapperArg) return null;
|
|
446
|
+
|
|
447
|
+
const resolved = resolveToExpression(mapperArg, program);
|
|
448
|
+
if (!resolved) return null;
|
|
449
|
+
|
|
450
|
+
if (resolved.type === AST_NODE_TYPES.FunctionExpression || resolved.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
451
|
+
return {fnNode: resolved, isFactory: false};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const rule: TSESLint.RuleModule<MessageIds, []> = {
|
|
458
|
+
meta: {
|
|
459
|
+
type: 'problem',
|
|
460
|
+
docs: {
|
|
461
|
+
description:
|
|
462
|
+
'Validate that functions passed to pureServerFn(), registerPureFnFactory(), and mapFrom() are pure and do not use forbidden identifiers, closures, or side effects.',
|
|
463
|
+
},
|
|
464
|
+
messages: {
|
|
465
|
+
purityThis: "'this' is not allowed in {{fnType}}",
|
|
466
|
+
purityAwait: 'async/await is not allowed in {{fnType}}',
|
|
467
|
+
purityYield: 'generators are not allowed in {{fnType}}',
|
|
468
|
+
purityDynamicImport: 'Dynamic import() is not allowed in {{fnType}}',
|
|
469
|
+
purityForbiddenIdentifier: '"{{name}}" is not allowed in {{fnType}}',
|
|
470
|
+
purityClosureVariable:
|
|
471
|
+
'Closure variable "{{name}}" is not allowed in {{fnType}}. Pure functions cannot access outer scope variables.',
|
|
472
|
+
importedArgument:
|
|
473
|
+
'{{callee}}() argument "{{name}}" is imported from another module. Pure functions must be defined inline or as a variable in the same file.',
|
|
474
|
+
unresolvedArgument:
|
|
475
|
+
'{{callee}}() argument "{{name}}" could not be resolved to a variable declaration in this file. Pure functions must be defined inline or as a variable in the same file.',
|
|
476
|
+
},
|
|
477
|
+
schema: [],
|
|
478
|
+
},
|
|
479
|
+
defaultOptions: [],
|
|
480
|
+
create(context) {
|
|
481
|
+
let importCache: PureFnImports | null = null;
|
|
482
|
+
let programNode: TSESTree.Program | null = null;
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
Program(node) {
|
|
486
|
+
programNode = node;
|
|
487
|
+
importCache = buildPureFnImportCache(node);
|
|
488
|
+
},
|
|
489
|
+
|
|
490
|
+
CallExpression(node: TSESTree.CallExpression) {
|
|
491
|
+
if (!importCache || !programNode || importCache.pureFnNames.size === 0) return;
|
|
492
|
+
if (node.callee.type !== AST_NODE_TYPES.Identifier) return;
|
|
493
|
+
|
|
494
|
+
const importedName = importCache.pureFnNames.get(node.callee.name);
|
|
495
|
+
if (!importedName) return;
|
|
496
|
+
|
|
497
|
+
let target: {
|
|
498
|
+
fnNode: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression;
|
|
499
|
+
isFactory: boolean;
|
|
500
|
+
} | null = null;
|
|
501
|
+
|
|
502
|
+
if (importedName === 'pureServerFn') {
|
|
503
|
+
target = extractPureServerFnTarget(node, programNode);
|
|
504
|
+
} else if (importedName === 'registerPureFnFactory') {
|
|
505
|
+
target = extractFactoryFnTarget(node, programNode);
|
|
506
|
+
} else if (importedName === 'mapFrom') {
|
|
507
|
+
target = extractMapFromMapperTarget(node, programNode);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (!target) {
|
|
511
|
+
reportUnresolvedArgument(node, importedName, programNode, context);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const fnTypeLabel = target.isFactory ? 'factory functions' : 'pure functions';
|
|
516
|
+
const localScope = collectLocalScope(target.fnNode);
|
|
517
|
+
checkPurityViolations(target.fnNode.body, localScope, fnTypeLabel, context);
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
export default rule;
|