@prairielearn/eslint-plugin 4.0.0 → 4.2.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/CHANGELOG.md +21 -0
- package/dist/index.d.ts +14 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -19
- package/dist/index.js.map +1 -1
- package/dist/rules/aws-client-mandatory-config.d.ts.map +1 -1
- package/dist/rules/aws-client-mandatory-config.js +4 -6
- package/dist/rules/aws-client-mandatory-config.js.map +1 -1
- package/dist/rules/aws-client-shared-config.d.ts.map +1 -1
- package/dist/rules/aws-client-shared-config.js +4 -6
- package/dist/rules/aws-client-shared-config.js.map +1 -1
- package/dist/rules/jsx-no-dollar-interpolation.d.ts.map +1 -1
- package/dist/rules/jsx-no-dollar-interpolation.js +2 -4
- package/dist/rules/jsx-no-dollar-interpolation.js.map +1 -1
- package/dist/rules/no-current-target-in-callback.d.ts +4 -4
- package/dist/rules/no-current-target-in-callback.d.ts.map +1 -1
- package/dist/rules/no-current-target-in-callback.js +191 -38
- package/dist/rules/no-current-target-in-callback.js.map +1 -1
- package/dist/rules/no-hydrate-reslocals.d.ts +4 -0
- package/dist/rules/no-hydrate-reslocals.d.ts.map +1 -0
- package/dist/rules/no-hydrate-reslocals.js +94 -0
- package/dist/rules/no-hydrate-reslocals.js.map +1 -0
- package/dist/rules/no-unused-sql-blocks.d.ts.map +1 -1
- package/dist/rules/no-unused-sql-blocks.js +4 -39
- package/dist/rules/no-unused-sql-blocks.js.map +1 -1
- package/dist/rules/require-trpc-permission-middleware.d.ts +4 -0
- package/dist/rules/require-trpc-permission-middleware.d.ts.map +1 -0
- package/dist/rules/require-trpc-permission-middleware.js +133 -0
- package/dist/rules/require-trpc-permission-middleware.js.map +1 -0
- package/dist/rules/safe-db-types.d.ts +1 -1
- package/dist/rules/safe-db-types.d.ts.map +1 -1
- package/dist/rules/safe-db-types.js +6 -40
- package/dist/rules/safe-db-types.js.map +1 -1
- package/dist/tests/aws-client-mandatory-config.test.d.ts.map +1 -1
- package/dist/tests/aws-client-mandatory-config.test.js +8 -13
- package/dist/tests/aws-client-mandatory-config.test.js.map +1 -1
- package/dist/tests/aws-client-shared-config.test.d.ts.map +1 -1
- package/dist/tests/aws-client-shared-config.test.js +8 -13
- package/dist/tests/aws-client-shared-config.test.js.map +1 -1
- package/dist/tests/jsx-no-dollar-interpolation.test.d.ts.map +1 -1
- package/dist/tests/jsx-no-dollar-interpolation.test.js +8 -13
- package/dist/tests/jsx-no-dollar-interpolation.test.js.map +1 -1
- package/dist/tests/no-current-target-in-callback.test.d.ts.map +1 -1
- package/dist/tests/no-current-target-in-callback.test.js +75 -23
- package/dist/tests/no-current-target-in-callback.test.js.map +1 -1
- package/dist/tests/no-hydrate-reslocals.test.d.ts +2 -0
- package/dist/tests/no-hydrate-reslocals.test.d.ts.map +1 -0
- package/dist/tests/no-hydrate-reslocals.test.js +86 -0
- package/dist/tests/no-hydrate-reslocals.test.js.map +1 -0
- package/dist/tests/require-trpc-permission-middleware.test.d.ts +2 -0
- package/dist/tests/require-trpc-permission-middleware.test.d.ts.map +1 -0
- package/dist/tests/require-trpc-permission-middleware.test.js +106 -0
- package/dist/tests/require-trpc-permission-middleware.test.js.map +1 -0
- package/dist/utils.js +1 -4
- package/dist/utils.js.map +1 -1
- package/package.json +11 -10
- package/src/index.ts +7 -1
- package/src/rules/no-current-target-in-callback.ts +273 -39
- package/src/rules/no-hydrate-reslocals.ts +102 -0
- package/src/rules/require-trpc-permission-middleware.ts +168 -0
- package/src/tests/aws-client-mandatory-config.test.ts +1 -1
- package/src/tests/aws-client-shared-config.test.ts +1 -1
- package/src/tests/jsx-no-dollar-interpolation.test.ts +1 -1
- package/src/tests/no-current-target-in-callback.test.ts +67 -10
- package/src/tests/no-hydrate-reslocals.test.ts +89 -0
- package/src/tests/require-trpc-permission-middleware.test.ts +109 -0
- package/tsconfig.json +1 -7
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const utils_1 = require("@typescript-eslint/utils");
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
4
2
|
/**
|
|
5
|
-
* This rule detects when `event.currentTarget` is accessed
|
|
6
|
-
* callback
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* This rule detects when `event.currentTarget` is accessed after an `await` or
|
|
4
|
+
* inside a callback passed to a likely deferred API within a React event
|
|
5
|
+
* handler. Those callbacks may run after the handler returns, at which point
|
|
6
|
+
* `currentTarget` may already be nullified.
|
|
9
7
|
*
|
|
10
8
|
* Bad:
|
|
11
9
|
* ```tsx
|
|
@@ -17,37 +15,140 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
17
15
|
* onChange={({ currentTarget }) => setChecks((c) => ({ ...c, value: currentTarget.checked }))}
|
|
18
16
|
* ```
|
|
19
17
|
*/
|
|
20
|
-
|
|
18
|
+
export default ESLintUtils.RuleCreator.withoutDocs({
|
|
21
19
|
meta: {
|
|
22
20
|
type: 'problem',
|
|
23
21
|
messages: {
|
|
24
|
-
noCurrentTargetInCallback: 'Accessing event.currentTarget
|
|
22
|
+
noCurrentTargetInCallback: 'Accessing event.currentTarget after an async boundary may fail because currentTarget can be nullified before the access runs. Destructure currentTarget at the start of the event handler instead.',
|
|
25
23
|
},
|
|
26
24
|
schema: [],
|
|
27
25
|
},
|
|
28
26
|
defaultOptions: [],
|
|
29
27
|
create(context) {
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
const trackedEventFunctions = new Map();
|
|
29
|
+
const visitorKeys = context.sourceCode.visitorKeys;
|
|
30
|
+
function isFunctionLike(node) {
|
|
31
|
+
return (node.type === 'ArrowFunctionExpression' ||
|
|
32
|
+
node.type === 'FunctionExpression' ||
|
|
33
|
+
node.type === 'FunctionDeclaration');
|
|
34
|
+
}
|
|
35
|
+
function isNode(value) {
|
|
36
|
+
return typeof value === 'object' && value !== null && 'type' in value;
|
|
37
|
+
}
|
|
38
|
+
function getChildNodes(node) {
|
|
39
|
+
const children = [];
|
|
40
|
+
for (const key of visitorKeys[node.type] ?? []) {
|
|
41
|
+
const value = node[key];
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
for (const child of value) {
|
|
44
|
+
if (isNode(child)) {
|
|
45
|
+
children.push(child);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else if (isNode(value)) {
|
|
50
|
+
children.push(value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return children;
|
|
54
|
+
}
|
|
55
|
+
function isKnownDeferredCallee(callee) {
|
|
56
|
+
if (callee.type === 'Super') {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (callee.type === 'Identifier') {
|
|
60
|
+
return (
|
|
61
|
+
// e.g. setTimeout, setInterval, setImmediate, etc.
|
|
62
|
+
/^set[A-Z0-9_]/.test(callee.name) ||
|
|
63
|
+
callee.name === 'queueMicrotask' ||
|
|
64
|
+
callee.name === 'requestAnimationFrame' ||
|
|
65
|
+
callee.name === 'requestIdleCallback');
|
|
66
|
+
}
|
|
67
|
+
if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
|
|
68
|
+
return (callee.property.name === 'then' ||
|
|
69
|
+
callee.property.name === 'catch' ||
|
|
70
|
+
callee.property.name === 'finally');
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
function isKnownEventBoundaryCallback(callExpression, callbackIndex) {
|
|
75
|
+
if (callbackIndex !== 0) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (callExpression.callee.type === 'Identifier' &&
|
|
79
|
+
callExpression.callee.name === 'handleSubmit') {
|
|
80
|
+
// react-hook-form invokes this callback after its own submit handling,
|
|
81
|
+
// so treat it as an event boundary like other deferred callbacks.
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Check if a node is inside a nested function that is passed to a callback
|
|
88
|
+
* API that may invoke it after the event handler returns.
|
|
89
|
+
*/
|
|
90
|
+
function isInsideDeferredCallback(node, eventHandlerFunction) {
|
|
91
|
+
// Check if the event handler itself is in a deferred/boundary context
|
|
92
|
+
const handlerParent = eventHandlerFunction.parent;
|
|
93
|
+
if (handlerParent?.type === 'CallExpression') {
|
|
94
|
+
const callbackIndex = handlerParent.arguments.indexOf(eventHandlerFunction);
|
|
95
|
+
if (callbackIndex !== -1 &&
|
|
96
|
+
(isKnownDeferredCallee(handlerParent.callee) ||
|
|
97
|
+
isKnownEventBoundaryCallback(handlerParent, callbackIndex))) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
let current = node;
|
|
36
102
|
while (current && current !== eventHandlerFunction) {
|
|
37
|
-
if (current
|
|
38
|
-
|
|
39
|
-
current.type
|
|
40
|
-
|
|
103
|
+
if (isFunctionLike(current)) {
|
|
104
|
+
const parent = current.parent;
|
|
105
|
+
if (current.type !== 'FunctionDeclaration' && parent?.type === 'CallExpression') {
|
|
106
|
+
const callbackIndex = parent.arguments.indexOf(current);
|
|
107
|
+
if (callbackIndex !== -1 &&
|
|
108
|
+
(isKnownDeferredCallee(parent.callee) ||
|
|
109
|
+
isKnownEventBoundaryCallback(parent, callbackIndex))) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
41
113
|
}
|
|
42
114
|
current = current.parent;
|
|
43
115
|
}
|
|
44
|
-
return
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
/** Check if the function contains an await before this access, ignoring nested functions */
|
|
119
|
+
function hasPriorAwaitInSameFunction(node, eventHandlerFunction) {
|
|
120
|
+
if (!('body' in eventHandlerFunction) ||
|
|
121
|
+
!eventHandlerFunction.body ||
|
|
122
|
+
Array.isArray(eventHandlerFunction.body)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
let foundPriorAwait = false;
|
|
126
|
+
const visit = (current) => {
|
|
127
|
+
if (foundPriorAwait || current.range[0] >= node.range[0]) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (current !== eventHandlerFunction && isFunctionLike(current)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (current.type === 'AwaitExpression') {
|
|
134
|
+
foundPriorAwait = true;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
for (const child of getChildNodes(current)) {
|
|
138
|
+
visit(child);
|
|
139
|
+
if (foundPriorAwait) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
visit(eventHandlerFunction.body);
|
|
145
|
+
return foundPriorAwait;
|
|
45
146
|
}
|
|
46
147
|
/** Find the function that contains this node */
|
|
47
148
|
function findContainingFunction(node) {
|
|
48
149
|
let current = node.parent;
|
|
49
150
|
while (current) {
|
|
50
|
-
if (current
|
|
151
|
+
if (isFunctionLike(current)) {
|
|
51
152
|
return current;
|
|
52
153
|
}
|
|
53
154
|
current = current.parent;
|
|
@@ -79,23 +180,66 @@ exports.default = utils_1.ESLintUtils.RuleCreator.withoutDocs({
|
|
|
79
180
|
}
|
|
80
181
|
return null;
|
|
81
182
|
}
|
|
183
|
+
function isReactEventType(typeAnnotation) {
|
|
184
|
+
if (typeAnnotation.type === 'TSTypeReference') {
|
|
185
|
+
if (typeAnnotation.typeName.type === 'TSQualifiedName' &&
|
|
186
|
+
typeAnnotation.typeName.left.type === 'Identifier' &&
|
|
187
|
+
typeAnnotation.typeName.left.name === 'React' &&
|
|
188
|
+
typeAnnotation.typeName.right.type === 'Identifier' &&
|
|
189
|
+
typeAnnotation.typeName.right.name.endsWith('Event')) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
if (typeAnnotation.typeName.type === 'Identifier' &&
|
|
193
|
+
(typeAnnotation.typeName.name === 'SyntheticEvent' ||
|
|
194
|
+
typeAnnotation.typeName.name.endsWith('Event'))) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
function isTypedReactEventHandler(func) {
|
|
201
|
+
const firstParam = func.params[0];
|
|
202
|
+
if (!firstParam || firstParam.type !== 'Identifier' || !firstParam.typeAnnotation) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
return isReactEventType(firstParam.typeAnnotation.typeAnnotation);
|
|
206
|
+
}
|
|
207
|
+
function trackIfEventHandler(node) {
|
|
208
|
+
if (isJSXEventHandler(node) || isTypedReactEventHandler(node)) {
|
|
209
|
+
const eventParam = getEventParameter(node);
|
|
210
|
+
if (eventParam) {
|
|
211
|
+
trackedEventFunctions.set(node, eventParam);
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// react-hook-form: handleSubmit((_data, e) => { ... })
|
|
216
|
+
// The event is the second parameter, and the entire callback is deferred.
|
|
217
|
+
const parent = node.parent;
|
|
218
|
+
if (parent?.type === 'CallExpression' &&
|
|
219
|
+
parent.arguments[0] === node &&
|
|
220
|
+
isKnownEventBoundaryCallback(parent, 0)) {
|
|
221
|
+
const secondParam = node.params[1];
|
|
222
|
+
if (secondParam?.type === 'Identifier') {
|
|
223
|
+
trackedEventFunctions.set(node, secondParam);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
82
227
|
return {
|
|
83
228
|
// When we enter a JSX event handler, track its event parameter
|
|
84
229
|
'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression': (node) => {
|
|
85
|
-
|
|
86
|
-
const eventParam = getEventParameter(node);
|
|
87
|
-
if (eventParam) {
|
|
88
|
-
eventHandlerParams.set(node, eventParam);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
230
|
+
trackIfEventHandler(node);
|
|
91
231
|
},
|
|
92
232
|
'JSXAttribute > JSXExpressionContainer > FunctionExpression': (node) => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
233
|
+
trackIfEventHandler(node);
|
|
234
|
+
},
|
|
235
|
+
ArrowFunctionExpression(node) {
|
|
236
|
+
trackIfEventHandler(node);
|
|
237
|
+
},
|
|
238
|
+
FunctionExpression(node) {
|
|
239
|
+
trackIfEventHandler(node);
|
|
240
|
+
},
|
|
241
|
+
FunctionDeclaration(node) {
|
|
242
|
+
trackIfEventHandler(node);
|
|
99
243
|
},
|
|
100
244
|
// Check for .currentTarget access
|
|
101
245
|
MemberExpression(node) {
|
|
@@ -113,12 +257,12 @@ exports.default = utils_1.ESLintUtils.RuleCreator.withoutDocs({
|
|
|
113
257
|
if (!containingFunction)
|
|
114
258
|
return;
|
|
115
259
|
// Check each tracked event handler to see if this access is problematic
|
|
116
|
-
for (const [eventHandler, eventParam] of
|
|
260
|
+
for (const [eventHandler, eventParam] of trackedEventFunctions) {
|
|
117
261
|
// Check if this is accessing the event parameter
|
|
118
262
|
if (objectName !== eventParam.name)
|
|
119
263
|
continue;
|
|
120
|
-
|
|
121
|
-
|
|
264
|
+
if (isInsideDeferredCallback(node, eventHandler) ||
|
|
265
|
+
hasPriorAwaitInSameFunction(node, eventHandler)) {
|
|
122
266
|
context.report({
|
|
123
267
|
node,
|
|
124
268
|
messageId: 'noCurrentTargetInCallback',
|
|
@@ -129,10 +273,19 @@ exports.default = utils_1.ESLintUtils.RuleCreator.withoutDocs({
|
|
|
129
273
|
},
|
|
130
274
|
// Clean up when leaving event handlers
|
|
131
275
|
'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression:exit': (node) => {
|
|
132
|
-
|
|
276
|
+
trackedEventFunctions.delete(node);
|
|
133
277
|
},
|
|
134
278
|
'JSXAttribute > JSXExpressionContainer > FunctionExpression:exit': (node) => {
|
|
135
|
-
|
|
279
|
+
trackedEventFunctions.delete(node);
|
|
280
|
+
},
|
|
281
|
+
'ArrowFunctionExpression:exit'(node) {
|
|
282
|
+
trackedEventFunctions.delete(node);
|
|
283
|
+
},
|
|
284
|
+
'FunctionExpression:exit'(node) {
|
|
285
|
+
trackedEventFunctions.delete(node);
|
|
286
|
+
},
|
|
287
|
+
'FunctionDeclaration:exit'(node) {
|
|
288
|
+
trackedEventFunctions.delete(node);
|
|
136
289
|
},
|
|
137
290
|
};
|
|
138
291
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-current-target-in-callback.js","sourceRoot":"","sources":["../../src/rules/no-current-target-in-callback.ts"],"names":[],"mappings":"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","sourcesContent":["import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils';\n\n/**\n * This rule detects when `event.currentTarget` is accessed inside a nested\n * callback function within a React event handler. This is problematic because\n * React may execute callbacks (like those passed to setState) asynchronously,\n * at which point `currentTarget` may already be nullified.\n *\n * Bad:\n * ```tsx\n * onChange={(e) => setChecks((c) => ({ ...c, value: e.currentTarget.checked }))}\n * ```\n *\n * Good:\n * ```tsx\n * onChange={({ currentTarget }) => setChecks((c) => ({ ...c, value: currentTarget.checked }))}\n * ```\n */\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n noCurrentTargetInCallback:\n 'Accessing event.currentTarget inside a callback may fail because currentTarget can be nullified before the callback runs. Destructure currentTarget at the start of the event handler instead.',\n },\n schema: [],\n },\n defaultOptions: [],\n\n create(context) {\n // Track event handler parameters and their scopes\n const eventHandlerParams = new Map<TSESTree.Node, TSESTree.Identifier>();\n\n /** Check if a node is inside a nested function relative to the event handler */\n function isInsideNestedFunction(\n node: TSESTree.Node,\n eventHandlerFunction: TSESTree.Node,\n ): boolean {\n let current: TSESTree.Node | undefined = node.parent;\n let foundNestedFunction = false;\n\n while (current && current !== eventHandlerFunction) {\n if (\n current.type === 'ArrowFunctionExpression' ||\n current.type === 'FunctionExpression' ||\n current.type === 'FunctionDeclaration'\n ) {\n foundNestedFunction = true;\n }\n current = current.parent;\n }\n\n return foundNestedFunction && current === eventHandlerFunction;\n }\n\n /** Find the function that contains this node */\n function findContainingFunction(\n node: TSESTree.Node,\n ): TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | null {\n let current: TSESTree.Node | undefined = node.parent;\n\n while (current) {\n if (current.type === 'ArrowFunctionExpression' || current.type === 'FunctionExpression') {\n return current;\n }\n current = current.parent;\n }\n\n return null;\n }\n\n /** Check if a function is a JSX event handler */\n function isJSXEventHandler(\n func: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,\n ): boolean {\n const parent = func.parent;\n\n // Check for JSX attribute like onChange={...}\n if (parent?.type === 'JSXExpressionContainer') {\n const jsxAttribute = parent.parent;\n if (jsxAttribute?.type === 'JSXAttribute') {\n const attrName = jsxAttribute.name.type === 'JSXIdentifier' ? jsxAttribute.name.name : '';\n // Match common event handler patterns\n return /^on[A-Z]/.test(attrName);\n }\n }\n\n return false;\n }\n\n /** Get the first parameter of an event handler if it looks like an event */\n function getEventParameter(\n func: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,\n ): TSESTree.Identifier | null {\n const firstParam = func.params[0];\n\n if (!firstParam) return null;\n\n // Simple identifier parameter like (e) => ...\n if (firstParam.type === 'Identifier') {\n return firstParam;\n }\n\n return null;\n }\n\n return {\n // When we enter a JSX event handler, track its event parameter\n 'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression': (\n node: TSESTree.ArrowFunctionExpression,\n ) => {\n if (isJSXEventHandler(node)) {\n const eventParam = getEventParameter(node);\n if (eventParam) {\n eventHandlerParams.set(node, eventParam);\n }\n }\n },\n 'JSXAttribute > JSXExpressionContainer > FunctionExpression': (\n node: TSESTree.FunctionExpression,\n ) => {\n if (isJSXEventHandler(node)) {\n const eventParam = getEventParameter(node);\n if (eventParam) {\n eventHandlerParams.set(node, eventParam);\n }\n }\n },\n\n // Check for .currentTarget access\n MemberExpression(node) {\n // Only check for .currentTarget property access\n if (node.property.type !== 'Identifier' || node.property.name !== 'currentTarget') {\n return;\n }\n\n // Check if the object is an identifier (like `e` in `e.currentTarget`)\n if (node.object.type !== 'Identifier') {\n return;\n }\n\n const objectName = node.object.name;\n\n // Find the containing function\n const containingFunction = findContainingFunction(node);\n if (!containingFunction) return;\n\n // Check each tracked event handler to see if this access is problematic\n for (const [eventHandler, eventParam] of eventHandlerParams) {\n // Check if this is accessing the event parameter\n if (objectName !== eventParam.name) continue;\n\n // Check if the access is inside a nested function within the event handler\n if (isInsideNestedFunction(node, eventHandler)) {\n context.report({\n node,\n messageId: 'noCurrentTargetInCallback',\n });\n return;\n }\n }\n },\n\n // Clean up when leaving event handlers\n 'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression:exit': (\n node: TSESTree.ArrowFunctionExpression,\n ) => {\n eventHandlerParams.delete(node);\n },\n 'JSXAttribute > JSXExpressionContainer > FunctionExpression:exit': (\n node: TSESTree.FunctionExpression,\n ) => {\n eventHandlerParams.delete(node);\n },\n };\n },\n});\n"]}
|
|
1
|
+
{"version":3,"file":"no-current-target-in-callback.js","sourceRoot":"","sources":["../../src/rules/no-current-target-in-callback.ts"],"names":[],"mappings":"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","sourcesContent":["import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils';\n\n/**\n * This rule detects when `event.currentTarget` is accessed after an `await` or\n * inside a callback passed to a likely deferred API within a React event\n * handler. Those callbacks may run after the handler returns, at which point\n * `currentTarget` may already be nullified.\n *\n * Bad:\n * ```tsx\n * onChange={(e) => setChecks((c) => ({ ...c, value: e.currentTarget.checked }))}\n * ```\n *\n * Good:\n * ```tsx\n * onChange={({ currentTarget }) => setChecks((c) => ({ ...c, value: currentTarget.checked }))}\n * ```\n */\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n noCurrentTargetInCallback:\n 'Accessing event.currentTarget after an async boundary may fail because currentTarget can be nullified before the access runs. Destructure currentTarget at the start of the event handler instead.',\n },\n schema: [],\n },\n defaultOptions: [],\n\n create(context) {\n const trackedEventFunctions = new Map<TSESTree.Node, TSESTree.Identifier>();\n const visitorKeys = context.sourceCode.visitorKeys;\n\n function isFunctionLike(\n node: TSESTree.Node,\n ): node is\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration {\n return (\n node.type === 'ArrowFunctionExpression' ||\n node.type === 'FunctionExpression' ||\n node.type === 'FunctionDeclaration'\n );\n }\n\n function isNode(value: unknown): value is TSESTree.Node {\n return typeof value === 'object' && value !== null && 'type' in value;\n }\n\n function getChildNodes(node: TSESTree.Node): TSESTree.Node[] {\n const children: TSESTree.Node[] = [];\n\n for (const key of visitorKeys[node.type] ?? []) {\n const value: unknown = node[key as keyof TSESTree.Node];\n\n if (Array.isArray(value)) {\n for (const child of value) {\n if (isNode(child)) {\n children.push(child);\n }\n }\n } else if (isNode(value)) {\n children.push(value);\n }\n }\n\n return children;\n }\n\n function isKnownDeferredCallee(callee: TSESTree.Expression | TSESTree.Super): boolean {\n if (callee.type === 'Super') {\n return false;\n }\n\n if (callee.type === 'Identifier') {\n return (\n // e.g. setTimeout, setInterval, setImmediate, etc.\n /^set[A-Z0-9_]/.test(callee.name) ||\n callee.name === 'queueMicrotask' ||\n callee.name === 'requestAnimationFrame' ||\n callee.name === 'requestIdleCallback'\n );\n }\n\n if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {\n return (\n callee.property.name === 'then' ||\n callee.property.name === 'catch' ||\n callee.property.name === 'finally'\n );\n }\n\n return false;\n }\n\n function isKnownEventBoundaryCallback(\n callExpression: TSESTree.CallExpression,\n callbackIndex: number,\n ): boolean {\n if (callbackIndex !== 0) {\n return false;\n }\n\n if (\n callExpression.callee.type === 'Identifier' &&\n callExpression.callee.name === 'handleSubmit'\n ) {\n // react-hook-form invokes this callback after its own submit handling,\n // so treat it as an event boundary like other deferred callbacks.\n return true;\n }\n\n return false;\n }\n\n /**\n * Check if a node is inside a nested function that is passed to a callback\n * API that may invoke it after the event handler returns.\n */\n function isInsideDeferredCallback(\n node: TSESTree.Node,\n eventHandlerFunction: TSESTree.Node,\n ): boolean {\n // Check if the event handler itself is in a deferred/boundary context\n const handlerParent = eventHandlerFunction.parent;\n if (handlerParent?.type === 'CallExpression') {\n const callbackIndex = handlerParent.arguments.indexOf(\n eventHandlerFunction as TSESTree.CallExpressionArgument,\n );\n if (\n callbackIndex !== -1 &&\n (isKnownDeferredCallee(handlerParent.callee) ||\n isKnownEventBoundaryCallback(handlerParent, callbackIndex))\n ) {\n return true;\n }\n }\n\n let current: TSESTree.Node | undefined = node;\n\n while (current && current !== eventHandlerFunction) {\n if (isFunctionLike(current)) {\n const parent = current.parent;\n if (current.type !== 'FunctionDeclaration' && parent?.type === 'CallExpression') {\n const callbackIndex = parent.arguments.indexOf(current);\n if (\n callbackIndex !== -1 &&\n (isKnownDeferredCallee(parent.callee) ||\n isKnownEventBoundaryCallback(parent, callbackIndex))\n ) {\n return true;\n }\n }\n }\n current = current.parent;\n }\n\n return false;\n }\n\n /** Check if the function contains an await before this access, ignoring nested functions */\n function hasPriorAwaitInSameFunction(\n node: TSESTree.Node,\n eventHandlerFunction: TSESTree.Node,\n ): boolean {\n if (\n !('body' in eventHandlerFunction) ||\n !eventHandlerFunction.body ||\n Array.isArray(eventHandlerFunction.body)\n ) {\n return false;\n }\n\n let foundPriorAwait = false;\n\n const visit = (current: TSESTree.Node) => {\n if (foundPriorAwait || current.range[0] >= node.range[0]) {\n return;\n }\n\n if (current !== eventHandlerFunction && isFunctionLike(current)) {\n return;\n }\n\n if (current.type === 'AwaitExpression') {\n foundPriorAwait = true;\n return;\n }\n\n for (const child of getChildNodes(current)) {\n visit(child);\n if (foundPriorAwait) {\n return;\n }\n }\n };\n\n visit(eventHandlerFunction.body);\n return foundPriorAwait;\n }\n\n /** Find the function that contains this node */\n function findContainingFunction(\n node: TSESTree.Node,\n ):\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration\n | null {\n let current: TSESTree.Node | undefined = node.parent;\n\n while (current) {\n if (isFunctionLike(current)) {\n return current;\n }\n current = current.parent;\n }\n\n return null;\n }\n\n /** Check if a function is a JSX event handler */\n function isJSXEventHandler(\n func:\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration,\n ): boolean {\n const parent = func.parent;\n\n // Check for JSX attribute like onChange={...}\n if (parent?.type === 'JSXExpressionContainer') {\n const jsxAttribute = parent.parent;\n if (jsxAttribute?.type === 'JSXAttribute') {\n const attrName = jsxAttribute.name.type === 'JSXIdentifier' ? jsxAttribute.name.name : '';\n // Match common event handler patterns\n return /^on[A-Z]/.test(attrName);\n }\n }\n\n return false;\n }\n\n /** Get the first parameter of an event handler if it looks like an event */\n function getEventParameter(\n func:\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration,\n ): TSESTree.Identifier | null {\n const firstParam = func.params[0];\n\n if (!firstParam) return null;\n\n // Simple identifier parameter like (e) => ...\n if (firstParam.type === 'Identifier') {\n return firstParam;\n }\n\n return null;\n }\n\n function isReactEventType(typeAnnotation: TSESTree.TypeNode): boolean {\n if (typeAnnotation.type === 'TSTypeReference') {\n if (\n typeAnnotation.typeName.type === 'TSQualifiedName' &&\n typeAnnotation.typeName.left.type === 'Identifier' &&\n typeAnnotation.typeName.left.name === 'React' &&\n typeAnnotation.typeName.right.type === 'Identifier' &&\n typeAnnotation.typeName.right.name.endsWith('Event')\n ) {\n return true;\n }\n\n if (\n typeAnnotation.typeName.type === 'Identifier' &&\n (typeAnnotation.typeName.name === 'SyntheticEvent' ||\n typeAnnotation.typeName.name.endsWith('Event'))\n ) {\n return true;\n }\n }\n\n return false;\n }\n\n function isTypedReactEventHandler(\n func:\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration,\n ): boolean {\n const firstParam = func.params[0];\n if (!firstParam || firstParam.type !== 'Identifier' || !firstParam.typeAnnotation) {\n return false;\n }\n\n return isReactEventType(firstParam.typeAnnotation.typeAnnotation);\n }\n\n function trackIfEventHandler(\n node:\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration,\n ) {\n if (isJSXEventHandler(node) || isTypedReactEventHandler(node)) {\n const eventParam = getEventParameter(node);\n if (eventParam) {\n trackedEventFunctions.set(node, eventParam);\n }\n return;\n }\n\n // react-hook-form: handleSubmit((_data, e) => { ... })\n // The event is the second parameter, and the entire callback is deferred.\n const parent = node.parent;\n if (\n parent?.type === 'CallExpression' &&\n parent.arguments[0] === node &&\n isKnownEventBoundaryCallback(parent, 0)\n ) {\n const secondParam = node.params[1];\n if (secondParam?.type === 'Identifier') {\n trackedEventFunctions.set(node, secondParam);\n }\n }\n }\n\n return {\n // When we enter a JSX event handler, track its event parameter\n 'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression': (\n node: TSESTree.ArrowFunctionExpression,\n ) => {\n trackIfEventHandler(node);\n },\n 'JSXAttribute > JSXExpressionContainer > FunctionExpression': (\n node: TSESTree.FunctionExpression,\n ) => {\n trackIfEventHandler(node);\n },\n ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression) {\n trackIfEventHandler(node);\n },\n FunctionExpression(node: TSESTree.FunctionExpression) {\n trackIfEventHandler(node);\n },\n FunctionDeclaration(node: TSESTree.FunctionDeclaration) {\n trackIfEventHandler(node);\n },\n\n // Check for .currentTarget access\n MemberExpression(node) {\n // Only check for .currentTarget property access\n if (node.property.type !== 'Identifier' || node.property.name !== 'currentTarget') {\n return;\n }\n\n // Check if the object is an identifier (like `e` in `e.currentTarget`)\n if (node.object.type !== 'Identifier') {\n return;\n }\n\n const objectName = node.object.name;\n\n // Find the containing function\n const containingFunction = findContainingFunction(node);\n if (!containingFunction) return;\n\n // Check each tracked event handler to see if this access is problematic\n for (const [eventHandler, eventParam] of trackedEventFunctions) {\n // Check if this is accessing the event parameter\n if (objectName !== eventParam.name) continue;\n\n if (\n isInsideDeferredCallback(node, eventHandler) ||\n hasPriorAwaitInSameFunction(node, eventHandler)\n ) {\n context.report({\n node,\n messageId: 'noCurrentTargetInCallback',\n });\n return;\n }\n }\n },\n\n // Clean up when leaving event handlers\n 'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression:exit': (\n node: TSESTree.ArrowFunctionExpression,\n ) => {\n trackedEventFunctions.delete(node);\n },\n 'JSXAttribute > JSXExpressionContainer > FunctionExpression:exit': (\n node: TSESTree.FunctionExpression,\n ) => {\n trackedEventFunctions.delete(node);\n },\n 'ArrowFunctionExpression:exit'(node: TSESTree.ArrowFunctionExpression) {\n trackedEventFunctions.delete(node);\n },\n 'FunctionExpression:exit'(node: TSESTree.FunctionExpression) {\n trackedEventFunctions.delete(node);\n },\n 'FunctionDeclaration:exit'(node: TSESTree.FunctionDeclaration) {\n trackedEventFunctions.delete(node);\n },\n };\n },\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-hydrate-reslocals.d.ts","sourceRoot":"","sources":["../../src/rules/no-hydrate-reslocals.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC","sourcesContent":["import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils';\n\n/**\n * All props on a hydrated component are serialized with superjson and shipped\n * to the browser. Passing `resLocals` or `locals` (or spreading them) would\n * leak the entire server-side `res.locals` object — CSRF tokens, auth data,\n * authz objects, full DB rows, etc.\n *\n * This rule forbids those prop names (and `{...res.locals}` / `{...resLocals}`\n * / `{...locals}` spreads) on the child of `<Hydrate>` and on the JSX argument\n * to `hydrateHtml(...)`. Extract the specific fields you need (e.g. via\n * `extractPageContext`) and pass them as individual props instead.\n */\n\nconst HYDRATE_COMPONENT_NAME = 'Hydrate';\nconst HYDRATE_FUNCTION_NAME = 'hydrateHtml';\nconst FORBIDDEN_NAMES = new Set(['resLocals', 'locals']);\n\nfunction extractSingleJsxChild(children: TSESTree.JSXChild[]): TSESTree.JSXElement | null {\n const nonWhitespace = children.filter((child) => {\n if (child.type === 'JSXText') return child.value.trim().length > 0;\n return true;\n });\n if (nonWhitespace.length !== 1 || nonWhitespace[0].type !== 'JSXElement') {\n return null;\n }\n return nonWhitespace[0];\n}\n\nfunction spreadArgumentDescribesResLocals(argument: TSESTree.Expression): boolean {\n if (argument.type === 'Identifier') {\n return FORBIDDEN_NAMES.has(argument.name);\n }\n if (\n argument.type === 'MemberExpression' &&\n !argument.computed &&\n argument.property.type === 'Identifier' &&\n argument.property.name === 'locals'\n ) {\n return true;\n }\n return false;\n}\n\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n forbiddenProp:\n 'Do not pass \"{{name}}\" to a hydrated component; props are serialized and sent to the client, so this would leak the full server-side res.locals. Pass the specific fields the component needs instead.',\n forbiddenSpread:\n 'Do not spread res.locals onto a hydrated component; props are serialized and sent to the client. Pass the specific fields the component needs instead.',\n },\n schema: [],\n },\n defaultOptions: [],\n\n create(context) {\n function checkHydratedElement(element: TSESTree.JSXElement) {\n for (const attribute of element.openingElement.attributes) {\n if (attribute.type === 'JSXAttribute') {\n if (attribute.name.type === 'JSXIdentifier' && FORBIDDEN_NAMES.has(attribute.name.name)) {\n context.report({\n node: attribute,\n messageId: 'forbiddenProp',\n data: { name: attribute.name.name },\n });\n }\n } else if (attribute.type === 'JSXSpreadAttribute') {\n if (spreadArgumentDescribesResLocals(attribute.argument)) {\n context.report({\n node: attribute,\n messageId: 'forbiddenSpread',\n });\n }\n }\n }\n }\n\n return {\n JSXElement(node) {\n const opening = node.openingElement.name;\n if (opening.type !== 'JSXIdentifier' || opening.name !== HYDRATE_COMPONENT_NAME) return;\n\n const child = extractSingleJsxChild(node.children);\n if (!child) return;\n\n checkHydratedElement(child);\n },\n\n CallExpression(node) {\n if (node.callee.type !== 'Identifier' || node.callee.name !== HYDRATE_FUNCTION_NAME) return;\n if (node.arguments.length === 0) return;\n\n const arg = node.arguments[0];\n if (arg.type !== 'JSXElement') return;\n\n checkHydratedElement(arg);\n },\n };\n },\n});\n"]}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* All props on a hydrated component are serialized with superjson and shipped
|
|
4
|
+
* to the browser. Passing `resLocals` or `locals` (or spreading them) would
|
|
5
|
+
* leak the entire server-side `res.locals` object — CSRF tokens, auth data,
|
|
6
|
+
* authz objects, full DB rows, etc.
|
|
7
|
+
*
|
|
8
|
+
* This rule forbids those prop names (and `{...res.locals}` / `{...resLocals}`
|
|
9
|
+
* / `{...locals}` spreads) on the child of `<Hydrate>` and on the JSX argument
|
|
10
|
+
* to `hydrateHtml(...)`. Extract the specific fields you need (e.g. via
|
|
11
|
+
* `extractPageContext`) and pass them as individual props instead.
|
|
12
|
+
*/
|
|
13
|
+
const HYDRATE_COMPONENT_NAME = 'Hydrate';
|
|
14
|
+
const HYDRATE_FUNCTION_NAME = 'hydrateHtml';
|
|
15
|
+
const FORBIDDEN_NAMES = new Set(['resLocals', 'locals']);
|
|
16
|
+
function extractSingleJsxChild(children) {
|
|
17
|
+
const nonWhitespace = children.filter((child) => {
|
|
18
|
+
if (child.type === 'JSXText')
|
|
19
|
+
return child.value.trim().length > 0;
|
|
20
|
+
return true;
|
|
21
|
+
});
|
|
22
|
+
if (nonWhitespace.length !== 1 || nonWhitespace[0].type !== 'JSXElement') {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return nonWhitespace[0];
|
|
26
|
+
}
|
|
27
|
+
function spreadArgumentDescribesResLocals(argument) {
|
|
28
|
+
if (argument.type === 'Identifier') {
|
|
29
|
+
return FORBIDDEN_NAMES.has(argument.name);
|
|
30
|
+
}
|
|
31
|
+
if (argument.type === 'MemberExpression' &&
|
|
32
|
+
!argument.computed &&
|
|
33
|
+
argument.property.type === 'Identifier' &&
|
|
34
|
+
argument.property.name === 'locals') {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
export default ESLintUtils.RuleCreator.withoutDocs({
|
|
40
|
+
meta: {
|
|
41
|
+
type: 'problem',
|
|
42
|
+
messages: {
|
|
43
|
+
forbiddenProp: 'Do not pass "{{name}}" to a hydrated component; props are serialized and sent to the client, so this would leak the full server-side res.locals. Pass the specific fields the component needs instead.',
|
|
44
|
+
forbiddenSpread: 'Do not spread res.locals onto a hydrated component; props are serialized and sent to the client. Pass the specific fields the component needs instead.',
|
|
45
|
+
},
|
|
46
|
+
schema: [],
|
|
47
|
+
},
|
|
48
|
+
defaultOptions: [],
|
|
49
|
+
create(context) {
|
|
50
|
+
function checkHydratedElement(element) {
|
|
51
|
+
for (const attribute of element.openingElement.attributes) {
|
|
52
|
+
if (attribute.type === 'JSXAttribute') {
|
|
53
|
+
if (attribute.name.type === 'JSXIdentifier' && FORBIDDEN_NAMES.has(attribute.name.name)) {
|
|
54
|
+
context.report({
|
|
55
|
+
node: attribute,
|
|
56
|
+
messageId: 'forbiddenProp',
|
|
57
|
+
data: { name: attribute.name.name },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if (attribute.type === 'JSXSpreadAttribute') {
|
|
62
|
+
if (spreadArgumentDescribesResLocals(attribute.argument)) {
|
|
63
|
+
context.report({
|
|
64
|
+
node: attribute,
|
|
65
|
+
messageId: 'forbiddenSpread',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
JSXElement(node) {
|
|
73
|
+
const opening = node.openingElement.name;
|
|
74
|
+
if (opening.type !== 'JSXIdentifier' || opening.name !== HYDRATE_COMPONENT_NAME)
|
|
75
|
+
return;
|
|
76
|
+
const child = extractSingleJsxChild(node.children);
|
|
77
|
+
if (!child)
|
|
78
|
+
return;
|
|
79
|
+
checkHydratedElement(child);
|
|
80
|
+
},
|
|
81
|
+
CallExpression(node) {
|
|
82
|
+
if (node.callee.type !== 'Identifier' || node.callee.name !== HYDRATE_FUNCTION_NAME)
|
|
83
|
+
return;
|
|
84
|
+
if (node.arguments.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
const arg = node.arguments[0];
|
|
87
|
+
if (arg.type !== 'JSXElement')
|
|
88
|
+
return;
|
|
89
|
+
checkHydratedElement(arg);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
//# sourceMappingURL=no-hydrate-reslocals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-hydrate-reslocals.js","sourceRoot":"","sources":["../../src/rules/no-hydrate-reslocals.ts"],"names":[],"mappings":"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","sourcesContent":["import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils';\n\n/**\n * All props on a hydrated component are serialized with superjson and shipped\n * to the browser. Passing `resLocals` or `locals` (or spreading them) would\n * leak the entire server-side `res.locals` object — CSRF tokens, auth data,\n * authz objects, full DB rows, etc.\n *\n * This rule forbids those prop names (and `{...res.locals}` / `{...resLocals}`\n * / `{...locals}` spreads) on the child of `<Hydrate>` and on the JSX argument\n * to `hydrateHtml(...)`. Extract the specific fields you need (e.g. via\n * `extractPageContext`) and pass them as individual props instead.\n */\n\nconst HYDRATE_COMPONENT_NAME = 'Hydrate';\nconst HYDRATE_FUNCTION_NAME = 'hydrateHtml';\nconst FORBIDDEN_NAMES = new Set(['resLocals', 'locals']);\n\nfunction extractSingleJsxChild(children: TSESTree.JSXChild[]): TSESTree.JSXElement | null {\n const nonWhitespace = children.filter((child) => {\n if (child.type === 'JSXText') return child.value.trim().length > 0;\n return true;\n });\n if (nonWhitespace.length !== 1 || nonWhitespace[0].type !== 'JSXElement') {\n return null;\n }\n return nonWhitespace[0];\n}\n\nfunction spreadArgumentDescribesResLocals(argument: TSESTree.Expression): boolean {\n if (argument.type === 'Identifier') {\n return FORBIDDEN_NAMES.has(argument.name);\n }\n if (\n argument.type === 'MemberExpression' &&\n !argument.computed &&\n argument.property.type === 'Identifier' &&\n argument.property.name === 'locals'\n ) {\n return true;\n }\n return false;\n}\n\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n forbiddenProp:\n 'Do not pass \"{{name}}\" to a hydrated component; props are serialized and sent to the client, so this would leak the full server-side res.locals. Pass the specific fields the component needs instead.',\n forbiddenSpread:\n 'Do not spread res.locals onto a hydrated component; props are serialized and sent to the client. Pass the specific fields the component needs instead.',\n },\n schema: [],\n },\n defaultOptions: [],\n\n create(context) {\n function checkHydratedElement(element: TSESTree.JSXElement) {\n for (const attribute of element.openingElement.attributes) {\n if (attribute.type === 'JSXAttribute') {\n if (attribute.name.type === 'JSXIdentifier' && FORBIDDEN_NAMES.has(attribute.name.name)) {\n context.report({\n node: attribute,\n messageId: 'forbiddenProp',\n data: { name: attribute.name.name },\n });\n }\n } else if (attribute.type === 'JSXSpreadAttribute') {\n if (spreadArgumentDescribesResLocals(attribute.argument)) {\n context.report({\n node: attribute,\n messageId: 'forbiddenSpread',\n });\n }\n }\n }\n }\n\n return {\n JSXElement(node) {\n const opening = node.openingElement.name;\n if (opening.type !== 'JSXIdentifier' || opening.name !== HYDRATE_COMPONENT_NAME) return;\n\n const child = extractSingleJsxChild(node.children);\n if (!child) return;\n\n checkHydratedElement(child);\n },\n\n CallExpression(node) {\n if (node.callee.type !== 'Identifier' || node.callee.name !== HYDRATE_FUNCTION_NAME) return;\n if (node.arguments.length === 0) return;\n\n const arg = node.arguments[0];\n if (arg.type !== 'JSXElement') return;\n\n checkHydratedElement(arg);\n },\n };\n },\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-unused-sql-blocks.d.ts","sourceRoot":"","sources":["../../src/rules/no-unused-sql-blocks.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC
|
|
1
|
+
{"version":3,"file":"no-unused-sql-blocks.d.ts","sourceRoot":"","sources":["../../src/rules/no-unused-sql-blocks.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { ESLintUtils } from '@typescript-eslint/utils';\n\nfunction extractSqlBlockDefinitions(sqlContent: string): Set<string> {\n const regex = / *-- *BLOCK +([^ \\n]+) */g;\n const defs = new Set<string>();\n let match;\n while ((match = regex.exec(sqlContent)) !== null) {\n defs.add(match[1]);\n }\n return defs;\n}\n\nfunction extractSqlBlockReferences(tsContent: string): Set<string> {\n const regex = /sql\\.([a-zA-Z0-9_]+)/g;\n const refs = new Set<string>();\n let match;\n while ((match = regex.exec(tsContent)) !== null) {\n refs.add(match[1]);\n }\n return refs;\n}\n\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n unusedSqlBlock:\n 'SQL block \"{{block}}\" in \"{{sqlFile}}\" is not used in this file and should be deleted.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const components = path.parse(context.filename);\n components.ext = '.sql';\n const sqlFile = path.join(components.dir, components.name) + components.ext;\n if (!fs.existsSync(sqlFile)) return {};\n const tsContent = fs.readFileSync(context.filename, 'utf8');\n const sqlContent = fs.readFileSync(sqlFile, 'utf8');\n\n const usedBlocks = extractSqlBlockReferences(tsContent);\n const definedBlocks = extractSqlBlockDefinitions(sqlContent);\n\n const unusedBlocks = [...definedBlocks].filter((block) => !usedBlocks.has(block));\n\n return {\n Program(node) {\n for (const block of unusedBlocks) {\n context.report({\n node,\n loc: {\n start: { line: 1, column: 1 },\n end: { line: 2, column: 0 },\n },\n messageId: 'unusedSqlBlock',\n data: { block, sqlFile: path.basename(sqlFile) },\n });\n }\n },\n };\n },\n});\n"]}
|
|
@@ -1,41 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
const fs = __importStar(require("fs"));
|
|
37
|
-
const path = __importStar(require("path"));
|
|
38
|
-
const utils_1 = require("@typescript-eslint/utils");
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
39
4
|
function extractSqlBlockDefinitions(sqlContent) {
|
|
40
5
|
const regex = / *-- *BLOCK +([^ \n]+) */g;
|
|
41
6
|
const defs = new Set();
|
|
@@ -54,7 +19,7 @@ function extractSqlBlockReferences(tsContent) {
|
|
|
54
19
|
}
|
|
55
20
|
return refs;
|
|
56
21
|
}
|
|
57
|
-
|
|
22
|
+
export default ESLintUtils.RuleCreator.withoutDocs({
|
|
58
23
|
meta: {
|
|
59
24
|
type: 'problem',
|
|
60
25
|
messages: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-unused-sql-blocks.js","sourceRoot":"","sources":["../../src/rules/no-unused-sql-blocks.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"no-unused-sql-blocks.js","sourceRoot":"","sources":["../../src/rules/no-unused-sql-blocks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD,SAAS,0BAA0B,CAAC,UAAkB;IACpD,MAAM,KAAK,GAAG,2BAA2B,CAAC;IAC1C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,IAAI,KAAK,CAAC;IACV,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACjD,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,yBAAyB,CAAC,SAAiB;IAClD,MAAM,KAAK,GAAG,uBAAuB,CAAC;IACtC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,IAAI,KAAK,CAAC;IACV,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAChD,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,eAAe,WAAW,CAAC,WAAW,CAAC,WAAW,CAAC;IACjD,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE;YACR,cAAc,EACZ,wFAAwF;SAC3F;QACD,MAAM,EAAE,EAAE;KACX;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO;QACZ,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAChD,UAAU,CAAC,GAAG,GAAG,MAAM,CAAC;QACxB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,UAAU,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC;QAC5E,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,OAAO,EAAE,CAAC;QACvC,MAAM,SAAS,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC5D,MAAM,UAAU,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAEpD,MAAM,UAAU,GAAG,yBAAyB,CAAC,SAAS,CAAC,CAAC;QACxD,MAAM,aAAa,GAAG,0BAA0B,CAAC,UAAU,CAAC,CAAC;QAE7D,MAAM,YAAY,GAAG,CAAC,GAAG,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;QAElF,OAAO;YACL,OAAO,CAAC,IAAI;gBACV,KAAK,MAAM,KAAK,IAAI,YAAY,EAAE,CAAC;oBACjC,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI;wBACJ,GAAG,EAAE;4BACH,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;4BAC7B,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;yBAC5B;wBACD,SAAS,EAAE,gBAAgB;wBAC3B,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE;qBACjD,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { ESLintUtils } from '@typescript-eslint/utils';\n\nfunction extractSqlBlockDefinitions(sqlContent: string): Set<string> {\n const regex = / *-- *BLOCK +([^ \\n]+) */g;\n const defs = new Set<string>();\n let match;\n while ((match = regex.exec(sqlContent)) !== null) {\n defs.add(match[1]);\n }\n return defs;\n}\n\nfunction extractSqlBlockReferences(tsContent: string): Set<string> {\n const regex = /sql\\.([a-zA-Z0-9_]+)/g;\n const refs = new Set<string>();\n let match;\n while ((match = regex.exec(tsContent)) !== null) {\n refs.add(match[1]);\n }\n return refs;\n}\n\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n unusedSqlBlock:\n 'SQL block \"{{block}}\" in \"{{sqlFile}}\" is not used in this file and should be deleted.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const components = path.parse(context.filename);\n components.ext = '.sql';\n const sqlFile = path.join(components.dir, components.name) + components.ext;\n if (!fs.existsSync(sqlFile)) return {};\n const tsContent = fs.readFileSync(context.filename, 'utf8');\n const sqlContent = fs.readFileSync(sqlFile, 'utf8');\n\n const usedBlocks = extractSqlBlockReferences(tsContent);\n const definedBlocks = extractSqlBlockDefinitions(sqlContent);\n\n const unusedBlocks = [...definedBlocks].filter((block) => !usedBlocks.has(block));\n\n return {\n Program(node) {\n for (const block of unusedBlocks) {\n context.report({\n node,\n loc: {\n start: { line: 1, column: 1 },\n end: { line: 2, column: 0 },\n },\n messageId: 'unusedSqlBlock',\n data: { block, sqlFile: path.basename(sqlFile) },\n });\n }\n },\n };\n },\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-trpc-permission-middleware.d.ts","sourceRoot":"","sources":["../../src/rules/require-trpc-permission-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,WAAW,EAAgC,MAAM,0BAA0B,CAAC","sourcesContent":["import { ASTUtils, ESLintUtils, type TSESLint, type TSESTree } from '@typescript-eslint/utils';\n\nconst { findVariable } = ASTUtils;\n\n/**\n * tRPC procedures must enforce their own authorization via a `require*`\n * permission middleware in the chain.\n * Individual procedures often need stricter permissions than the page they're\n * mounted under (e.g. an Edit-only mutation under a route that allows View).\n * Requiring an explicit permission middleware per procedure makes the\n * authz gate visible at the call site and helps prevent missing permission checks.\n *\n * Bad:\n * ```ts\n * const list = t.procedure.query(async ({ ctx }) => { ... });\n * ```\n *\n * Good:\n * ```ts\n * const list = t.procedure.use(requireCoursePermissionEdit).query(...);\n * ```\n *\n * Recognized permission middlewares:\n * - requireCoursePermissionPreview / View / Edit / Own\n * - requireCourseInstancePermissionPreview / View / Edit / Own\n * - requireAdministrator\n *\n * Feature gates like `requireEnhancedAccessControl` and `requireAiGradingFeature`\n * don't count — they must be paired with a permission middleware.\n *\n * The rule follows intermediate procedure-base variables so patterns like\n * `const protected = t.procedure.use(...); ...; protected.query(...)` are\n * checked end-to-end.\n */\n\n// TODO: This list requires manual maintenance. We should consider making feature gates\n// not use the `require` prefix, and then we can use a more generic pattern here.\nconst PERMISSION_MIDDLEWARE_PATTERN =\n /^require(Course|CourseInstance)Permission(Preview|View|Edit|Own)$|^requireAdministrator$/;\nconst TERMINAL_METHODS = new Set(['query', 'mutation', 'subscription']);\n\nfunction unwrapMemberObject(expr: TSESTree.Expression): TSESTree.Expression {\n return expr.type === 'TSNonNullExpression' || expr.type === 'TSAsExpression'\n ? unwrapMemberObject(expr.expression)\n : expr;\n}\n\nfunction isTProcedure(expr: TSESTree.Expression): boolean {\n return (\n expr.type === 'MemberExpression' &&\n !expr.computed &&\n expr.property.type === 'Identifier' &&\n expr.property.name === 'procedure' &&\n expr.object.type === 'Identifier' &&\n expr.object.name === 't'\n );\n}\n\ninterface ChainInfo {\n matchesTProcedure: boolean;\n middlewares: string[];\n}\n\n/**\n * Walks back through a chain like `expr.use(A).input(B).use(C)` collecting\n * `.use(...)` argument identifiers. If the base of the chain is `t.procedure`,\n * returns the collected middlewares. If the base is a variable, follows the\n * variable's initializer (e.g. `const protected = t.procedure.use(X)`).\n */\nfunction getProcedureChainInfo(\n expr: TSESTree.Expression,\n scope: TSESLint.Scope.Scope,\n visited: Set<TSESTree.Node>,\n): ChainInfo {\n if (visited.has(expr)) return { matchesTProcedure: false, middlewares: [] };\n visited.add(expr);\n\n const middlewares: string[] = [];\n let cursor: TSESTree.Expression = expr;\n\n while (cursor.type === 'CallExpression') {\n const callee = cursor.callee;\n if (\n callee.type !== 'MemberExpression' ||\n callee.computed ||\n callee.property.type !== 'Identifier'\n ) {\n return { matchesTProcedure: false, middlewares: [] };\n }\n if (callee.property.name === 'use' && cursor.arguments[0]?.type === 'Identifier') {\n middlewares.push(cursor.arguments[0].name);\n }\n cursor = callee.object;\n }\n\n cursor = unwrapMemberObject(cursor);\n\n if (isTProcedure(cursor)) {\n return { matchesTProcedure: true, middlewares };\n }\n\n // Follow an intermediate procedure-base variable defined in the same module:\n // const protectedProcedure = t.procedure.use(...);\n // protectedProcedure.query(...);\n if (cursor.type === 'Identifier') {\n const variable = findVariable(scope, cursor);\n if (variable && variable.defs.length === 1) {\n const def = variable.defs[0];\n if (\n def.node.type === 'VariableDeclarator' &&\n def.node.init &&\n // Conservative: only follow `const` bindings to avoid stale data\n // from let/var rebinding.\n def.parent?.type === 'VariableDeclaration' &&\n def.parent.kind === 'const'\n ) {\n const upstream = getProcedureChainInfo(def.node.init, scope, visited);\n if (upstream.matchesTProcedure) {\n return {\n matchesTProcedure: true,\n middlewares: [...middlewares, ...upstream.middlewares],\n };\n }\n }\n }\n }\n\n return { matchesTProcedure: false, middlewares: [] };\n}\n\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n missingPermissionMiddleware:\n 'tRPC procedure must call .use() with a permission middleware (e.g. requireCoursePermissionEdit)',\n },\n schema: [],\n },\n defaultOptions: [],\n\n create(context) {\n return {\n CallExpression(node) {\n if (\n node.callee.type !== 'MemberExpression' ||\n node.callee.computed ||\n node.callee.property.type !== 'Identifier' ||\n !TERMINAL_METHODS.has(node.callee.property.name)\n ) {\n return;\n }\n\n const scope = context.sourceCode.getScope(node);\n const info = getProcedureChainInfo(node.callee.object, scope, new Set());\n\n if (!info.matchesTProcedure) return;\n\n const hasPermissionGate = info.middlewares.some((name) =>\n PERMISSION_MIDDLEWARE_PATTERN.test(name),\n );\n if (!hasPermissionGate) {\n context.report({ node, messageId: 'missingPermissionMiddleware' });\n }\n },\n };\n },\n});\n"]}
|