@prairielearn/eslint-plugin 3.0.1 → 3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @prairielearn/eslint-plugin
2
2
 
3
+ ## 3.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 7b937fb: Remove unused exports, add `@knipignore` for intentionally public exports, and re-export newly used symbols from `@prairielearn/formatter`.
8
+
9
+ ## 3.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 174fbb4: Add `no-current-target-in-callback` lint rule to detect when `event.currentTarget` is accessed inside a nested callback within a React event handler. This pattern is problematic because React may execute callbacks asynchronously, at which point `currentTarget` may already be nullified.
14
+
3
15
  ## 3.0.1
4
16
 
5
17
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export declare const rules: {
2
2
  'aws-client-mandatory-config': import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingConfig", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
3
3
  'aws-client-shared-config': import("@typescript-eslint/utils/ts-eslint").RuleModule<"improperConfig" | "unknownConfig", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
4
4
  'jsx-no-dollar-interpolation': import("@typescript-eslint/utils/ts-eslint").RuleModule<"dollarInterpolationNotAllowed", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
5
+ 'no-current-target-in-callback': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noCurrentTargetInCallback", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
5
6
  'no-unused-sql-blocks': import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedSqlBlock", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
6
7
  'safe-db-types': import("@typescript-eslint/utils/ts-eslint").RuleModule<"spreadAttributes" | "unsafeTypes", [({
7
8
  allowDbTypes?: (string | RegExp)[] | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,KAAK;;;;;;;;CAMjB,CAAC","sourcesContent":["import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';\nimport awsClientSharedConfig from './rules/aws-client-shared-config.js';\nimport jsxNoDollarInterpolation from './rules/jsx-no-dollar-interpolation.js';\nimport noUnusedSqlBlocks from './rules/no-unused-sql-blocks.js';\nimport safeDbTypes from './rules/safe-db-types.js';\n\nexport const rules = {\n 'aws-client-mandatory-config': awsClientMandatoryConfig,\n 'aws-client-shared-config': awsClientSharedConfig,\n 'jsx-no-dollar-interpolation': jsxNoDollarInterpolation,\n 'no-unused-sql-blocks': noUnusedSqlBlocks,\n 'safe-db-types': safeDbTypes,\n};\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,KAAK;;;;;;;;;CAOjB,CAAC","sourcesContent":["import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';\nimport awsClientSharedConfig from './rules/aws-client-shared-config.js';\nimport jsxNoDollarInterpolation from './rules/jsx-no-dollar-interpolation.js';\nimport noCurrentTargetInCallback from './rules/no-current-target-in-callback.js';\nimport noUnusedSqlBlocks from './rules/no-unused-sql-blocks.js';\nimport safeDbTypes from './rules/safe-db-types.js';\n\nexport const rules = {\n 'aws-client-mandatory-config': awsClientMandatoryConfig,\n 'aws-client-shared-config': awsClientSharedConfig,\n 'jsx-no-dollar-interpolation': jsxNoDollarInterpolation,\n 'no-current-target-in-callback': noCurrentTargetInCallback,\n 'no-unused-sql-blocks': noUnusedSqlBlocks,\n 'safe-db-types': safeDbTypes,\n};\n"]}
package/dist/index.js CHANGED
@@ -7,12 +7,14 @@ exports.rules = void 0;
7
7
  const aws_client_mandatory_config_js_1 = __importDefault(require("./rules/aws-client-mandatory-config.js"));
8
8
  const aws_client_shared_config_js_1 = __importDefault(require("./rules/aws-client-shared-config.js"));
9
9
  const jsx_no_dollar_interpolation_js_1 = __importDefault(require("./rules/jsx-no-dollar-interpolation.js"));
10
+ const no_current_target_in_callback_js_1 = __importDefault(require("./rules/no-current-target-in-callback.js"));
10
11
  const no_unused_sql_blocks_js_1 = __importDefault(require("./rules/no-unused-sql-blocks.js"));
11
12
  const safe_db_types_js_1 = __importDefault(require("./rules/safe-db-types.js"));
12
13
  exports.rules = {
13
14
  'aws-client-mandatory-config': aws_client_mandatory_config_js_1.default,
14
15
  'aws-client-shared-config': aws_client_shared_config_js_1.default,
15
16
  'jsx-no-dollar-interpolation': jsx_no_dollar_interpolation_js_1.default,
17
+ 'no-current-target-in-callback': no_current_target_in_callback_js_1.default,
16
18
  'no-unused-sql-blocks': no_unused_sql_blocks_js_1.default,
17
19
  'safe-db-types': safe_db_types_js_1.default,
18
20
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAAA,4GAA8E;AAC9E,sGAAwE;AACxE,4GAA8E;AAC9E,8FAAgE;AAChE,gFAAmD;AAEtC,QAAA,KAAK,GAAG;IACnB,6BAA6B,EAAE,wCAAwB;IACvD,0BAA0B,EAAE,qCAAqB;IACjD,6BAA6B,EAAE,wCAAwB;IACvD,sBAAsB,EAAE,iCAAiB;IACzC,eAAe,EAAE,0BAAW;CAC7B,CAAC","sourcesContent":["import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';\nimport awsClientSharedConfig from './rules/aws-client-shared-config.js';\nimport jsxNoDollarInterpolation from './rules/jsx-no-dollar-interpolation.js';\nimport noUnusedSqlBlocks from './rules/no-unused-sql-blocks.js';\nimport safeDbTypes from './rules/safe-db-types.js';\n\nexport const rules = {\n 'aws-client-mandatory-config': awsClientMandatoryConfig,\n 'aws-client-shared-config': awsClientSharedConfig,\n 'jsx-no-dollar-interpolation': jsxNoDollarInterpolation,\n 'no-unused-sql-blocks': noUnusedSqlBlocks,\n 'safe-db-types': safeDbTypes,\n};\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAAA,4GAA8E;AAC9E,sGAAwE;AACxE,4GAA8E;AAC9E,gHAAiF;AACjF,8FAAgE;AAChE,gFAAmD;AAEtC,QAAA,KAAK,GAAG;IACnB,6BAA6B,EAAE,wCAAwB;IACvD,0BAA0B,EAAE,qCAAqB;IACjD,6BAA6B,EAAE,wCAAwB;IACvD,+BAA+B,EAAE,0CAAyB;IAC1D,sBAAsB,EAAE,iCAAiB;IACzC,eAAe,EAAE,0BAAW;CAC7B,CAAC","sourcesContent":["import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';\nimport awsClientSharedConfig from './rules/aws-client-shared-config.js';\nimport jsxNoDollarInterpolation from './rules/jsx-no-dollar-interpolation.js';\nimport noCurrentTargetInCallback from './rules/no-current-target-in-callback.js';\nimport noUnusedSqlBlocks from './rules/no-unused-sql-blocks.js';\nimport safeDbTypes from './rules/safe-db-types.js';\n\nexport const rules = {\n 'aws-client-mandatory-config': awsClientMandatoryConfig,\n 'aws-client-shared-config': awsClientSharedConfig,\n 'jsx-no-dollar-interpolation': jsxNoDollarInterpolation,\n 'no-current-target-in-callback': noCurrentTargetInCallback,\n 'no-unused-sql-blocks': noUnusedSqlBlocks,\n 'safe-db-types': safeDbTypes,\n};\n"]}
@@ -0,0 +1,20 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ /**
3
+ * This rule detects when `event.currentTarget` is accessed inside a nested
4
+ * callback function within a React event handler. This is problematic because
5
+ * React may execute callbacks (like those passed to setState) asynchronously,
6
+ * at which point `currentTarget` may already be nullified.
7
+ *
8
+ * Bad:
9
+ * ```tsx
10
+ * onChange={(e) => setChecks((c) => ({ ...c, value: e.currentTarget.checked }))}
11
+ * ```
12
+ *
13
+ * Good:
14
+ * ```tsx
15
+ * onChange={({ currentTarget }) => setChecks((c) => ({ ...c, value: currentTarget.checked }))}
16
+ * ```
17
+ */
18
+ declare const _default: ESLintUtils.RuleModule<"noCurrentTargetInCallback", [], unknown, ESLintUtils.RuleListener>;
19
+ export default _default;
20
+ //# sourceMappingURL=no-current-target-in-callback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-current-target-in-callback.d.ts","sourceRoot":"","sources":["../../src/rules/no-current-target-in-callback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAEtE;;;;;;;;;;;;;;;GAeG;;AACH,wBA8JG","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"]}
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("@typescript-eslint/utils");
4
+ /**
5
+ * This rule detects when `event.currentTarget` is accessed inside a nested
6
+ * callback function within a React event handler. This is problematic because
7
+ * React may execute callbacks (like those passed to setState) asynchronously,
8
+ * at which point `currentTarget` may already be nullified.
9
+ *
10
+ * Bad:
11
+ * ```tsx
12
+ * onChange={(e) => setChecks((c) => ({ ...c, value: e.currentTarget.checked }))}
13
+ * ```
14
+ *
15
+ * Good:
16
+ * ```tsx
17
+ * onChange={({ currentTarget }) => setChecks((c) => ({ ...c, value: currentTarget.checked }))}
18
+ * ```
19
+ */
20
+ exports.default = utils_1.ESLintUtils.RuleCreator.withoutDocs({
21
+ meta: {
22
+ type: 'problem',
23
+ messages: {
24
+ noCurrentTargetInCallback: '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.',
25
+ },
26
+ schema: [],
27
+ },
28
+ defaultOptions: [],
29
+ create(context) {
30
+ // Track event handler parameters and their scopes
31
+ const eventHandlerParams = new Map();
32
+ /** Check if a node is inside a nested function relative to the event handler */
33
+ function isInsideNestedFunction(node, eventHandlerFunction) {
34
+ let current = node.parent;
35
+ let foundNestedFunction = false;
36
+ while (current && current !== eventHandlerFunction) {
37
+ if (current.type === 'ArrowFunctionExpression' ||
38
+ current.type === 'FunctionExpression' ||
39
+ current.type === 'FunctionDeclaration') {
40
+ foundNestedFunction = true;
41
+ }
42
+ current = current.parent;
43
+ }
44
+ return foundNestedFunction && current === eventHandlerFunction;
45
+ }
46
+ /** Find the function that contains this node */
47
+ function findContainingFunction(node) {
48
+ let current = node.parent;
49
+ while (current) {
50
+ if (current.type === 'ArrowFunctionExpression' || current.type === 'FunctionExpression') {
51
+ return current;
52
+ }
53
+ current = current.parent;
54
+ }
55
+ return null;
56
+ }
57
+ /** Check if a function is a JSX event handler */
58
+ function isJSXEventHandler(func) {
59
+ const parent = func.parent;
60
+ // Check for JSX attribute like onChange={...}
61
+ if (parent?.type === 'JSXExpressionContainer') {
62
+ const jsxAttribute = parent.parent;
63
+ if (jsxAttribute?.type === 'JSXAttribute') {
64
+ const attrName = jsxAttribute.name.type === 'JSXIdentifier' ? jsxAttribute.name.name : '';
65
+ // Match common event handler patterns
66
+ return /^on[A-Z]/.test(attrName);
67
+ }
68
+ }
69
+ return false;
70
+ }
71
+ /** Get the first parameter of an event handler if it looks like an event */
72
+ function getEventParameter(func) {
73
+ const firstParam = func.params[0];
74
+ if (!firstParam)
75
+ return null;
76
+ // Simple identifier parameter like (e) => ...
77
+ if (firstParam.type === 'Identifier') {
78
+ return firstParam;
79
+ }
80
+ return null;
81
+ }
82
+ return {
83
+ // When we enter a JSX event handler, track its event parameter
84
+ 'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression': (node) => {
85
+ if (isJSXEventHandler(node)) {
86
+ const eventParam = getEventParameter(node);
87
+ if (eventParam) {
88
+ eventHandlerParams.set(node, eventParam);
89
+ }
90
+ }
91
+ },
92
+ 'JSXAttribute > JSXExpressionContainer > FunctionExpression': (node) => {
93
+ if (isJSXEventHandler(node)) {
94
+ const eventParam = getEventParameter(node);
95
+ if (eventParam) {
96
+ eventHandlerParams.set(node, eventParam);
97
+ }
98
+ }
99
+ },
100
+ // Check for .currentTarget access
101
+ MemberExpression(node) {
102
+ // Only check for .currentTarget property access
103
+ if (node.property.type !== 'Identifier' || node.property.name !== 'currentTarget') {
104
+ return;
105
+ }
106
+ // Check if the object is an identifier (like `e` in `e.currentTarget`)
107
+ if (node.object.type !== 'Identifier') {
108
+ return;
109
+ }
110
+ const objectName = node.object.name;
111
+ // Find the containing function
112
+ const containingFunction = findContainingFunction(node);
113
+ if (!containingFunction)
114
+ return;
115
+ // Check each tracked event handler to see if this access is problematic
116
+ for (const [eventHandler, eventParam] of eventHandlerParams) {
117
+ // Check if this is accessing the event parameter
118
+ if (objectName !== eventParam.name)
119
+ continue;
120
+ // Check if the access is inside a nested function within the event handler
121
+ if (isInsideNestedFunction(node, eventHandler)) {
122
+ context.report({
123
+ node,
124
+ messageId: 'noCurrentTargetInCallback',
125
+ });
126
+ return;
127
+ }
128
+ }
129
+ },
130
+ // Clean up when leaving event handlers
131
+ 'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression:exit': (node) => {
132
+ eventHandlerParams.delete(node);
133
+ },
134
+ 'JSXAttribute > JSXExpressionContainer > FunctionExpression:exit': (node) => {
135
+ eventHandlerParams.delete(node);
136
+ },
137
+ };
138
+ },
139
+ });
140
+ //# sourceMappingURL=no-current-target-in-callback.js.map
@@ -0,0 +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 +1 @@
1
- {"version":3,"file":"safe-db-types.d.ts","sourceRoot":"","sources":["../../src/rules/safe-db-types.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;;;;AAyevD,wBAsGG","sourcesContent":["import { type TSESTree } from '@typescript-eslint/types';\nimport { ESLintUtils } from '@typescript-eslint/utils';\nimport * as ts from 'typescript';\n\nconst HYDRATE_FUNCTION_NAME = 'hydrateHtml';\nconst HYDRATE_COMPONENT_NAME = 'Hydrate';\n\n/**\n * Check if a variable declaration is a Zod schema that uses schemas from db-types.ts\n * For example: const RubricDataSchema = RubricSchema.extend({...})\n */\nfunction checkZodSchemaForDbTypes(\n declaration: ts.VariableDeclaration,\n typeChecker: ts.TypeChecker,\n): string[] {\n const violations: string[] = [];\n\n if (!declaration.initializer) return violations;\n\n // Walk the expression tree to find all identifiers\n const findIdentifiers = (node: ts.Node): void => {\n // Check property access expressions like RubricSchema.extend() or InstanceQuestionSchema.shape\n if (ts.isPropertyAccessExpression(node)) {\n const objectSymbol = typeChecker.getSymbolAtLocation(node.expression);\n if (objectSymbol) {\n const aliasedSymbol =\n objectSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(objectSymbol)\n : objectSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n\n // Check spread elements in object literals (e.g., ...SomeSchema.shape)\n if (ts.isSpreadAssignment(node)) {\n // The expression being spread (e.g., SomeSchema.shape)\n const spreadExpr = node.expression;\n\n // Check if it's a property access (e.g., accessing .shape)\n if (ts.isPropertyAccessExpression(spreadExpr)) {\n const objectSymbol = typeChecker.getSymbolAtLocation(spreadExpr.expression);\n if (objectSymbol) {\n const aliasedSymbol =\n objectSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(objectSymbol)\n : objectSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n } else {\n // The schema is defined locally, check if IT uses db-types\n for (const decl of decls) {\n if (ts.isVariableDeclaration(decl)) {\n // Recursively check the local schema\n const nestedViolations = checkZodSchemaForDbTypes(decl, typeChecker);\n violations.push(...nestedViolations);\n }\n }\n }\n }\n }\n }\n // Also check if the spread is a direct identifier (e.g., ...someObject)\n else if (ts.isIdentifier(spreadExpr)) {\n const spreadSymbol = typeChecker.getSymbolAtLocation(spreadExpr);\n if (spreadSymbol) {\n const aliasedSymbol =\n spreadSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(spreadSymbol)\n : spreadSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n }\n\n // Check call expressions for arguments\n if (ts.isCallExpression(node)) {\n for (const arg of node.arguments) {\n // Check if argument is an identifier (e.g., RubricItemSchema)\n if (ts.isIdentifier(arg)) {\n const argSymbol = typeChecker.getSymbolAtLocation(arg);\n if (argSymbol) {\n const aliasedSymbol =\n argSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(argSymbol)\n : argSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n }\n }\n\n // Check object literal property assignments (e.g., { rubric: RubricSchema })\n if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.initializer)) {\n const identifier = node.initializer;\n const symbol = typeChecker.getSymbolAtLocation(identifier);\n if (symbol) {\n const aliasedSymbol =\n symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n\n ts.forEachChild(node, findIdentifiers);\n };\n\n findIdentifiers(declaration.initializer);\n return violations;\n}\n\n/**\n * Check if a type node is z.infer<typeof SchemaName> and if SchemaName uses db-types\n * Returns the names of db-types that the schema depends on\n */\nfunction checkForZodInferPattern(typeNode: ts.TypeNode, typeChecker: ts.TypeChecker): string[] {\n // Check if this is a type reference with type arguments\n if (!ts.isTypeReferenceNode(typeNode)) return [];\n\n // Check if the type reference is named (e.g., \"infer\" from z.infer)\n const typeName = typeNode.typeName;\n if (!ts.isQualifiedName(typeName)) return [];\n\n // Check if it's z.infer or similar pattern\n // The pattern is: z.infer<typeof SchemaName>\n const typeArgs = typeNode.typeArguments;\n if (!typeArgs || typeArgs.length !== 1) return [];\n\n const typeArg = typeArgs[0];\n\n // Check if the type argument is a typeof expression\n if (!ts.isTypeQueryNode(typeArg)) return [];\n\n // Get the schema name from typeof X\n const exprName = typeArg.exprName;\n if (!ts.isIdentifier(exprName)) return [];\n\n // Now find the schema variable declaration\n const schemaSymbol = typeChecker.getSymbolAtLocation(exprName);\n if (!schemaSymbol) return [];\n\n // Check if this is an imported symbol (alias) - follow it to the original\n const symbolToCheck =\n schemaSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(schemaSymbol)\n : schemaSymbol;\n\n const schemaDecls = symbolToCheck.getDeclarations();\n if (!schemaDecls || schemaDecls.length === 0) return [];\n\n // Check if it's a variable declaration\n for (const decl of schemaDecls) {\n if (ts.isVariableDeclaration(decl)) {\n // If the schema is defined in safe-db-types.ts, it's safe by definition\n const sourceFile = decl.getSourceFile();\n if (sourceFile.fileName.endsWith('/safe-db-types.ts')) {\n return [];\n }\n\n // Use our existing helper to check if the schema uses db-types\n return checkZodSchemaForDbTypes(decl, typeChecker);\n }\n }\n\n return [];\n}\n\nfunction extractChild(children: TSESTree.JSXChild[]): TSESTree.JSXElement | null {\n const nonWhitespaceChildren = children.filter((child) => {\n if (child.type === 'JSXText') {\n return child.value.trim().length > 0;\n }\n return true;\n });\n\n if (nonWhitespaceChildren.length !== 1 || nonWhitespaceChildren[0].type !== 'JSXElement') {\n return null;\n }\n\n return nonWhitespaceChildren[0];\n}\n\n/**\n * Check if a TypeScript type node references a type from db-types.ts\n * This checks the actual source code type annotation, not the resolved type.\n * Follows type aliases (imports) to their original declaration.\n * Returns all unsafe type names found.\n */\nfunction checkTypeNodeForDbTypes(\n typeNode: ts.TypeNode,\n typeChecker: ts.TypeChecker,\n visited = new Set<ts.TypeNode>(),\n): string[] {\n if (visited.has(typeNode)) return [];\n visited.add(typeNode);\n\n const violations: string[] = [];\n\n // Check type references (e.g., User, Course, AuthnProvider)\n if (ts.isTypeReferenceNode(typeNode)) {\n const typeName = typeNode.typeName;\n const symbol = typeChecker.getSymbolAtLocation(typeName);\n\n if (symbol) {\n // Check if this is an imported symbol (alias) - follow it to the original\n const symbolToCheck =\n symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;\n\n const declarations = symbolToCheck.getDeclarations();\n if (declarations && declarations.length > 0) {\n for (const decl of declarations) {\n const sourceFile = decl.getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n // Found a type from db-types.ts!\n violations.push(symbolToCheck.getName());\n } else {\n // If it's a type alias or interface defined locally, check its properties\n if (ts.isTypeAliasDeclaration(decl) && decl.type) {\n // Special case: Check if this is z.infer<typeof XxxSchema>\n const zodSchemaViolations = checkForZodInferPattern(decl.type, typeChecker);\n violations.push(...zodSchemaViolations);\n\n // Also check the type itself\n const nestedViolations = checkTypeNodeForDbTypes(decl.type, typeChecker, visited);\n violations.push(...nestedViolations);\n } else if (ts.isInterfaceDeclaration(decl)) {\n // Check interface members\n for (const member of decl.members) {\n if (ts.isPropertySignature(member) && member.type) {\n const nestedViolations = checkTypeNodeForDbTypes(\n member.type,\n typeChecker,\n visited,\n );\n violations.push(...nestedViolations);\n }\n }\n }\n }\n }\n }\n }\n\n // Check type arguments (e.g., Array<User>, Promise<Course>)\n if (typeNode.typeArguments) {\n for (const typeArg of typeNode.typeArguments) {\n const nestedViolations = checkTypeNodeForDbTypes(typeArg, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n }\n\n // Check array types (e.g., User[])\n if (ts.isArrayTypeNode(typeNode)) {\n const nestedViolations = checkTypeNodeForDbTypes(typeNode.elementType, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n\n // Check union types (e.g., User | null)\n if (ts.isUnionTypeNode(typeNode)) {\n for (const type of typeNode.types) {\n const nestedViolations = checkTypeNodeForDbTypes(type, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n\n // Check intersection types (e.g., User & { extra: string })\n if (ts.isIntersectionTypeNode(typeNode)) {\n for (const type of typeNode.types) {\n const nestedViolations = checkTypeNodeForDbTypes(type, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n\n // Check object type literals and their properties\n if (ts.isTypeLiteralNode(typeNode)) {\n for (const member of typeNode.members) {\n if (ts.isPropertySignature(member) && member.type) {\n const nestedViolations = checkTypeNodeForDbTypes(member.type, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n }\n\n // Check indexed access types (e.g., User['name'])\n // These are safe! We're only extracting a specific property, not passing the whole object.\n // So we DON'T recurse into the object type for indexed access.\n if (ts.isIndexedAccessTypeNode(typeNode)) {\n // Do not check - indexed access is safe\n }\n\n return violations;\n}\n\n/**\n * Check if a type name is in the allowlist of safe types\n */\nfunction isTypeInAllowlist(typeName: string, allowlist: (string | RegExp)[]): boolean {\n return allowlist.some((pattern) => {\n if (typeof pattern === 'string') {\n return typeName === pattern;\n }\n return pattern.test(typeName);\n });\n}\n\n/**\n * Check the props of a component for unsafe types from db-types.ts\n */\nfunction checkComponentProps(\n context: ReturnType<typeof ESLintUtils.RuleCreator.withoutDocs>['create'] extends (\n context: infer C,\n ) => any\n ? C\n : never,\n typeChecker: ts.TypeChecker,\n componentSymbol: ts.Symbol,\n tsComponentNode: ts.Node,\n jsxElement: TSESTree.JSXElement,\n reportNode: TSESTree.Node,\n allowlist: (string | RegExp)[],\n): void {\n const childOpeningElement = jsxElement.openingElement;\n\n // Get the component's type (function or class component)\n const componentType = typeChecker.getTypeOfSymbolAtLocation(componentSymbol, tsComponentNode);\n const signatures = componentType.getCallSignatures();\n\n if (signatures.length === 0) return;\n\n // Get the first parameter (props) of the component function\n const propsParam = signatures[0].getParameters()[0];\n if (!propsParam) return;\n\n const propsDeclaration = propsParam.valueDeclaration;\n\n if (!propsDeclaration || !ts.isParameter(propsDeclaration)) return;\n\n // Get the type annotation node from the props parameter\n const propsTypeNode = propsDeclaration.type;\n if (!propsTypeNode) return;\n\n // Check each property in the props type\n if (ts.isTypeLiteralNode(propsTypeNode)) {\n // Inline props object: { foo: string; bar: number }\n for (const member of propsTypeNode.members) {\n if (ts.isPropertySignature(member) && member.type && member.name) {\n if (!ts.isIdentifier(member.name)) continue;\n\n const propName = member.name.text;\n const violations = checkTypeNodeForDbTypes(member.type, typeChecker);\n\n if (violations.length > 0) {\n // Find the JSX attribute for this prop\n const attribute = childOpeningElement.attributes.find(\n (attr) =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === propName,\n );\n\n for (const typeName of violations) {\n if (isTypeInAllowlist(typeName, allowlist)) continue;\n\n context.report({\n node: attribute || reportNode,\n messageId: 'unsafeTypes',\n data: { propName, typeName },\n });\n }\n }\n }\n }\n } else if (ts.isTypeReferenceNode(propsTypeNode)) {\n // Props is a type reference (e.g., interface or type alias)\n const symbol = typeChecker.getSymbolAtLocation(propsTypeNode.typeName);\n if (symbol) {\n const resolvedSymbol =\n symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;\n const declarations = resolvedSymbol.getDeclarations();\n\n if (declarations && declarations.length > 0) {\n for (const decl of declarations) {\n if (ts.isInterfaceDeclaration(decl) || ts.isTypeLiteralNode(decl)) {\n const members = ts.isInterfaceDeclaration(decl) ? decl.members : decl.members;\n\n for (const member of members) {\n if (ts.isPropertySignature(member) && member.type && member.name) {\n if (!ts.isIdentifier(member.name)) continue;\n\n const propName = member.name.text;\n const violations = checkTypeNodeForDbTypes(member.type, typeChecker);\n\n if (violations.length > 0) {\n // Find the JSX attribute for this prop\n const attribute = childOpeningElement.attributes.find(\n (attr) =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === propName,\n );\n\n for (const typeName of violations) {\n if (isTypeInAllowlist(typeName, allowlist)) continue;\n\n context.report({\n node: attribute || reportNode,\n messageId: 'unsafeTypes',\n data: { propName, typeName },\n });\n }\n }\n }\n }\n } else if (ts.isTypeAliasDeclaration(decl) && decl.type) {\n // Type alias might be an inline object type\n if (ts.isTypeLiteralNode(decl.type)) {\n for (const member of decl.type.members) {\n if (ts.isPropertySignature(member) && member.type && member.name) {\n if (!ts.isIdentifier(member.name)) continue;\n\n const propName = member.name.text;\n const violations = checkTypeNodeForDbTypes(member.type, typeChecker);\n\n if (violations.length > 0) {\n // Find the JSX attribute for this prop\n const attribute = childOpeningElement.attributes.find(\n (attr) =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === propName,\n );\n\n for (const typeName of violations) {\n if (isTypeInAllowlist(typeName, allowlist)) continue;\n\n context.report({\n node: attribute || reportNode,\n messageId: 'unsafeTypes',\n data: { propName, typeName },\n });\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n\n // Check for spread attributes\n const attributes = childOpeningElement.attributes;\n for (const attr of attributes) {\n if (attr.type === 'JSXSpreadAttribute') {\n context.report({\n node: reportNode,\n messageId: 'spreadAttributes',\n });\n continue;\n }\n }\n}\n\nexport default ESLintUtils.RuleCreator.withoutDocs<\n [{ allowDbTypes?: (string | RegExp)[] }?],\n 'spreadAttributes' | 'unsafeTypes'\n>({\n meta: {\n type: 'problem',\n messages: {\n spreadAttributes: 'Spread attributes are not allowed in Hydrate children.',\n unsafeTypes:\n 'Prop \"{{propName}}\" uses type \"{{typeName}}\" which is derived from db-types.ts. Use safe-db-types.ts instead.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n allowDbTypes: {\n type: 'array',\n items: {\n type: 'string',\n },\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{}],\n create(context) {\n const options = context.options[0] || {};\n const allowlist = options.allowDbTypes || [];\n return {\n JSXElement(node) {\n const openingElementNameExpression = node.openingElement.name;\n if (openingElementNameExpression.type !== 'JSXIdentifier') return;\n\n const elementName = openingElementNameExpression.name;\n if (elementName !== HYDRATE_COMPONENT_NAME) return;\n\n const child = extractChild(node.children);\n if (!child) return;\n\n // Get the component being rendered\n const childOpeningElement = child.openingElement;\n const childElementName = childOpeningElement.name;\n\n if (childElementName.type !== 'JSXIdentifier') return;\n\n // Get the component's type to inspect its props\n const services = ESLintUtils.getParserServices(context);\n const typeChecker = services.program.getTypeChecker();\n const tsChildNode = services.esTreeNodeToTSNodeMap.get(childElementName);\n const componentSymbol = typeChecker.getSymbolAtLocation(tsChildNode);\n\n if (!componentSymbol) return;\n\n checkComponentProps(\n context,\n typeChecker,\n componentSymbol,\n tsChildNode,\n child,\n child,\n allowlist,\n );\n },\n\n CallExpression(node) {\n // Check for hydrateHtml(<Component ... />, props?) calls\n if (node.callee.type !== 'Identifier' || node.callee.name !== HYDRATE_FUNCTION_NAME) return;\n\n // Should have at least one argument, the first is JSX element.\n if (node.arguments.length === 0) return;\n\n const arg = node.arguments[0];\n if (arg.type !== 'JSXElement') return;\n\n const jsxElement = arg;\n const openingElement = jsxElement.openingElement;\n const elementName = openingElement.name;\n\n if (elementName.type !== 'JSXIdentifier') return;\n\n // Get the component's type to inspect its props\n const services = ESLintUtils.getParserServices(context);\n const typeChecker = services.program.getTypeChecker();\n const tsElementNode = services.esTreeNodeToTSNodeMap.get(elementName);\n const componentSymbol = typeChecker.getSymbolAtLocation(tsElementNode);\n\n if (!componentSymbol) return;\n\n checkComponentProps(\n context,\n typeChecker,\n componentSymbol,\n tsElementNode,\n jsxElement,\n node,\n allowlist,\n );\n },\n };\n },\n});\n"]}
1
+ {"version":3,"file":"safe-db-types.d.ts","sourceRoot":"","sources":["../../src/rules/safe-db-types.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;;;;AAifvD,wBAsGG","sourcesContent":["import { type TSESTree } from '@typescript-eslint/types';\nimport { ESLintUtils } from '@typescript-eslint/utils';\nimport * as ts from 'typescript';\n\nconst HYDRATE_FUNCTION_NAME = 'hydrateHtml';\nconst HYDRATE_COMPONENT_NAME = 'Hydrate';\n\n/**\n * Check if a variable declaration is a Zod schema that uses schemas from db-types.ts\n * For example: const RubricDataSchema = RubricSchema.extend({...})\n */\nfunction checkZodSchemaForDbTypes(\n declaration: ts.VariableDeclaration,\n typeChecker: ts.TypeChecker,\n): string[] {\n const violations: string[] = [];\n\n if (!declaration.initializer) return violations;\n\n // Walk the expression tree to find all identifiers\n const findIdentifiers = (node: ts.Node): void => {\n // Check property access expressions like RubricSchema.extend() or InstanceQuestionSchema.shape\n if (ts.isPropertyAccessExpression(node)) {\n const objectSymbol = typeChecker.getSymbolAtLocation(node.expression);\n if (objectSymbol) {\n const aliasedSymbol =\n objectSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(objectSymbol)\n : objectSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n\n // Check spread elements in object literals (e.g., ...SomeSchema.shape)\n if (ts.isSpreadAssignment(node)) {\n // The expression being spread (e.g., SomeSchema.shape)\n const spreadExpr = node.expression;\n\n // Check if it's a property access (e.g., accessing .shape)\n if (ts.isPropertyAccessExpression(spreadExpr)) {\n const objectSymbol = typeChecker.getSymbolAtLocation(spreadExpr.expression);\n if (objectSymbol) {\n const aliasedSymbol =\n objectSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(objectSymbol)\n : objectSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n } else {\n // The schema is defined locally, check if IT uses db-types\n for (const decl of decls) {\n if (ts.isVariableDeclaration(decl)) {\n // Recursively check the local schema\n const nestedViolations = checkZodSchemaForDbTypes(decl, typeChecker);\n violations.push(...nestedViolations);\n }\n }\n }\n }\n }\n }\n // Also check if the spread is a direct identifier (e.g., ...someObject)\n else if (ts.isIdentifier(spreadExpr)) {\n const spreadSymbol = typeChecker.getSymbolAtLocation(spreadExpr);\n if (spreadSymbol) {\n const aliasedSymbol =\n spreadSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(spreadSymbol)\n : spreadSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n }\n\n // Check call expressions for arguments\n if (ts.isCallExpression(node)) {\n for (const arg of node.arguments) {\n // Check if argument is an identifier (e.g., RubricItemSchema)\n if (ts.isIdentifier(arg)) {\n const argSymbol = typeChecker.getSymbolAtLocation(arg);\n if (argSymbol) {\n const aliasedSymbol =\n argSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(argSymbol)\n : argSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n }\n }\n\n // Check object literal property assignments (e.g., { rubric: RubricSchema })\n if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.initializer)) {\n const identifier = node.initializer;\n const symbol = typeChecker.getSymbolAtLocation(identifier);\n if (symbol) {\n const aliasedSymbol =\n symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n\n ts.forEachChild(node, findIdentifiers);\n };\n\n findIdentifiers(declaration.initializer);\n return violations;\n}\n\n/**\n * Check if a type node is z.infer<typeof SchemaName> and if SchemaName uses db-types\n * Returns the names of db-types that the schema depends on\n */\nfunction checkForZodInferPattern(typeNode: ts.TypeNode, typeChecker: ts.TypeChecker): string[] {\n // Check if this is a type reference with type arguments\n if (!ts.isTypeReferenceNode(typeNode)) return [];\n\n // Check if the type reference is named (e.g., \"infer\" from z.infer)\n const typeName = typeNode.typeName;\n if (!ts.isQualifiedName(typeName)) return [];\n\n // Check if it's z.infer or similar pattern\n // The pattern is: z.infer<typeof SchemaName>\n const typeArgs = typeNode.typeArguments;\n if (!typeArgs || typeArgs.length !== 1) return [];\n\n const typeArg = typeArgs[0];\n\n // Check if the type argument is a typeof expression\n if (!ts.isTypeQueryNode(typeArg)) return [];\n\n // Get the schema name from typeof X\n const exprName = typeArg.exprName;\n if (!ts.isIdentifier(exprName)) return [];\n\n // Now find the schema variable declaration\n const schemaSymbol = typeChecker.getSymbolAtLocation(exprName);\n if (!schemaSymbol) return [];\n\n // Check if this is an imported symbol (alias) - follow it to the original\n const symbolToCheck =\n schemaSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(schemaSymbol)\n : schemaSymbol;\n\n const schemaDecls = symbolToCheck.getDeclarations();\n if (!schemaDecls || schemaDecls.length === 0) return [];\n\n // Check if it's a variable declaration\n for (const decl of schemaDecls) {\n if (ts.isVariableDeclaration(decl)) {\n // If the schema is defined in safe-db-types.ts, it's safe by definition\n const sourceFile = decl.getSourceFile();\n if (sourceFile.fileName.endsWith('/safe-db-types.ts')) {\n return [];\n }\n\n // Use our existing helper to check if the schema uses db-types\n return checkZodSchemaForDbTypes(decl, typeChecker);\n }\n }\n\n return [];\n}\n\nfunction extractChild(children: TSESTree.JSXChild[]): TSESTree.JSXElement | null {\n const nonWhitespaceChildren = children.filter((child) => {\n if (child.type === 'JSXText') {\n return child.value.trim().length > 0;\n }\n return true;\n });\n\n if (nonWhitespaceChildren.length !== 1 || nonWhitespaceChildren[0].type !== 'JSXElement') {\n return null;\n }\n\n return nonWhitespaceChildren[0];\n}\n\n/**\n * Check if a TypeScript type node references a type from db-types.ts\n * This checks the actual source code type annotation, not the resolved type.\n * Follows type aliases (imports) to their original declaration.\n * Returns all unsafe type names found.\n */\nfunction checkTypeNodeForDbTypes(\n typeNode: ts.TypeNode,\n typeChecker: ts.TypeChecker,\n visited = new Set<ts.TypeNode>(),\n): string[] {\n if (visited.has(typeNode)) return [];\n visited.add(typeNode);\n\n const violations: string[] = [];\n\n // Check type references (e.g., User, Course, AuthnProvider)\n if (ts.isTypeReferenceNode(typeNode)) {\n const typeName = typeNode.typeName;\n const symbol = typeChecker.getSymbolAtLocation(typeName);\n\n if (symbol) {\n // Check if this is an imported symbol (alias) - follow it to the original\n const symbolToCheck =\n symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;\n\n const declarations = symbolToCheck.getDeclarations();\n if (declarations && declarations.length > 0) {\n for (const decl of declarations) {\n const sourceFile = decl.getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n // Found a type from db-types.ts!\n violations.push(symbolToCheck.getName());\n } else {\n // If it's a type alias or interface defined locally, check its properties\n if (ts.isTypeAliasDeclaration(decl) && decl.type) {\n // Special case: Check if this is z.infer<typeof XxxSchema>\n const zodSchemaViolations = checkForZodInferPattern(decl.type, typeChecker);\n violations.push(...zodSchemaViolations);\n\n // Also check the type itself\n const nestedViolations = checkTypeNodeForDbTypes(decl.type, typeChecker, visited);\n violations.push(...nestedViolations);\n } else if (ts.isInterfaceDeclaration(decl)) {\n // Check interface members\n for (const member of decl.members) {\n if (ts.isPropertySignature(member) && member.type) {\n const nestedViolations = checkTypeNodeForDbTypes(\n member.type,\n typeChecker,\n visited,\n );\n violations.push(...nestedViolations);\n }\n }\n }\n }\n }\n }\n }\n\n // Check type arguments (e.g., Array<User>, Promise<Course>)\n if (typeNode.typeArguments) {\n for (const typeArg of typeNode.typeArguments) {\n const nestedViolations = checkTypeNodeForDbTypes(typeArg, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n }\n\n // Check array types (e.g., User[])\n if (ts.isArrayTypeNode(typeNode)) {\n const nestedViolations = checkTypeNodeForDbTypes(typeNode.elementType, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n\n // Check union types (e.g., User | null)\n if (ts.isUnionTypeNode(typeNode)) {\n for (const type of typeNode.types) {\n const nestedViolations = checkTypeNodeForDbTypes(type, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n\n // Check intersection types (e.g., User & { extra: string })\n if (ts.isIntersectionTypeNode(typeNode)) {\n for (const type of typeNode.types) {\n const nestedViolations = checkTypeNodeForDbTypes(type, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n\n // Check object type literals and their properties\n if (ts.isTypeLiteralNode(typeNode)) {\n for (const member of typeNode.members) {\n if (ts.isPropertySignature(member) && member.type) {\n const nestedViolations = checkTypeNodeForDbTypes(member.type, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n }\n\n // Check indexed access types (e.g., User['name'])\n // These are safe! We're only extracting a specific property, not passing the whole object.\n // So we DON'T recurse into the object type for indexed access.\n if (ts.isIndexedAccessTypeNode(typeNode)) {\n // Do not check - indexed access is safe\n }\n\n return violations;\n}\n\n/**\n * Check if a type name is in the allowlist of safe types\n */\nfunction isTypeInAllowlist(typeName: string, allowlist: (string | RegExp)[]): boolean {\n return allowlist.some((pattern) => {\n if (typeof pattern === 'string') {\n return typeName === pattern;\n }\n return pattern.test(typeName);\n });\n}\n\n/**\n * Check the props of a component for unsafe types from db-types.ts\n */\nfunction checkComponentProps({\n context,\n typeChecker,\n componentSymbol,\n tsComponentNode,\n jsxElement,\n reportNode,\n allowlist,\n}: {\n context: ReturnType<typeof ESLintUtils.RuleCreator.withoutDocs>['create'] extends (\n context: infer C,\n ) => any\n ? C\n : never;\n typeChecker: ts.TypeChecker;\n componentSymbol: ts.Symbol;\n tsComponentNode: ts.Node;\n jsxElement: TSESTree.JSXElement;\n reportNode: TSESTree.Node;\n allowlist: (string | RegExp)[];\n}): void {\n const childOpeningElement = jsxElement.openingElement;\n\n // Get the component's type (function or class component)\n const componentType = typeChecker.getTypeOfSymbolAtLocation(componentSymbol, tsComponentNode);\n const signatures = componentType.getCallSignatures();\n\n if (signatures.length === 0) return;\n\n // Get the first parameter (props) of the component function\n const propsParam = signatures[0].getParameters()[0];\n if (!propsParam) return;\n\n const propsDeclaration = propsParam.valueDeclaration;\n\n if (!propsDeclaration || !ts.isParameter(propsDeclaration)) return;\n\n // Get the type annotation node from the props parameter\n const propsTypeNode = propsDeclaration.type;\n if (!propsTypeNode) return;\n\n // Check each property in the props type\n if (ts.isTypeLiteralNode(propsTypeNode)) {\n // Inline props object: { foo: string; bar: number }\n for (const member of propsTypeNode.members) {\n if (ts.isPropertySignature(member) && member.type && member.name) {\n if (!ts.isIdentifier(member.name)) continue;\n\n const propName = member.name.text;\n const violations = checkTypeNodeForDbTypes(member.type, typeChecker);\n\n if (violations.length > 0) {\n // Find the JSX attribute for this prop\n const attribute = childOpeningElement.attributes.find(\n (attr) =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === propName,\n );\n\n for (const typeName of violations) {\n if (isTypeInAllowlist(typeName, allowlist)) continue;\n\n context.report({\n node: attribute || reportNode,\n messageId: 'unsafeTypes',\n data: { propName, typeName },\n });\n }\n }\n }\n }\n } else if (ts.isTypeReferenceNode(propsTypeNode)) {\n // Props is a type reference (e.g., interface or type alias)\n const symbol = typeChecker.getSymbolAtLocation(propsTypeNode.typeName);\n if (symbol) {\n const resolvedSymbol =\n symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;\n const declarations = resolvedSymbol.getDeclarations();\n\n if (declarations && declarations.length > 0) {\n for (const decl of declarations) {\n if (ts.isInterfaceDeclaration(decl) || ts.isTypeLiteralNode(decl)) {\n const members = ts.isInterfaceDeclaration(decl) ? decl.members : decl.members;\n\n for (const member of members) {\n if (ts.isPropertySignature(member) && member.type && member.name) {\n if (!ts.isIdentifier(member.name)) continue;\n\n const propName = member.name.text;\n const violations = checkTypeNodeForDbTypes(member.type, typeChecker);\n\n if (violations.length > 0) {\n // Find the JSX attribute for this prop\n const attribute = childOpeningElement.attributes.find(\n (attr) =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === propName,\n );\n\n for (const typeName of violations) {\n if (isTypeInAllowlist(typeName, allowlist)) continue;\n\n context.report({\n node: attribute || reportNode,\n messageId: 'unsafeTypes',\n data: { propName, typeName },\n });\n }\n }\n }\n }\n } else if (ts.isTypeAliasDeclaration(decl) && decl.type) {\n // Type alias might be an inline object type\n if (ts.isTypeLiteralNode(decl.type)) {\n for (const member of decl.type.members) {\n if (ts.isPropertySignature(member) && member.type && member.name) {\n if (!ts.isIdentifier(member.name)) continue;\n\n const propName = member.name.text;\n const violations = checkTypeNodeForDbTypes(member.type, typeChecker);\n\n if (violations.length > 0) {\n // Find the JSX attribute for this prop\n const attribute = childOpeningElement.attributes.find(\n (attr) =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === propName,\n );\n\n for (const typeName of violations) {\n if (isTypeInAllowlist(typeName, allowlist)) continue;\n\n context.report({\n node: attribute || reportNode,\n messageId: 'unsafeTypes',\n data: { propName, typeName },\n });\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n\n // Check for spread attributes\n const attributes = childOpeningElement.attributes;\n for (const attr of attributes) {\n if (attr.type === 'JSXSpreadAttribute') {\n context.report({\n node: reportNode,\n messageId: 'spreadAttributes',\n });\n continue;\n }\n }\n}\n\nexport default ESLintUtils.RuleCreator.withoutDocs<\n [{ allowDbTypes?: (string | RegExp)[] }?],\n 'spreadAttributes' | 'unsafeTypes'\n>({\n meta: {\n type: 'problem',\n messages: {\n spreadAttributes: 'Spread attributes are not allowed in Hydrate children.',\n unsafeTypes:\n 'Prop \"{{propName}}\" uses type \"{{typeName}}\" which is derived from db-types.ts. Use safe-db-types.ts instead.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n allowDbTypes: {\n type: 'array',\n items: {\n type: 'string',\n },\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{}],\n create(context) {\n const options = context.options[0] || {};\n const allowlist = options.allowDbTypes || [];\n return {\n JSXElement(node) {\n const openingElementNameExpression = node.openingElement.name;\n if (openingElementNameExpression.type !== 'JSXIdentifier') return;\n\n const elementName = openingElementNameExpression.name;\n if (elementName !== HYDRATE_COMPONENT_NAME) return;\n\n const child = extractChild(node.children);\n if (!child) return;\n\n // Get the component being rendered\n const childOpeningElement = child.openingElement;\n const childElementName = childOpeningElement.name;\n\n if (childElementName.type !== 'JSXIdentifier') return;\n\n // Get the component's type to inspect its props\n const services = ESLintUtils.getParserServices(context);\n const typeChecker = services.program.getTypeChecker();\n const tsChildNode = services.esTreeNodeToTSNodeMap.get(childElementName);\n const componentSymbol = typeChecker.getSymbolAtLocation(tsChildNode);\n\n if (!componentSymbol) return;\n\n checkComponentProps({\n context,\n typeChecker,\n componentSymbol,\n tsComponentNode: tsChildNode,\n jsxElement: child,\n reportNode: child,\n allowlist,\n });\n },\n\n CallExpression(node) {\n // Check for hydrateHtml(<Component ... />, props?) calls\n if (node.callee.type !== 'Identifier' || node.callee.name !== HYDRATE_FUNCTION_NAME) return;\n\n // Should have at least one argument, the first is JSX element.\n if (node.arguments.length === 0) return;\n\n const arg = node.arguments[0];\n if (arg.type !== 'JSXElement') return;\n\n const jsxElement = arg;\n const openingElement = jsxElement.openingElement;\n const elementName = openingElement.name;\n\n if (elementName.type !== 'JSXIdentifier') return;\n\n // Get the component's type to inspect its props\n const services = ESLintUtils.getParserServices(context);\n const typeChecker = services.program.getTypeChecker();\n const tsElementNode = services.esTreeNodeToTSNodeMap.get(elementName);\n const componentSymbol = typeChecker.getSymbolAtLocation(tsElementNode);\n\n if (!componentSymbol) return;\n\n checkComponentProps({\n context,\n typeChecker,\n componentSymbol,\n tsComponentNode: tsElementNode,\n jsxElement,\n reportNode: node,\n allowlist,\n });\n },\n };\n },\n});\n"]}
@@ -320,7 +320,7 @@ function isTypeInAllowlist(typeName, allowlist) {
320
320
  /**
321
321
  * Check the props of a component for unsafe types from db-types.ts
322
322
  */
323
- function checkComponentProps(context, typeChecker, componentSymbol, tsComponentNode, jsxElement, reportNode, allowlist) {
323
+ function checkComponentProps({ context, typeChecker, componentSymbol, tsComponentNode, jsxElement, reportNode, allowlist, }) {
324
324
  const childOpeningElement = jsxElement.openingElement;
325
325
  // Get the component's type (function or class component)
326
326
  const componentType = typeChecker.getTypeOfSymbolAtLocation(componentSymbol, tsComponentNode);
@@ -492,7 +492,15 @@ exports.default = utils_1.ESLintUtils.RuleCreator.withoutDocs({
492
492
  const componentSymbol = typeChecker.getSymbolAtLocation(tsChildNode);
493
493
  if (!componentSymbol)
494
494
  return;
495
- checkComponentProps(context, typeChecker, componentSymbol, tsChildNode, child, child, allowlist);
495
+ checkComponentProps({
496
+ context,
497
+ typeChecker,
498
+ componentSymbol,
499
+ tsComponentNode: tsChildNode,
500
+ jsxElement: child,
501
+ reportNode: child,
502
+ allowlist,
503
+ });
496
504
  },
497
505
  CallExpression(node) {
498
506
  // Check for hydrateHtml(<Component ... />, props?) calls
@@ -516,7 +524,15 @@ exports.default = utils_1.ESLintUtils.RuleCreator.withoutDocs({
516
524
  const componentSymbol = typeChecker.getSymbolAtLocation(tsElementNode);
517
525
  if (!componentSymbol)
518
526
  return;
519
- checkComponentProps(context, typeChecker, componentSymbol, tsElementNode, jsxElement, node, allowlist);
527
+ checkComponentProps({
528
+ context,
529
+ typeChecker,
530
+ componentSymbol,
531
+ tsComponentNode: tsElementNode,
532
+ jsxElement,
533
+ reportNode: node,
534
+ allowlist,
535
+ });
520
536
  },
521
537
  };
522
538
  },
@@ -1 +1 @@
1
- {"version":3,"file":"safe-db-types.js","sourceRoot":"","sources":["../../src/rules/safe-db-types.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,oDAAuD;AACvD,MAAY,EAAE,uCAAmB;AAEjC,MAAM,qBAAqB,GAAG,aAAa,CAAC;AAC5C,MAAM,sBAAsB,GAAG,SAAS,CAAC;AAEzC;;;GAGG;AACH,SAAS,wBAAwB,CAC/B,WAAmC,EACnC,WAA2B,EACjB;IACV,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,IAAI,CAAC,WAAW,CAAC,WAAW;QAAE,OAAO,UAAU,CAAC;IAEhD,mDAAmD;IACnD,MAAM,eAAe,GAAG,CAAC,IAAa,EAAQ,EAAE,CAAC;QAC/C,+FAA+F;QAC/F,IAAI,EAAE,CAAC,0BAA0B,CAAC,IAAI,CAAC,EAAE,CAAC;YACxC,MAAM,YAAY,GAAG,WAAW,CAAC,mBAAmB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACtE,IAAI,YAAY,EAAE,CAAC;gBACjB,MAAM,aAAa,GACjB,YAAY,CAAC,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK;oBACvC,CAAC,CAAC,WAAW,CAAC,gBAAgB,CAAC,YAAY,CAAC;oBAC5C,CAAC,CAAC,YAAY,CAAC;gBACnB,MAAM,KAAK,GAAG,aAAa,CAAC,eAAe,EAAE,CAAC;gBAC9C,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC9B,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;oBAC5C,IAAI,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;wBACjD,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC,CAAC;oBAC3C,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAED,uEAAuE;QACvE,IAAI,EAAE,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;YAChC,uDAAuD;YACvD,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;YAEnC,2DAA2D;YAC3D,IAAI,EAAE,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC9C,MAAM,YAAY,GAAG,WAAW,CAAC,mBAAmB,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;gBAC5E,IAAI,YAAY,EAAE,CAAC;oBACjB,MAAM,aAAa,GACjB,YAAY,CAAC,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK;wBACvC,CAAC,CAAC,WAAW,CAAC,gBAAgB,CAAC,YAAY,CAAC;wBAC5C,CAAC,CAAC,YAAY,CAAC;oBACnB,MAAM,KAAK,GAAG,aAAa,CAAC,eAAe,EAAE,CAAC;oBAC9C,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBAC9B,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;wBAC5C,IAAI,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;4BACjD,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC,CAAC;wBAC3C,CAAC;6BAAM,CAAC;4BACN,2DAA2D;4BAC3D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gCACzB,IAAI,EAAE,CAAC,qBAAqB,CAAC,IAAI,CAAC,EAAE,CAAC;oCACnC,qCAAqC;oCACrC,MAAM,gBAAgB,GAAG,wBAAwB,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;oCACrE,UAAU,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,CAAC;gCACvC,CAAC;4BACH,CAAC;wBACH,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;YACD,wEAAwE;iBACnE,IAAI,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,CAAC;gBACrC,MAAM,YAAY,GAAG,WAAW,CAAC,mBAAmB,CAAC,UAAU,CAAC,CAAC;gBACjE,IAAI,YAAY,EAAE,CAAC;oBACjB,MAAM,aAAa,GACjB,YAAY,CAAC,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK;wBACvC,CAAC,CAAC,WAAW,CAAC,gBAAgB,CAAC,YAAY,CAAC;wBAC5C,CAAC,CAAC,YAAY,CAAC;oBACnB,MAAM,KAAK,GAAG,aAAa,CAAC,eAAe,EAAE,CAAC;oBAC9C,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBAC9B,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;wBAC5C,IAAI,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;4BACjD,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC,CAAC;wBAC3C,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAED,uCAAuC;QACvC,IAAI,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;gBACjC,8DAA8D;gBAC9D,IAAI,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;oBACzB,MAAM,SAAS,GAAG,WAAW,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC;oBACvD,IAAI,SAAS,EAAE,CAAC;wBACd,MAAM,aAAa,GACjB,SAAS,CAAC,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK;4BACpC,CAAC,CAAC,WAAW,CAAC,gBAAgB,CAAC,SAAS,CAAC;4BACzC,CAAC,CAAC,SAAS,CAAC;wBAChB,MAAM,KAAK,GAAG,aAAa,CAAC,eAAe,EAAE,CAAC;wBAC9C,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;4BAC9B,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;4BAC5C,IAAI,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;gCACjD,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC,CAAC;4BAC3C,CAAC;wBACH,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAED,6EAA6E;QAC7E,IAAI,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;YACvE,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,CAAC;YACpC,MAAM,MAAM,GAAG,WAAW,CAAC,mBAAmB,CAAC,UAAU,CAAC,CAAC;YAC3D,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,aAAa,GACjB,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;gBACtF,MAAM,KAAK,GAAG,aAAa,CAAC,eAAe,EAAE,CAAC;gBAC9C,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC9B,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;oBAC5C,IAAI,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;wBACjD,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC,CAAC;oBAC3C,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAED,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC;IAAA,CACxC,CAAC;IAEF,eAAe,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;IACzC,OAAO,UAAU,CAAC;AAAA,CACnB;AAED;;;GAGG;AACH,SAAS,uBAAuB,CAAC,QAAqB,EAAE,WAA2B,EAAY;IAC7F,wDAAwD;IACxD,IAAI,CAAC,EAAE,CAAC,mBAAmB,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,CAAC;IAEjD,oEAAoE;IACpE,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC;IACnC,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,CAAC;IAE7C,2CAA2C;IAC3C,6CAA6C;IAC7C,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC;IACxC,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAElD,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAE5B,oDAAoD;IACpD,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC;IAE5C,oCAAoC;IACpC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAClC,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,CAAC;IAE1C,2CAA2C;IAC3C,MAAM,YAAY,GAAG,WAAW,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IAC/D,IAAI,CAAC,YAAY;QAAE,OAAO,EAAE,CAAC;IAE7B,0EAA0E;IAC1E,MAAM,aAAa,GACjB,YAAY,CAAC,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK;QACvC,CAAC,CAAC,WAAW,CAAC,gBAAgB,CAAC,YAAY,CAAC;QAC5C,CAAC,CAAC,YAAY,CAAC;IAEnB,MAAM,WAAW,GAAG,aAAa,CAAC,eAAe,EAAE,CAAC;IACpD,IAAI,CAAC,WAAW,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAExD,uCAAuC;IACvC,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,IAAI,EAAE,CAAC,qBAAqB,CAAC,IAAI,CAAC,EAAE,CAAC;YACnC,wEAAwE;YACxE,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;YACxC,IAAI,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,CAAC;gBACtD,OAAO,EAAE,CAAC;YACZ,CAAC;YAED,+DAA+D;YAC/D,OAAO,wBAAwB,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IAED,OAAO,EAAE,CAAC;AAAA,CACX;AAED,SAAS,YAAY,CAAC,QAA6B,EAA8B;IAC/E,MAAM,qBAAqB,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;QACvD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC7B,OAAO,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;QACvC,CAAC;QACD,OAAO,IAAI,CAAC;IAAA,CACb,CAAC,CAAC;IAEH,IAAI,qBAAqB,CAAC,MAAM,KAAK,CAAC,IAAI,qBAAqB,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QACzF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,qBAAqB,CAAC,CAAC,CAAC,CAAC;AAAA,CACjC;AAED;;;;;GAKG;AACH,SAAS,uBAAuB,CAC9B,QAAqB,EACrB,WAA2B,EAC3B,OAAO,GAAG,IAAI,GAAG,EAAe,EACtB;IACV,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAEtB,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,4DAA4D;IAC5D,IAAI,EAAE,CAAC,mBAAmB,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC;QACnC,MAAM,MAAM,GAAG,WAAW,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;QAEzD,IAAI,MAAM,EAAE,CAAC;YACX,0EAA0E;YAC1E,MAAM,aAAa,GACjB,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YAEtF,MAAM,YAAY,GAAG,aAAa,CAAC,eAAe,EAAE,CAAC;YACrD,IAAI,YAAY,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC5C,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;oBAChC,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;oBACxC,IAAI,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;wBACjD,iCAAiC;wBACjC,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC,CAAC;oBAC3C,CAAC;yBAAM,CAAC;wBACN,0EAA0E;wBAC1E,IAAI,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;4BACjD,2DAA2D;4BAC3D,MAAM,mBAAmB,GAAG,uBAAuB,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;4BAC5E,UAAU,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,CAAC;4BAExC,6BAA6B;4BAC7B,MAAM,gBAAgB,GAAG,uBAAuB,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;4BAClF,UAAU,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,CAAC;wBACvC,CAAC;6BAAM,IAAI,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,EAAE,CAAC;4BAC3C,0BAA0B;4BAC1B,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gCAClC,IAAI,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;oCAClD,MAAM,gBAAgB,GAAG,uBAAuB,CAC9C,MAAM,CAAC,IAAI,EACX,WAAW,EACX,OAAO,CACR,CAAC;oCACF,UAAU,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,CAAC;gCACvC,CAAC;4BACH,CAAC;wBACH,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAED,4DAA4D;QAC5D,IAAI,QAAQ,CAAC,aAAa,EAAE,CAAC;YAC3B,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE,CAAC;gBAC7C,MAAM,gBAAgB,GAAG,uBAAuB,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;gBAChF,UAAU,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,CAAC;YACvC,CAAC;QACH,CAAC;IACH,CAAC;IAED,mCAAmC;IACnC,IAAI,EAAE,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjC,MAAM,gBAAgB,GAAG,uBAAuB,CAAC,QAAQ,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;QAC7F,UAAU,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,CAAC;IACvC,CAAC;IAED,wCAAwC;IACxC,IAAI,EAAE,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjC,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;YAClC,MAAM,gBAAgB,GAAG,uBAAuB,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;YAC7E,UAAU,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,4DAA4D;IAC5D,IAAI,EAAE,CAAC,sBAAsB,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxC,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;YAClC,MAAM,gBAAgB,GAAG,uBAAuB,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;YAC7E,UAAU,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,kDAAkD;IAClD,IAAI,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnC,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;YACtC,IAAI,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;gBAClD,MAAM,gBAAgB,GAAG,uBAAuB,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;gBACpF,UAAU,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,CAAC;YACvC,CAAC;QACH,CAAC;IACH,CAAC;IAED,kDAAkD;IAClD,2FAA2F;IAC3F,+DAA+D;IAC/D,IAAI,EAAE,CAAC,uBAAuB,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzC,wCAAwC;IAC1C,CAAC;IAED,OAAO,UAAU,CAAC;AAAA,CACnB;AAED;;GAEG;AACH,SAAS,iBAAiB,CAAC,QAAgB,EAAE,SAA8B,EAAW;IACpF,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;QACjC,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YAChC,OAAO,QAAQ,KAAK,OAAO,CAAC;QAC9B,CAAC;QACD,OAAO,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAAA,CAC/B,CAAC,CAAC;AAAA,CACJ;AAED;;GAEG;AACH,SAAS,mBAAmB,CAC1B,OAIS,EACT,WAA2B,EAC3B,eAA0B,EAC1B,eAAwB,EACxB,UAA+B,EAC/B,UAAyB,EACzB,SAA8B,EACxB;IACN,MAAM,mBAAmB,GAAG,UAAU,CAAC,cAAc,CAAC;IAEtD,yDAAyD;IACzD,MAAM,aAAa,GAAG,WAAW,CAAC,yBAAyB,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC;IAC9F,MAAM,UAAU,GAAG,aAAa,CAAC,iBAAiB,EAAE,CAAC;IAErD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAEpC,4DAA4D;IAC5D,MAAM,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,CAAC;IACpD,IAAI,CAAC,UAAU;QAAE,OAAO;IAExB,MAAM,gBAAgB,GAAG,UAAU,CAAC,gBAAgB,CAAC;IAErD,IAAI,CAAC,gBAAgB,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,gBAAgB,CAAC;QAAE,OAAO;IAEnE,wDAAwD;IACxD,MAAM,aAAa,GAAG,gBAAgB,CAAC,IAAI,CAAC;IAC5C,IAAI,CAAC,aAAa;QAAE,OAAO;IAE3B,wCAAwC;IACxC,IAAI,EAAE,CAAC,iBAAiB,CAAC,aAAa,CAAC,EAAE,CAAC;QACxC,oDAAoD;QACpD,KAAK,MAAM,MAAM,IAAI,aAAa,CAAC,OAAO,EAAE,CAAC;YAC3C,IAAI,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;gBACjE,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC;oBAAE,SAAS;gBAE5C,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;gBAClC,MAAM,UAAU,GAAG,uBAAuB,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;gBAErE,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC1B,uCAAuC;oBACvC,MAAM,SAAS,GAAG,mBAAmB,CAAC,UAAU,CAAC,IAAI,CACnD,CAAC,IAAI,EAAE,EAAE,CACP,IAAI,CAAC,IAAI,KAAK,cAAc;wBAC5B,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,eAAe;wBAClC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,CAC9B,CAAC;oBAEF,KAAK,MAAM,QAAQ,IAAI,UAAU,EAAE,CAAC;wBAClC,IAAI,iBAAiB,CAAC,QAAQ,EAAE,SAAS,CAAC;4BAAE,SAAS;wBAErD,OAAO,CAAC,MAAM,CAAC;4BACb,IAAI,EAAE,SAAS,IAAI,UAAU;4BAC7B,SAAS,EAAE,aAAa;4BACxB,IAAI,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;yBAC7B,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;SAAM,IAAI,EAAE,CAAC,mBAAmB,CAAC,aAAa,CAAC,EAAE,CAAC;QACjD,4DAA4D;QAC5D,MAAM,MAAM,GAAG,WAAW,CAAC,mBAAmB,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QACvE,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,cAAc,GAClB,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YACtF,MAAM,YAAY,GAAG,cAAc,CAAC,eAAe,EAAE,CAAC;YAEtD,IAAI,YAAY,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC5C,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;oBAChC,IAAI,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC;wBAClE,MAAM,OAAO,GAAG,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;wBAE9E,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;4BAC7B,IAAI,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;gCACjE,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC;oCAAE,SAAS;gCAE5C,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;gCAClC,MAAM,UAAU,GAAG,uBAAuB,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;gCAErE,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oCAC1B,uCAAuC;oCACvC,MAAM,SAAS,GAAG,mBAAmB,CAAC,UAAU,CAAC,IAAI,CACnD,CAAC,IAAI,EAAE,EAAE,CACP,IAAI,CAAC,IAAI,KAAK,cAAc;wCAC5B,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,eAAe;wCAClC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,CAC9B,CAAC;oCAEF,KAAK,MAAM,QAAQ,IAAI,UAAU,EAAE,CAAC;wCAClC,IAAI,iBAAiB,CAAC,QAAQ,EAAE,SAAS,CAAC;4CAAE,SAAS;wCAErD,OAAO,CAAC,MAAM,CAAC;4CACb,IAAI,EAAE,SAAS,IAAI,UAAU;4CAC7B,SAAS,EAAE,aAAa;4CACxB,IAAI,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;yCAC7B,CAAC,CAAC;oCACL,CAAC;gCACH,CAAC;4BACH,CAAC;wBACH,CAAC;oBACH,CAAC;yBAAM,IAAI,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;wBACxD,4CAA4C;wBAC5C,IAAI,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;4BACpC,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;gCACvC,IAAI,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;oCACjE,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC;wCAAE,SAAS;oCAE5C,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;oCAClC,MAAM,UAAU,GAAG,uBAAuB,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;oCAErE,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wCAC1B,uCAAuC;wCACvC,MAAM,SAAS,GAAG,mBAAmB,CAAC,UAAU,CAAC,IAAI,CACnD,CAAC,IAAI,EAAE,EAAE,CACP,IAAI,CAAC,IAAI,KAAK,cAAc;4CAC5B,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,eAAe;4CAClC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,CAC9B,CAAC;wCAEF,KAAK,MAAM,QAAQ,IAAI,UAAU,EAAE,CAAC;4CAClC,IAAI,iBAAiB,CAAC,QAAQ,EAAE,SAAS,CAAC;gDAAE,SAAS;4CAErD,OAAO,CAAC,MAAM,CAAC;gDACb,IAAI,EAAE,SAAS,IAAI,UAAU;gDAC7B,SAAS,EAAE,aAAa;gDACxB,IAAI,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;6CAC7B,CAAC,CAAC;wCACL,CAAC;oCACH,CAAC;gCACH,CAAC;4BACH,CAAC;wBACH,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,8BAA8B;IAC9B,MAAM,UAAU,GAAG,mBAAmB,CAAC,UAAU,CAAC;IAClD,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,IAAI,IAAI,CAAC,IAAI,KAAK,oBAAoB,EAAE,CAAC;YACvC,OAAO,CAAC,MAAM,CAAC;gBACb,IAAI,EAAE,UAAU;gBAChB,SAAS,EAAE,kBAAkB;aAC9B,CAAC,CAAC;YACH,SAAS;QACX,CAAC;IACH,CAAC;AAAA,CACF;kBAEc,mBAAW,CAAC,WAAW,CAAC,WAAW,CAGhD;IACA,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE;YACR,gBAAgB,EAAE,wDAAwD;YAC1E,WAAW,EACT,+GAA+G;SAClH;QACD,MAAM,EAAE;YACN;gBACE,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,YAAY,EAAE;wBACZ,IAAI,EAAE,OAAO;wBACb,KAAK,EAAE;4BACL,IAAI,EAAE,QAAQ;yBACf;qBACF;iBACF;gBACD,oBAAoB,EAAE,KAAK;aAC5B;SACF;KACF;IACD,cAAc,EAAE,CAAC,EAAE,CAAC;IACpB,MAAM,CAAC,OAAO,EAAE;QACd,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACzC,MAAM,SAAS,GAAG,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;QAC7C,OAAO;YACL,UAAU,CAAC,IAAI,EAAE;gBACf,MAAM,4BAA4B,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC;gBAC9D,IAAI,4BAA4B,CAAC,IAAI,KAAK,eAAe;oBAAE,OAAO;gBAElE,MAAM,WAAW,GAAG,4BAA4B,CAAC,IAAI,CAAC;gBACtD,IAAI,WAAW,KAAK,sBAAsB;oBAAE,OAAO;gBAEnD,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBAC1C,IAAI,CAAC,KAAK;oBAAE,OAAO;gBAEnB,mCAAmC;gBACnC,MAAM,mBAAmB,GAAG,KAAK,CAAC,cAAc,CAAC;gBACjD,MAAM,gBAAgB,GAAG,mBAAmB,CAAC,IAAI,CAAC;gBAElD,IAAI,gBAAgB,CAAC,IAAI,KAAK,eAAe;oBAAE,OAAO;gBAEtD,gDAAgD;gBAChD,MAAM,QAAQ,GAAG,mBAAW,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;gBACxD,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;gBACtD,MAAM,WAAW,GAAG,QAAQ,CAAC,qBAAqB,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;gBACzE,MAAM,eAAe,GAAG,WAAW,CAAC,mBAAmB,CAAC,WAAW,CAAC,CAAC;gBAErE,IAAI,CAAC,eAAe;oBAAE,OAAO;gBAE7B,mBAAmB,CACjB,OAAO,EACP,WAAW,EACX,eAAe,EACf,WAAW,EACX,KAAK,EACL,KAAK,EACL,SAAS,CACV,CAAC;YAAA,CACH;YAED,cAAc,CAAC,IAAI,EAAE;gBACnB,yDAAyD;gBACzD,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,qBAAqB;oBAAE,OAAO;gBAE5F,+DAA+D;gBAC/D,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC;oBAAE,OAAO;gBAExC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;gBAC9B,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY;oBAAE,OAAO;gBAEtC,MAAM,UAAU,GAAG,GAAG,CAAC;gBACvB,MAAM,cAAc,GAAG,UAAU,CAAC,cAAc,CAAC;gBACjD,MAAM,WAAW,GAAG,cAAc,CAAC,IAAI,CAAC;gBAExC,IAAI,WAAW,CAAC,IAAI,KAAK,eAAe;oBAAE,OAAO;gBAEjD,gDAAgD;gBAChD,MAAM,QAAQ,GAAG,mBAAW,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;gBACxD,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;gBACtD,MAAM,aAAa,GAAG,QAAQ,CAAC,qBAAqB,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;gBACtE,MAAM,eAAe,GAAG,WAAW,CAAC,mBAAmB,CAAC,aAAa,CAAC,CAAC;gBAEvE,IAAI,CAAC,eAAe;oBAAE,OAAO;gBAE7B,mBAAmB,CACjB,OAAO,EACP,WAAW,EACX,eAAe,EACf,aAAa,EACb,UAAU,EACV,IAAI,EACJ,SAAS,CACV,CAAC;YAAA,CACH;SACF,CAAC;IAAA,CACH;CACF,CAAC","sourcesContent":["import { type TSESTree } from '@typescript-eslint/types';\nimport { ESLintUtils } from '@typescript-eslint/utils';\nimport * as ts from 'typescript';\n\nconst HYDRATE_FUNCTION_NAME = 'hydrateHtml';\nconst HYDRATE_COMPONENT_NAME = 'Hydrate';\n\n/**\n * Check if a variable declaration is a Zod schema that uses schemas from db-types.ts\n * For example: const RubricDataSchema = RubricSchema.extend({...})\n */\nfunction checkZodSchemaForDbTypes(\n declaration: ts.VariableDeclaration,\n typeChecker: ts.TypeChecker,\n): string[] {\n const violations: string[] = [];\n\n if (!declaration.initializer) return violations;\n\n // Walk the expression tree to find all identifiers\n const findIdentifiers = (node: ts.Node): void => {\n // Check property access expressions like RubricSchema.extend() or InstanceQuestionSchema.shape\n if (ts.isPropertyAccessExpression(node)) {\n const objectSymbol = typeChecker.getSymbolAtLocation(node.expression);\n if (objectSymbol) {\n const aliasedSymbol =\n objectSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(objectSymbol)\n : objectSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n\n // Check spread elements in object literals (e.g., ...SomeSchema.shape)\n if (ts.isSpreadAssignment(node)) {\n // The expression being spread (e.g., SomeSchema.shape)\n const spreadExpr = node.expression;\n\n // Check if it's a property access (e.g., accessing .shape)\n if (ts.isPropertyAccessExpression(spreadExpr)) {\n const objectSymbol = typeChecker.getSymbolAtLocation(spreadExpr.expression);\n if (objectSymbol) {\n const aliasedSymbol =\n objectSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(objectSymbol)\n : objectSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n } else {\n // The schema is defined locally, check if IT uses db-types\n for (const decl of decls) {\n if (ts.isVariableDeclaration(decl)) {\n // Recursively check the local schema\n const nestedViolations = checkZodSchemaForDbTypes(decl, typeChecker);\n violations.push(...nestedViolations);\n }\n }\n }\n }\n }\n }\n // Also check if the spread is a direct identifier (e.g., ...someObject)\n else if (ts.isIdentifier(spreadExpr)) {\n const spreadSymbol = typeChecker.getSymbolAtLocation(spreadExpr);\n if (spreadSymbol) {\n const aliasedSymbol =\n spreadSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(spreadSymbol)\n : spreadSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n }\n\n // Check call expressions for arguments\n if (ts.isCallExpression(node)) {\n for (const arg of node.arguments) {\n // Check if argument is an identifier (e.g., RubricItemSchema)\n if (ts.isIdentifier(arg)) {\n const argSymbol = typeChecker.getSymbolAtLocation(arg);\n if (argSymbol) {\n const aliasedSymbol =\n argSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(argSymbol)\n : argSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n }\n }\n\n // Check object literal property assignments (e.g., { rubric: RubricSchema })\n if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.initializer)) {\n const identifier = node.initializer;\n const symbol = typeChecker.getSymbolAtLocation(identifier);\n if (symbol) {\n const aliasedSymbol =\n symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n\n ts.forEachChild(node, findIdentifiers);\n };\n\n findIdentifiers(declaration.initializer);\n return violations;\n}\n\n/**\n * Check if a type node is z.infer<typeof SchemaName> and if SchemaName uses db-types\n * Returns the names of db-types that the schema depends on\n */\nfunction checkForZodInferPattern(typeNode: ts.TypeNode, typeChecker: ts.TypeChecker): string[] {\n // Check if this is a type reference with type arguments\n if (!ts.isTypeReferenceNode(typeNode)) return [];\n\n // Check if the type reference is named (e.g., \"infer\" from z.infer)\n const typeName = typeNode.typeName;\n if (!ts.isQualifiedName(typeName)) return [];\n\n // Check if it's z.infer or similar pattern\n // The pattern is: z.infer<typeof SchemaName>\n const typeArgs = typeNode.typeArguments;\n if (!typeArgs || typeArgs.length !== 1) return [];\n\n const typeArg = typeArgs[0];\n\n // Check if the type argument is a typeof expression\n if (!ts.isTypeQueryNode(typeArg)) return [];\n\n // Get the schema name from typeof X\n const exprName = typeArg.exprName;\n if (!ts.isIdentifier(exprName)) return [];\n\n // Now find the schema variable declaration\n const schemaSymbol = typeChecker.getSymbolAtLocation(exprName);\n if (!schemaSymbol) return [];\n\n // Check if this is an imported symbol (alias) - follow it to the original\n const symbolToCheck =\n schemaSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(schemaSymbol)\n : schemaSymbol;\n\n const schemaDecls = symbolToCheck.getDeclarations();\n if (!schemaDecls || schemaDecls.length === 0) return [];\n\n // Check if it's a variable declaration\n for (const decl of schemaDecls) {\n if (ts.isVariableDeclaration(decl)) {\n // If the schema is defined in safe-db-types.ts, it's safe by definition\n const sourceFile = decl.getSourceFile();\n if (sourceFile.fileName.endsWith('/safe-db-types.ts')) {\n return [];\n }\n\n // Use our existing helper to check if the schema uses db-types\n return checkZodSchemaForDbTypes(decl, typeChecker);\n }\n }\n\n return [];\n}\n\nfunction extractChild(children: TSESTree.JSXChild[]): TSESTree.JSXElement | null {\n const nonWhitespaceChildren = children.filter((child) => {\n if (child.type === 'JSXText') {\n return child.value.trim().length > 0;\n }\n return true;\n });\n\n if (nonWhitespaceChildren.length !== 1 || nonWhitespaceChildren[0].type !== 'JSXElement') {\n return null;\n }\n\n return nonWhitespaceChildren[0];\n}\n\n/**\n * Check if a TypeScript type node references a type from db-types.ts\n * This checks the actual source code type annotation, not the resolved type.\n * Follows type aliases (imports) to their original declaration.\n * Returns all unsafe type names found.\n */\nfunction checkTypeNodeForDbTypes(\n typeNode: ts.TypeNode,\n typeChecker: ts.TypeChecker,\n visited = new Set<ts.TypeNode>(),\n): string[] {\n if (visited.has(typeNode)) return [];\n visited.add(typeNode);\n\n const violations: string[] = [];\n\n // Check type references (e.g., User, Course, AuthnProvider)\n if (ts.isTypeReferenceNode(typeNode)) {\n const typeName = typeNode.typeName;\n const symbol = typeChecker.getSymbolAtLocation(typeName);\n\n if (symbol) {\n // Check if this is an imported symbol (alias) - follow it to the original\n const symbolToCheck =\n symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;\n\n const declarations = symbolToCheck.getDeclarations();\n if (declarations && declarations.length > 0) {\n for (const decl of declarations) {\n const sourceFile = decl.getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n // Found a type from db-types.ts!\n violations.push(symbolToCheck.getName());\n } else {\n // If it's a type alias or interface defined locally, check its properties\n if (ts.isTypeAliasDeclaration(decl) && decl.type) {\n // Special case: Check if this is z.infer<typeof XxxSchema>\n const zodSchemaViolations = checkForZodInferPattern(decl.type, typeChecker);\n violations.push(...zodSchemaViolations);\n\n // Also check the type itself\n const nestedViolations = checkTypeNodeForDbTypes(decl.type, typeChecker, visited);\n violations.push(...nestedViolations);\n } else if (ts.isInterfaceDeclaration(decl)) {\n // Check interface members\n for (const member of decl.members) {\n if (ts.isPropertySignature(member) && member.type) {\n const nestedViolations = checkTypeNodeForDbTypes(\n member.type,\n typeChecker,\n visited,\n );\n violations.push(...nestedViolations);\n }\n }\n }\n }\n }\n }\n }\n\n // Check type arguments (e.g., Array<User>, Promise<Course>)\n if (typeNode.typeArguments) {\n for (const typeArg of typeNode.typeArguments) {\n const nestedViolations = checkTypeNodeForDbTypes(typeArg, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n }\n\n // Check array types (e.g., User[])\n if (ts.isArrayTypeNode(typeNode)) {\n const nestedViolations = checkTypeNodeForDbTypes(typeNode.elementType, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n\n // Check union types (e.g., User | null)\n if (ts.isUnionTypeNode(typeNode)) {\n for (const type of typeNode.types) {\n const nestedViolations = checkTypeNodeForDbTypes(type, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n\n // Check intersection types (e.g., User & { extra: string })\n if (ts.isIntersectionTypeNode(typeNode)) {\n for (const type of typeNode.types) {\n const nestedViolations = checkTypeNodeForDbTypes(type, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n\n // Check object type literals and their properties\n if (ts.isTypeLiteralNode(typeNode)) {\n for (const member of typeNode.members) {\n if (ts.isPropertySignature(member) && member.type) {\n const nestedViolations = checkTypeNodeForDbTypes(member.type, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n }\n\n // Check indexed access types (e.g., User['name'])\n // These are safe! We're only extracting a specific property, not passing the whole object.\n // So we DON'T recurse into the object type for indexed access.\n if (ts.isIndexedAccessTypeNode(typeNode)) {\n // Do not check - indexed access is safe\n }\n\n return violations;\n}\n\n/**\n * Check if a type name is in the allowlist of safe types\n */\nfunction isTypeInAllowlist(typeName: string, allowlist: (string | RegExp)[]): boolean {\n return allowlist.some((pattern) => {\n if (typeof pattern === 'string') {\n return typeName === pattern;\n }\n return pattern.test(typeName);\n });\n}\n\n/**\n * Check the props of a component for unsafe types from db-types.ts\n */\nfunction checkComponentProps(\n context: ReturnType<typeof ESLintUtils.RuleCreator.withoutDocs>['create'] extends (\n context: infer C,\n ) => any\n ? C\n : never,\n typeChecker: ts.TypeChecker,\n componentSymbol: ts.Symbol,\n tsComponentNode: ts.Node,\n jsxElement: TSESTree.JSXElement,\n reportNode: TSESTree.Node,\n allowlist: (string | RegExp)[],\n): void {\n const childOpeningElement = jsxElement.openingElement;\n\n // Get the component's type (function or class component)\n const componentType = typeChecker.getTypeOfSymbolAtLocation(componentSymbol, tsComponentNode);\n const signatures = componentType.getCallSignatures();\n\n if (signatures.length === 0) return;\n\n // Get the first parameter (props) of the component function\n const propsParam = signatures[0].getParameters()[0];\n if (!propsParam) return;\n\n const propsDeclaration = propsParam.valueDeclaration;\n\n if (!propsDeclaration || !ts.isParameter(propsDeclaration)) return;\n\n // Get the type annotation node from the props parameter\n const propsTypeNode = propsDeclaration.type;\n if (!propsTypeNode) return;\n\n // Check each property in the props type\n if (ts.isTypeLiteralNode(propsTypeNode)) {\n // Inline props object: { foo: string; bar: number }\n for (const member of propsTypeNode.members) {\n if (ts.isPropertySignature(member) && member.type && member.name) {\n if (!ts.isIdentifier(member.name)) continue;\n\n const propName = member.name.text;\n const violations = checkTypeNodeForDbTypes(member.type, typeChecker);\n\n if (violations.length > 0) {\n // Find the JSX attribute for this prop\n const attribute = childOpeningElement.attributes.find(\n (attr) =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === propName,\n );\n\n for (const typeName of violations) {\n if (isTypeInAllowlist(typeName, allowlist)) continue;\n\n context.report({\n node: attribute || reportNode,\n messageId: 'unsafeTypes',\n data: { propName, typeName },\n });\n }\n }\n }\n }\n } else if (ts.isTypeReferenceNode(propsTypeNode)) {\n // Props is a type reference (e.g., interface or type alias)\n const symbol = typeChecker.getSymbolAtLocation(propsTypeNode.typeName);\n if (symbol) {\n const resolvedSymbol =\n symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;\n const declarations = resolvedSymbol.getDeclarations();\n\n if (declarations && declarations.length > 0) {\n for (const decl of declarations) {\n if (ts.isInterfaceDeclaration(decl) || ts.isTypeLiteralNode(decl)) {\n const members = ts.isInterfaceDeclaration(decl) ? decl.members : decl.members;\n\n for (const member of members) {\n if (ts.isPropertySignature(member) && member.type && member.name) {\n if (!ts.isIdentifier(member.name)) continue;\n\n const propName = member.name.text;\n const violations = checkTypeNodeForDbTypes(member.type, typeChecker);\n\n if (violations.length > 0) {\n // Find the JSX attribute for this prop\n const attribute = childOpeningElement.attributes.find(\n (attr) =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === propName,\n );\n\n for (const typeName of violations) {\n if (isTypeInAllowlist(typeName, allowlist)) continue;\n\n context.report({\n node: attribute || reportNode,\n messageId: 'unsafeTypes',\n data: { propName, typeName },\n });\n }\n }\n }\n }\n } else if (ts.isTypeAliasDeclaration(decl) && decl.type) {\n // Type alias might be an inline object type\n if (ts.isTypeLiteralNode(decl.type)) {\n for (const member of decl.type.members) {\n if (ts.isPropertySignature(member) && member.type && member.name) {\n if (!ts.isIdentifier(member.name)) continue;\n\n const propName = member.name.text;\n const violations = checkTypeNodeForDbTypes(member.type, typeChecker);\n\n if (violations.length > 0) {\n // Find the JSX attribute for this prop\n const attribute = childOpeningElement.attributes.find(\n (attr) =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === propName,\n );\n\n for (const typeName of violations) {\n if (isTypeInAllowlist(typeName, allowlist)) continue;\n\n context.report({\n node: attribute || reportNode,\n messageId: 'unsafeTypes',\n data: { propName, typeName },\n });\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n\n // Check for spread attributes\n const attributes = childOpeningElement.attributes;\n for (const attr of attributes) {\n if (attr.type === 'JSXSpreadAttribute') {\n context.report({\n node: reportNode,\n messageId: 'spreadAttributes',\n });\n continue;\n }\n }\n}\n\nexport default ESLintUtils.RuleCreator.withoutDocs<\n [{ allowDbTypes?: (string | RegExp)[] }?],\n 'spreadAttributes' | 'unsafeTypes'\n>({\n meta: {\n type: 'problem',\n messages: {\n spreadAttributes: 'Spread attributes are not allowed in Hydrate children.',\n unsafeTypes:\n 'Prop \"{{propName}}\" uses type \"{{typeName}}\" which is derived from db-types.ts. Use safe-db-types.ts instead.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n allowDbTypes: {\n type: 'array',\n items: {\n type: 'string',\n },\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{}],\n create(context) {\n const options = context.options[0] || {};\n const allowlist = options.allowDbTypes || [];\n return {\n JSXElement(node) {\n const openingElementNameExpression = node.openingElement.name;\n if (openingElementNameExpression.type !== 'JSXIdentifier') return;\n\n const elementName = openingElementNameExpression.name;\n if (elementName !== HYDRATE_COMPONENT_NAME) return;\n\n const child = extractChild(node.children);\n if (!child) return;\n\n // Get the component being rendered\n const childOpeningElement = child.openingElement;\n const childElementName = childOpeningElement.name;\n\n if (childElementName.type !== 'JSXIdentifier') return;\n\n // Get the component's type to inspect its props\n const services = ESLintUtils.getParserServices(context);\n const typeChecker = services.program.getTypeChecker();\n const tsChildNode = services.esTreeNodeToTSNodeMap.get(childElementName);\n const componentSymbol = typeChecker.getSymbolAtLocation(tsChildNode);\n\n if (!componentSymbol) return;\n\n checkComponentProps(\n context,\n typeChecker,\n componentSymbol,\n tsChildNode,\n child,\n child,\n allowlist,\n );\n },\n\n CallExpression(node) {\n // Check for hydrateHtml(<Component ... />, props?) calls\n if (node.callee.type !== 'Identifier' || node.callee.name !== HYDRATE_FUNCTION_NAME) return;\n\n // Should have at least one argument, the first is JSX element.\n if (node.arguments.length === 0) return;\n\n const arg = node.arguments[0];\n if (arg.type !== 'JSXElement') return;\n\n const jsxElement = arg;\n const openingElement = jsxElement.openingElement;\n const elementName = openingElement.name;\n\n if (elementName.type !== 'JSXIdentifier') return;\n\n // Get the component's type to inspect its props\n const services = ESLintUtils.getParserServices(context);\n const typeChecker = services.program.getTypeChecker();\n const tsElementNode = services.esTreeNodeToTSNodeMap.get(elementName);\n const componentSymbol = typeChecker.getSymbolAtLocation(tsElementNode);\n\n if (!componentSymbol) return;\n\n checkComponentProps(\n context,\n typeChecker,\n componentSymbol,\n tsElementNode,\n jsxElement,\n node,\n allowlist,\n );\n },\n };\n },\n});\n"]}
1
+ {"version":3,"file":"safe-db-types.js","sourceRoot":"","sources":["../../src/rules/safe-db-types.ts"],"names":[],"mappings":"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","sourcesContent":["import { type TSESTree } from '@typescript-eslint/types';\nimport { ESLintUtils } from '@typescript-eslint/utils';\nimport * as ts from 'typescript';\n\nconst HYDRATE_FUNCTION_NAME = 'hydrateHtml';\nconst HYDRATE_COMPONENT_NAME = 'Hydrate';\n\n/**\n * Check if a variable declaration is a Zod schema that uses schemas from db-types.ts\n * For example: const RubricDataSchema = RubricSchema.extend({...})\n */\nfunction checkZodSchemaForDbTypes(\n declaration: ts.VariableDeclaration,\n typeChecker: ts.TypeChecker,\n): string[] {\n const violations: string[] = [];\n\n if (!declaration.initializer) return violations;\n\n // Walk the expression tree to find all identifiers\n const findIdentifiers = (node: ts.Node): void => {\n // Check property access expressions like RubricSchema.extend() or InstanceQuestionSchema.shape\n if (ts.isPropertyAccessExpression(node)) {\n const objectSymbol = typeChecker.getSymbolAtLocation(node.expression);\n if (objectSymbol) {\n const aliasedSymbol =\n objectSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(objectSymbol)\n : objectSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n\n // Check spread elements in object literals (e.g., ...SomeSchema.shape)\n if (ts.isSpreadAssignment(node)) {\n // The expression being spread (e.g., SomeSchema.shape)\n const spreadExpr = node.expression;\n\n // Check if it's a property access (e.g., accessing .shape)\n if (ts.isPropertyAccessExpression(spreadExpr)) {\n const objectSymbol = typeChecker.getSymbolAtLocation(spreadExpr.expression);\n if (objectSymbol) {\n const aliasedSymbol =\n objectSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(objectSymbol)\n : objectSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n } else {\n // The schema is defined locally, check if IT uses db-types\n for (const decl of decls) {\n if (ts.isVariableDeclaration(decl)) {\n // Recursively check the local schema\n const nestedViolations = checkZodSchemaForDbTypes(decl, typeChecker);\n violations.push(...nestedViolations);\n }\n }\n }\n }\n }\n }\n // Also check if the spread is a direct identifier (e.g., ...someObject)\n else if (ts.isIdentifier(spreadExpr)) {\n const spreadSymbol = typeChecker.getSymbolAtLocation(spreadExpr);\n if (spreadSymbol) {\n const aliasedSymbol =\n spreadSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(spreadSymbol)\n : spreadSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n }\n\n // Check call expressions for arguments\n if (ts.isCallExpression(node)) {\n for (const arg of node.arguments) {\n // Check if argument is an identifier (e.g., RubricItemSchema)\n if (ts.isIdentifier(arg)) {\n const argSymbol = typeChecker.getSymbolAtLocation(arg);\n if (argSymbol) {\n const aliasedSymbol =\n argSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(argSymbol)\n : argSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n }\n }\n\n // Check object literal property assignments (e.g., { rubric: RubricSchema })\n if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.initializer)) {\n const identifier = node.initializer;\n const symbol = typeChecker.getSymbolAtLocation(identifier);\n if (symbol) {\n const aliasedSymbol =\n symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n\n ts.forEachChild(node, findIdentifiers);\n };\n\n findIdentifiers(declaration.initializer);\n return violations;\n}\n\n/**\n * Check if a type node is z.infer<typeof SchemaName> and if SchemaName uses db-types\n * Returns the names of db-types that the schema depends on\n */\nfunction checkForZodInferPattern(typeNode: ts.TypeNode, typeChecker: ts.TypeChecker): string[] {\n // Check if this is a type reference with type arguments\n if (!ts.isTypeReferenceNode(typeNode)) return [];\n\n // Check if the type reference is named (e.g., \"infer\" from z.infer)\n const typeName = typeNode.typeName;\n if (!ts.isQualifiedName(typeName)) return [];\n\n // Check if it's z.infer or similar pattern\n // The pattern is: z.infer<typeof SchemaName>\n const typeArgs = typeNode.typeArguments;\n if (!typeArgs || typeArgs.length !== 1) return [];\n\n const typeArg = typeArgs[0];\n\n // Check if the type argument is a typeof expression\n if (!ts.isTypeQueryNode(typeArg)) return [];\n\n // Get the schema name from typeof X\n const exprName = typeArg.exprName;\n if (!ts.isIdentifier(exprName)) return [];\n\n // Now find the schema variable declaration\n const schemaSymbol = typeChecker.getSymbolAtLocation(exprName);\n if (!schemaSymbol) return [];\n\n // Check if this is an imported symbol (alias) - follow it to the original\n const symbolToCheck =\n schemaSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(schemaSymbol)\n : schemaSymbol;\n\n const schemaDecls = symbolToCheck.getDeclarations();\n if (!schemaDecls || schemaDecls.length === 0) return [];\n\n // Check if it's a variable declaration\n for (const decl of schemaDecls) {\n if (ts.isVariableDeclaration(decl)) {\n // If the schema is defined in safe-db-types.ts, it's safe by definition\n const sourceFile = decl.getSourceFile();\n if (sourceFile.fileName.endsWith('/safe-db-types.ts')) {\n return [];\n }\n\n // Use our existing helper to check if the schema uses db-types\n return checkZodSchemaForDbTypes(decl, typeChecker);\n }\n }\n\n return [];\n}\n\nfunction extractChild(children: TSESTree.JSXChild[]): TSESTree.JSXElement | null {\n const nonWhitespaceChildren = children.filter((child) => {\n if (child.type === 'JSXText') {\n return child.value.trim().length > 0;\n }\n return true;\n });\n\n if (nonWhitespaceChildren.length !== 1 || nonWhitespaceChildren[0].type !== 'JSXElement') {\n return null;\n }\n\n return nonWhitespaceChildren[0];\n}\n\n/**\n * Check if a TypeScript type node references a type from db-types.ts\n * This checks the actual source code type annotation, not the resolved type.\n * Follows type aliases (imports) to their original declaration.\n * Returns all unsafe type names found.\n */\nfunction checkTypeNodeForDbTypes(\n typeNode: ts.TypeNode,\n typeChecker: ts.TypeChecker,\n visited = new Set<ts.TypeNode>(),\n): string[] {\n if (visited.has(typeNode)) return [];\n visited.add(typeNode);\n\n const violations: string[] = [];\n\n // Check type references (e.g., User, Course, AuthnProvider)\n if (ts.isTypeReferenceNode(typeNode)) {\n const typeName = typeNode.typeName;\n const symbol = typeChecker.getSymbolAtLocation(typeName);\n\n if (symbol) {\n // Check if this is an imported symbol (alias) - follow it to the original\n const symbolToCheck =\n symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;\n\n const declarations = symbolToCheck.getDeclarations();\n if (declarations && declarations.length > 0) {\n for (const decl of declarations) {\n const sourceFile = decl.getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n // Found a type from db-types.ts!\n violations.push(symbolToCheck.getName());\n } else {\n // If it's a type alias or interface defined locally, check its properties\n if (ts.isTypeAliasDeclaration(decl) && decl.type) {\n // Special case: Check if this is z.infer<typeof XxxSchema>\n const zodSchemaViolations = checkForZodInferPattern(decl.type, typeChecker);\n violations.push(...zodSchemaViolations);\n\n // Also check the type itself\n const nestedViolations = checkTypeNodeForDbTypes(decl.type, typeChecker, visited);\n violations.push(...nestedViolations);\n } else if (ts.isInterfaceDeclaration(decl)) {\n // Check interface members\n for (const member of decl.members) {\n if (ts.isPropertySignature(member) && member.type) {\n const nestedViolations = checkTypeNodeForDbTypes(\n member.type,\n typeChecker,\n visited,\n );\n violations.push(...nestedViolations);\n }\n }\n }\n }\n }\n }\n }\n\n // Check type arguments (e.g., Array<User>, Promise<Course>)\n if (typeNode.typeArguments) {\n for (const typeArg of typeNode.typeArguments) {\n const nestedViolations = checkTypeNodeForDbTypes(typeArg, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n }\n\n // Check array types (e.g., User[])\n if (ts.isArrayTypeNode(typeNode)) {\n const nestedViolations = checkTypeNodeForDbTypes(typeNode.elementType, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n\n // Check union types (e.g., User | null)\n if (ts.isUnionTypeNode(typeNode)) {\n for (const type of typeNode.types) {\n const nestedViolations = checkTypeNodeForDbTypes(type, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n\n // Check intersection types (e.g., User & { extra: string })\n if (ts.isIntersectionTypeNode(typeNode)) {\n for (const type of typeNode.types) {\n const nestedViolations = checkTypeNodeForDbTypes(type, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n\n // Check object type literals and their properties\n if (ts.isTypeLiteralNode(typeNode)) {\n for (const member of typeNode.members) {\n if (ts.isPropertySignature(member) && member.type) {\n const nestedViolations = checkTypeNodeForDbTypes(member.type, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n }\n\n // Check indexed access types (e.g., User['name'])\n // These are safe! We're only extracting a specific property, not passing the whole object.\n // So we DON'T recurse into the object type for indexed access.\n if (ts.isIndexedAccessTypeNode(typeNode)) {\n // Do not check - indexed access is safe\n }\n\n return violations;\n}\n\n/**\n * Check if a type name is in the allowlist of safe types\n */\nfunction isTypeInAllowlist(typeName: string, allowlist: (string | RegExp)[]): boolean {\n return allowlist.some((pattern) => {\n if (typeof pattern === 'string') {\n return typeName === pattern;\n }\n return pattern.test(typeName);\n });\n}\n\n/**\n * Check the props of a component for unsafe types from db-types.ts\n */\nfunction checkComponentProps({\n context,\n typeChecker,\n componentSymbol,\n tsComponentNode,\n jsxElement,\n reportNode,\n allowlist,\n}: {\n context: ReturnType<typeof ESLintUtils.RuleCreator.withoutDocs>['create'] extends (\n context: infer C,\n ) => any\n ? C\n : never;\n typeChecker: ts.TypeChecker;\n componentSymbol: ts.Symbol;\n tsComponentNode: ts.Node;\n jsxElement: TSESTree.JSXElement;\n reportNode: TSESTree.Node;\n allowlist: (string | RegExp)[];\n}): void {\n const childOpeningElement = jsxElement.openingElement;\n\n // Get the component's type (function or class component)\n const componentType = typeChecker.getTypeOfSymbolAtLocation(componentSymbol, tsComponentNode);\n const signatures = componentType.getCallSignatures();\n\n if (signatures.length === 0) return;\n\n // Get the first parameter (props) of the component function\n const propsParam = signatures[0].getParameters()[0];\n if (!propsParam) return;\n\n const propsDeclaration = propsParam.valueDeclaration;\n\n if (!propsDeclaration || !ts.isParameter(propsDeclaration)) return;\n\n // Get the type annotation node from the props parameter\n const propsTypeNode = propsDeclaration.type;\n if (!propsTypeNode) return;\n\n // Check each property in the props type\n if (ts.isTypeLiteralNode(propsTypeNode)) {\n // Inline props object: { foo: string; bar: number }\n for (const member of propsTypeNode.members) {\n if (ts.isPropertySignature(member) && member.type && member.name) {\n if (!ts.isIdentifier(member.name)) continue;\n\n const propName = member.name.text;\n const violations = checkTypeNodeForDbTypes(member.type, typeChecker);\n\n if (violations.length > 0) {\n // Find the JSX attribute for this prop\n const attribute = childOpeningElement.attributes.find(\n (attr) =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === propName,\n );\n\n for (const typeName of violations) {\n if (isTypeInAllowlist(typeName, allowlist)) continue;\n\n context.report({\n node: attribute || reportNode,\n messageId: 'unsafeTypes',\n data: { propName, typeName },\n });\n }\n }\n }\n }\n } else if (ts.isTypeReferenceNode(propsTypeNode)) {\n // Props is a type reference (e.g., interface or type alias)\n const symbol = typeChecker.getSymbolAtLocation(propsTypeNode.typeName);\n if (symbol) {\n const resolvedSymbol =\n symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;\n const declarations = resolvedSymbol.getDeclarations();\n\n if (declarations && declarations.length > 0) {\n for (const decl of declarations) {\n if (ts.isInterfaceDeclaration(decl) || ts.isTypeLiteralNode(decl)) {\n const members = ts.isInterfaceDeclaration(decl) ? decl.members : decl.members;\n\n for (const member of members) {\n if (ts.isPropertySignature(member) && member.type && member.name) {\n if (!ts.isIdentifier(member.name)) continue;\n\n const propName = member.name.text;\n const violations = checkTypeNodeForDbTypes(member.type, typeChecker);\n\n if (violations.length > 0) {\n // Find the JSX attribute for this prop\n const attribute = childOpeningElement.attributes.find(\n (attr) =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === propName,\n );\n\n for (const typeName of violations) {\n if (isTypeInAllowlist(typeName, allowlist)) continue;\n\n context.report({\n node: attribute || reportNode,\n messageId: 'unsafeTypes',\n data: { propName, typeName },\n });\n }\n }\n }\n }\n } else if (ts.isTypeAliasDeclaration(decl) && decl.type) {\n // Type alias might be an inline object type\n if (ts.isTypeLiteralNode(decl.type)) {\n for (const member of decl.type.members) {\n if (ts.isPropertySignature(member) && member.type && member.name) {\n if (!ts.isIdentifier(member.name)) continue;\n\n const propName = member.name.text;\n const violations = checkTypeNodeForDbTypes(member.type, typeChecker);\n\n if (violations.length > 0) {\n // Find the JSX attribute for this prop\n const attribute = childOpeningElement.attributes.find(\n (attr) =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === propName,\n );\n\n for (const typeName of violations) {\n if (isTypeInAllowlist(typeName, allowlist)) continue;\n\n context.report({\n node: attribute || reportNode,\n messageId: 'unsafeTypes',\n data: { propName, typeName },\n });\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n\n // Check for spread attributes\n const attributes = childOpeningElement.attributes;\n for (const attr of attributes) {\n if (attr.type === 'JSXSpreadAttribute') {\n context.report({\n node: reportNode,\n messageId: 'spreadAttributes',\n });\n continue;\n }\n }\n}\n\nexport default ESLintUtils.RuleCreator.withoutDocs<\n [{ allowDbTypes?: (string | RegExp)[] }?],\n 'spreadAttributes' | 'unsafeTypes'\n>({\n meta: {\n type: 'problem',\n messages: {\n spreadAttributes: 'Spread attributes are not allowed in Hydrate children.',\n unsafeTypes:\n 'Prop \"{{propName}}\" uses type \"{{typeName}}\" which is derived from db-types.ts. Use safe-db-types.ts instead.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n allowDbTypes: {\n type: 'array',\n items: {\n type: 'string',\n },\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{}],\n create(context) {\n const options = context.options[0] || {};\n const allowlist = options.allowDbTypes || [];\n return {\n JSXElement(node) {\n const openingElementNameExpression = node.openingElement.name;\n if (openingElementNameExpression.type !== 'JSXIdentifier') return;\n\n const elementName = openingElementNameExpression.name;\n if (elementName !== HYDRATE_COMPONENT_NAME) return;\n\n const child = extractChild(node.children);\n if (!child) return;\n\n // Get the component being rendered\n const childOpeningElement = child.openingElement;\n const childElementName = childOpeningElement.name;\n\n if (childElementName.type !== 'JSXIdentifier') return;\n\n // Get the component's type to inspect its props\n const services = ESLintUtils.getParserServices(context);\n const typeChecker = services.program.getTypeChecker();\n const tsChildNode = services.esTreeNodeToTSNodeMap.get(childElementName);\n const componentSymbol = typeChecker.getSymbolAtLocation(tsChildNode);\n\n if (!componentSymbol) return;\n\n checkComponentProps({\n context,\n typeChecker,\n componentSymbol,\n tsComponentNode: tsChildNode,\n jsxElement: child,\n reportNode: child,\n allowlist,\n });\n },\n\n CallExpression(node) {\n // Check for hydrateHtml(<Component ... />, props?) calls\n if (node.callee.type !== 'Identifier' || node.callee.name !== HYDRATE_FUNCTION_NAME) return;\n\n // Should have at least one argument, the first is JSX element.\n if (node.arguments.length === 0) return;\n\n const arg = node.arguments[0];\n if (arg.type !== 'JSXElement') return;\n\n const jsxElement = arg;\n const openingElement = jsxElement.openingElement;\n const elementName = openingElement.name;\n\n if (elementName.type !== 'JSXIdentifier') return;\n\n // Get the component's type to inspect its props\n const services = ESLintUtils.getParserServices(context);\n const typeChecker = services.program.getTypeChecker();\n const tsElementNode = services.esTreeNodeToTSNodeMap.get(elementName);\n const componentSymbol = typeChecker.getSymbolAtLocation(tsElementNode);\n\n if (!componentSymbol) return;\n\n checkComponentProps({\n context,\n typeChecker,\n componentSymbol,\n tsComponentNode: tsElementNode,\n jsxElement,\n reportNode: node,\n allowlist,\n });\n },\n };\n },\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=no-current-target-in-callback.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-current-target-in-callback.test.d.ts","sourceRoot":"","sources":["../../src/tests/no-current-target-in-callback.test.ts"],"names":[],"mappings":"","sourcesContent":["import { RuleTester } from '@typescript-eslint/rule-tester';\nimport { afterAll, describe, it } from 'vitest';\n\nimport rule from '../rules/no-current-target-in-callback.js';\n\nRuleTester.afterAll = afterAll;\nRuleTester.describe = describe;\nRuleTester.it = it;\n\nconst ruleTester = new RuleTester({\n languageOptions: {\n parserOptions: {\n ecmaFeatures: {\n jsx: true,\n },\n },\n },\n});\n\nruleTester.run('no-current-target-in-callback', rule, {\n valid: [\n // Direct access to currentTarget (no nesting) is fine\n {\n code: '<input onChange={(e) => console.log(e.currentTarget.value)} />',\n },\n // Destructuring at the handler level is the recommended pattern\n {\n code: '<input onChange={({ currentTarget }) => setChecks((c) => ({ ...c, value: currentTarget.checked }))} />',\n },\n // Using a local variable is fine\n {\n code: `<input onChange={(e) => {\n const target = e.currentTarget;\n setChecks((c) => ({ ...c, value: target.checked }));\n }} />`,\n },\n // Non-event handlers are not checked\n {\n code: '<Component render={(e) => setFoo(() => e.currentTarget)} />',\n },\n // Direct setState without callback\n {\n code: '<input onChange={(e) => setChecks({ value: e.currentTarget.checked })} />',\n },\n // Event target (not currentTarget) - different issue, not covered by this rule\n {\n code: '<input onChange={(e) => setChecks((c) => ({ ...c, value: e.target.checked }))} />',\n },\n ],\n invalid: [\n // Basic case: accessing e.currentTarget inside setState callback\n {\n code: '<input onChange={(e) => setChecks((c) => ({ ...c, value: e.currentTarget.checked }))} />',\n errors: [{ messageId: 'noCurrentTargetInCallback' }],\n },\n // Multiple accesses\n {\n code: `<input onChange={(e) => setChecks((c) => ({\n ...c,\n value: e.currentTarget.checked,\n name: e.currentTarget.name\n }))} />`,\n errors: [\n { messageId: 'noCurrentTargetInCallback' },\n { messageId: 'noCurrentTargetInCallback' },\n ],\n },\n // Different event parameter names\n {\n code: '<input onChange={(event) => setChecks((c) => ({ ...c, value: event.currentTarget.checked }))} />',\n errors: [{ messageId: 'noCurrentTargetInCallback' }],\n },\n {\n code: '<input onChange={(evt) => setChecks((c) => ({ ...c, value: evt.currentTarget.checked }))} />',\n errors: [{ messageId: 'noCurrentTargetInCallback' }],\n },\n // onClick handler\n {\n code: '<button onClick={(e) => setFoo((prev) => ({ ...prev, clicked: e.currentTarget.id }))} />',\n errors: [{ messageId: 'noCurrentTargetInCallback' }],\n },\n // Nested arrow function in array method\n {\n code: '<input onChange={(e) => items.map(() => e.currentTarget.value)} />',\n errors: [{ messageId: 'noCurrentTargetInCallback' }],\n },\n // Function expression callback\n {\n code: '<input onChange={(e) => setChecks(function(c) { return { ...c, value: e.currentTarget.checked }; })} />',\n errors: [{ messageId: 'noCurrentTargetInCallback' }],\n },\n // Deeply nested\n {\n code: '<input onChange={(e) => outer(() => inner(() => e.currentTarget.value))} />',\n errors: [{ messageId: 'noCurrentTargetInCallback' }],\n },\n // With block body\n {\n code: `<input onChange={(e) => {\n setChecks((c) => {\n return { ...c, value: e.currentTarget.checked };\n });\n }} />`,\n errors: [{ messageId: 'noCurrentTargetInCallback' }],\n },\n ],\n});\n"]}
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const rule_tester_1 = require("@typescript-eslint/rule-tester");
7
+ const vitest_1 = require("vitest");
8
+ const no_current_target_in_callback_js_1 = __importDefault(require("../rules/no-current-target-in-callback.js"));
9
+ rule_tester_1.RuleTester.afterAll = vitest_1.afterAll;
10
+ rule_tester_1.RuleTester.describe = vitest_1.describe;
11
+ rule_tester_1.RuleTester.it = vitest_1.it;
12
+ const ruleTester = new rule_tester_1.RuleTester({
13
+ languageOptions: {
14
+ parserOptions: {
15
+ ecmaFeatures: {
16
+ jsx: true,
17
+ },
18
+ },
19
+ },
20
+ });
21
+ ruleTester.run('no-current-target-in-callback', no_current_target_in_callback_js_1.default, {
22
+ valid: [
23
+ // Direct access to currentTarget (no nesting) is fine
24
+ {
25
+ code: '<input onChange={(e) => console.log(e.currentTarget.value)} />',
26
+ },
27
+ // Destructuring at the handler level is the recommended pattern
28
+ {
29
+ code: '<input onChange={({ currentTarget }) => setChecks((c) => ({ ...c, value: currentTarget.checked }))} />',
30
+ },
31
+ // Using a local variable is fine
32
+ {
33
+ code: `<input onChange={(e) => {
34
+ const target = e.currentTarget;
35
+ setChecks((c) => ({ ...c, value: target.checked }));
36
+ }} />`,
37
+ },
38
+ // Non-event handlers are not checked
39
+ {
40
+ code: '<Component render={(e) => setFoo(() => e.currentTarget)} />',
41
+ },
42
+ // Direct setState without callback
43
+ {
44
+ code: '<input onChange={(e) => setChecks({ value: e.currentTarget.checked })} />',
45
+ },
46
+ // Event target (not currentTarget) - different issue, not covered by this rule
47
+ {
48
+ code: '<input onChange={(e) => setChecks((c) => ({ ...c, value: e.target.checked }))} />',
49
+ },
50
+ ],
51
+ invalid: [
52
+ // Basic case: accessing e.currentTarget inside setState callback
53
+ {
54
+ code: '<input onChange={(e) => setChecks((c) => ({ ...c, value: e.currentTarget.checked }))} />',
55
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
56
+ },
57
+ // Multiple accesses
58
+ {
59
+ code: `<input onChange={(e) => setChecks((c) => ({
60
+ ...c,
61
+ value: e.currentTarget.checked,
62
+ name: e.currentTarget.name
63
+ }))} />`,
64
+ errors: [
65
+ { messageId: 'noCurrentTargetInCallback' },
66
+ { messageId: 'noCurrentTargetInCallback' },
67
+ ],
68
+ },
69
+ // Different event parameter names
70
+ {
71
+ code: '<input onChange={(event) => setChecks((c) => ({ ...c, value: event.currentTarget.checked }))} />',
72
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
73
+ },
74
+ {
75
+ code: '<input onChange={(evt) => setChecks((c) => ({ ...c, value: evt.currentTarget.checked }))} />',
76
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
77
+ },
78
+ // onClick handler
79
+ {
80
+ code: '<button onClick={(e) => setFoo((prev) => ({ ...prev, clicked: e.currentTarget.id }))} />',
81
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
82
+ },
83
+ // Nested arrow function in array method
84
+ {
85
+ code: '<input onChange={(e) => items.map(() => e.currentTarget.value)} />',
86
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
87
+ },
88
+ // Function expression callback
89
+ {
90
+ code: '<input onChange={(e) => setChecks(function(c) { return { ...c, value: e.currentTarget.checked }; })} />',
91
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
92
+ },
93
+ // Deeply nested
94
+ {
95
+ code: '<input onChange={(e) => outer(() => inner(() => e.currentTarget.value))} />',
96
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
97
+ },
98
+ // With block body
99
+ {
100
+ code: `<input onChange={(e) => {
101
+ setChecks((c) => {
102
+ return { ...c, value: e.currentTarget.checked };
103
+ });
104
+ }} />`,
105
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
106
+ },
107
+ ],
108
+ });
109
+ //# sourceMappingURL=no-current-target-in-callback.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-current-target-in-callback.test.js","sourceRoot":"","sources":["../../src/tests/no-current-target-in-callback.test.ts"],"names":[],"mappings":";;;;;AAAA,gEAA4D;AAC5D,mCAAgD;AAEhD,iHAA6D;AAE7D,wBAAU,CAAC,QAAQ,GAAG,iBAAQ,CAAC;AAC/B,wBAAU,CAAC,QAAQ,GAAG,iBAAQ,CAAC;AAC/B,wBAAU,CAAC,EAAE,GAAG,WAAE,CAAC;AAEnB,MAAM,UAAU,GAAG,IAAI,wBAAU,CAAC;IAChC,eAAe,EAAE;QACf,aAAa,EAAE;YACb,YAAY,EAAE;gBACZ,GAAG,EAAE,IAAI;aACV;SACF;KACF;CACF,CAAC,CAAC;AAEH,UAAU,CAAC,GAAG,CAAC,+BAA+B,EAAE,0CAAI,EAAE;IACpD,KAAK,EAAE;QACL,sDAAsD;QACtD;YACE,IAAI,EAAE,gEAAgE;SACvE;QACD,gEAAgE;QAChE;YACE,IAAI,EAAE,wGAAwG;SAC/G;QACD,iCAAiC;QACjC;YACE,IAAI,EAAE;;;YAGA;SACP;QACD,qCAAqC;QACrC;YACE,IAAI,EAAE,6DAA6D;SACpE;QACD,mCAAmC;QACnC;YACE,IAAI,EAAE,2EAA2E;SAClF;QACD,+EAA+E;QAC/E;YACE,IAAI,EAAE,mFAAmF;SAC1F;KACF;IACD,OAAO,EAAE;QACP,iEAAiE;QACjE;YACE,IAAI,EAAE,0FAA0F;YAChG,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,2BAA2B,EAAE,CAAC;SACrD;QACD,oBAAoB;QACpB;YACE,IAAI,EAAE;;;;cAIE;YACR,MAAM,EAAE;gBACN,EAAE,SAAS,EAAE,2BAA2B,EAAE;gBAC1C,EAAE,SAAS,EAAE,2BAA2B,EAAE;aAC3C;SACF;QACD,kCAAkC;QAClC;YACE,IAAI,EAAE,kGAAkG;YACxG,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,2BAA2B,EAAE,CAAC;SACrD;QACD;YACE,IAAI,EAAE,8FAA8F;YACpG,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,2BAA2B,EAAE,CAAC;SACrD;QACD,kBAAkB;QAClB;YACE,IAAI,EAAE,0FAA0F;YAChG,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,2BAA2B,EAAE,CAAC;SACrD;QACD,wCAAwC;QACxC;YACE,IAAI,EAAE,oEAAoE;YAC1E,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,2BAA2B,EAAE,CAAC;SACrD;QACD,+BAA+B;QAC/B;YACE,IAAI,EAAE,yGAAyG;YAC/G,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,2BAA2B,EAAE,CAAC;SACrD;QACD,gBAAgB;QAChB;YACE,IAAI,EAAE,6EAA6E;YACnF,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,2BAA2B,EAAE,CAAC;SACrD;QACD,kBAAkB;QAClB;YACE,IAAI,EAAE;;;;YAIA;YACN,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,2BAA2B,EAAE,CAAC;SACrD;KACF;CACF,CAAC,CAAC","sourcesContent":["import { RuleTester } from '@typescript-eslint/rule-tester';\nimport { afterAll, describe, it } from 'vitest';\n\nimport rule from '../rules/no-current-target-in-callback.js';\n\nRuleTester.afterAll = afterAll;\nRuleTester.describe = describe;\nRuleTester.it = it;\n\nconst ruleTester = new RuleTester({\n languageOptions: {\n parserOptions: {\n ecmaFeatures: {\n jsx: true,\n },\n },\n },\n});\n\nruleTester.run('no-current-target-in-callback', rule, {\n valid: [\n // Direct access to currentTarget (no nesting) is fine\n {\n code: '<input onChange={(e) => console.log(e.currentTarget.value)} />',\n },\n // Destructuring at the handler level is the recommended pattern\n {\n code: '<input onChange={({ currentTarget }) => setChecks((c) => ({ ...c, value: currentTarget.checked }))} />',\n },\n // Using a local variable is fine\n {\n code: `<input onChange={(e) => {\n const target = e.currentTarget;\n setChecks((c) => ({ ...c, value: target.checked }));\n }} />`,\n },\n // Non-event handlers are not checked\n {\n code: '<Component render={(e) => setFoo(() => e.currentTarget)} />',\n },\n // Direct setState without callback\n {\n code: '<input onChange={(e) => setChecks({ value: e.currentTarget.checked })} />',\n },\n // Event target (not currentTarget) - different issue, not covered by this rule\n {\n code: '<input onChange={(e) => setChecks((c) => ({ ...c, value: e.target.checked }))} />',\n },\n ],\n invalid: [\n // Basic case: accessing e.currentTarget inside setState callback\n {\n code: '<input onChange={(e) => setChecks((c) => ({ ...c, value: e.currentTarget.checked }))} />',\n errors: [{ messageId: 'noCurrentTargetInCallback' }],\n },\n // Multiple accesses\n {\n code: `<input onChange={(e) => setChecks((c) => ({\n ...c,\n value: e.currentTarget.checked,\n name: e.currentTarget.name\n }))} />`,\n errors: [\n { messageId: 'noCurrentTargetInCallback' },\n { messageId: 'noCurrentTargetInCallback' },\n ],\n },\n // Different event parameter names\n {\n code: '<input onChange={(event) => setChecks((c) => ({ ...c, value: event.currentTarget.checked }))} />',\n errors: [{ messageId: 'noCurrentTargetInCallback' }],\n },\n {\n code: '<input onChange={(evt) => setChecks((c) => ({ ...c, value: evt.currentTarget.checked }))} />',\n errors: [{ messageId: 'noCurrentTargetInCallback' }],\n },\n // onClick handler\n {\n code: '<button onClick={(e) => setFoo((prev) => ({ ...prev, clicked: e.currentTarget.id }))} />',\n errors: [{ messageId: 'noCurrentTargetInCallback' }],\n },\n // Nested arrow function in array method\n {\n code: '<input onChange={(e) => items.map(() => e.currentTarget.value)} />',\n errors: [{ messageId: 'noCurrentTargetInCallback' }],\n },\n // Function expression callback\n {\n code: '<input onChange={(e) => setChecks(function(c) { return { ...c, value: e.currentTarget.checked }; })} />',\n errors: [{ messageId: 'noCurrentTargetInCallback' }],\n },\n // Deeply nested\n {\n code: '<input onChange={(e) => outer(() => inner(() => e.currentTarget.value))} />',\n errors: [{ messageId: 'noCurrentTargetInCallback' }],\n },\n // With block body\n {\n code: `<input onChange={(e) => {\n setChecks((c) => {\n return { ...c, value: e.currentTarget.checked };\n });\n }} />`,\n errors: [{ messageId: 'noCurrentTargetInCallback' }],\n },\n ],\n});\n"]}
package/dist/utils.d.ts CHANGED
@@ -1,8 +1,3 @@
1
- /**
2
- * Determines if the given identifier name corresponds to a client from the
3
- * given package.
4
- */
5
- export declare function isIdentifierClient(identifierName: string, packageName: string): boolean;
6
1
  /**
7
2
  * Retrieves the names of AWS clients specified by the given import declaration.
8
3
  *
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAcvF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sCAAsC,CAAC,IAAI,EAAE,GAAG,eAgB/D","sourcesContent":["/**\n * Determines if the given identifier name corresponds to a client from the\n * given package.\n */\nexport function isIdentifierClient(identifierName: string, packageName: string): boolean {\n // If the identifier ends with \"Client\", include it in the set.\n if (identifierName.endsWith('Client')) {\n return true;\n }\n\n // If the identifier matches the package name directly, include it in the set.\n const clientName = packageName.replace('@aws-sdk/client-', '');\n const packageIdentifier = clientName.replaceAll('-', '').toLowerCase();\n if (identifierName.toLowerCase() === packageIdentifier) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Retrieves the names of AWS clients specified by the given import declaration.\n *\n * For instance, if the import declaration is:\n *\n * ```ts\n * import { S3, S3Client } from '@aws-sdk/client-s3';\n * ```\n *\n * then this function will return a set containing the strings \"S3\" and \"S3Client\".\n */\nexport function getAwsClientNamesFromImportDeclaration(node: any) {\n const clientNames = new Set<string>();\n\n const importSource = node.source.value;\n if (importSource.startsWith('@aws-sdk/client-')) {\n node.specifiers.forEach((specifier: any) => {\n if (specifier.type === 'ImportSpecifier') {\n const specifierName = specifier.imported.name;\n if (isIdentifierClient(specifierName, importSource)) {\n clientNames.add(specifierName);\n }\n }\n });\n }\n\n return clientNames;\n}\n"]}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;;GAUG;AACH,wBAAgB,sCAAsC,CAAC,IAAI,EAAE,GAAG,eAgB/D","sourcesContent":["/**\n * Determines if the given identifier name corresponds to a client from the\n * given package.\n */\nfunction isIdentifierClient(identifierName: string, packageName: string): boolean {\n // If the identifier ends with \"Client\", include it in the set.\n if (identifierName.endsWith('Client')) {\n return true;\n }\n\n // If the identifier matches the package name directly, include it in the set.\n const clientName = packageName.replace('@aws-sdk/client-', '');\n const packageIdentifier = clientName.replaceAll('-', '').toLowerCase();\n if (identifierName.toLowerCase() === packageIdentifier) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Retrieves the names of AWS clients specified by the given import declaration.\n *\n * For instance, if the import declaration is:\n *\n * ```ts\n * import { S3, S3Client } from '@aws-sdk/client-s3';\n * ```\n *\n * then this function will return a set containing the strings \"S3\" and \"S3Client\".\n */\nexport function getAwsClientNamesFromImportDeclaration(node: any) {\n const clientNames = new Set<string>();\n\n const importSource = node.source.value;\n if (importSource.startsWith('@aws-sdk/client-')) {\n node.specifiers.forEach((specifier: any) => {\n if (specifier.type === 'ImportSpecifier') {\n const specifierName = specifier.imported.name;\n if (isIdentifierClient(specifierName, importSource)) {\n clientNames.add(specifierName);\n }\n }\n });\n }\n\n return clientNames;\n}\n"]}
package/dist/utils.js CHANGED
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isIdentifierClient = isIdentifierClient;
4
3
  exports.getAwsClientNamesFromImportDeclaration = getAwsClientNamesFromImportDeclaration;
5
4
  /**
6
5
  * Determines if the given identifier name corresponds to a client from the
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";;;;AAAA;;;GAGG;AACH,4BAAmC,cAAsB,EAAE,WAAmB,EAAW;IACvF,+DAA+D;IAC/D,IAAI,cAAc,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,8EAA8E;IAC9E,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;IAC/D,MAAM,iBAAiB,GAAG,UAAU,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACvE,IAAI,cAAc,CAAC,WAAW,EAAE,KAAK,iBAAiB,EAAE,CAAC;QACvD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,KAAK,CAAC;AAAA,CACd;AAED;;;;;;;;;;GAUG;AACH,gDAAuD,IAAS,EAAE;IAChE,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IAEtC,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;IACvC,IAAI,YAAY,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,CAAC;QAChD,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,SAAc,EAAE,EAAE,CAAC;YAC1C,IAAI,SAAS,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;gBACzC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAC9C,IAAI,kBAAkB,CAAC,aAAa,EAAE,YAAY,CAAC,EAAE,CAAC;oBACpD,WAAW,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;gBACjC,CAAC;YACH,CAAC;QAAA,CACF,CAAC,CAAC;IACL,CAAC;IAED,OAAO,WAAW,CAAC;AAAA,CACpB","sourcesContent":["/**\n * Determines if the given identifier name corresponds to a client from the\n * given package.\n */\nexport function isIdentifierClient(identifierName: string, packageName: string): boolean {\n // If the identifier ends with \"Client\", include it in the set.\n if (identifierName.endsWith('Client')) {\n return true;\n }\n\n // If the identifier matches the package name directly, include it in the set.\n const clientName = packageName.replace('@aws-sdk/client-', '');\n const packageIdentifier = clientName.replaceAll('-', '').toLowerCase();\n if (identifierName.toLowerCase() === packageIdentifier) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Retrieves the names of AWS clients specified by the given import declaration.\n *\n * For instance, if the import declaration is:\n *\n * ```ts\n * import { S3, S3Client } from '@aws-sdk/client-s3';\n * ```\n *\n * then this function will return a set containing the strings \"S3\" and \"S3Client\".\n */\nexport function getAwsClientNamesFromImportDeclaration(node: any) {\n const clientNames = new Set<string>();\n\n const importSource = node.source.value;\n if (importSource.startsWith('@aws-sdk/client-')) {\n node.specifiers.forEach((specifier: any) => {\n if (specifier.type === 'ImportSpecifier') {\n const specifierName = specifier.imported.name;\n if (isIdentifierClient(specifierName, importSource)) {\n clientNames.add(specifierName);\n }\n }\n });\n }\n\n return clientNames;\n}\n"]}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";;;AAAA;;;GAGG;AACH,SAAS,kBAAkB,CAAC,cAAsB,EAAE,WAAmB,EAAW;IAChF,+DAA+D;IAC/D,IAAI,cAAc,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,8EAA8E;IAC9E,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;IAC/D,MAAM,iBAAiB,GAAG,UAAU,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACvE,IAAI,cAAc,CAAC,WAAW,EAAE,KAAK,iBAAiB,EAAE,CAAC;QACvD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,KAAK,CAAC;AAAA,CACd;AAED;;;;;;;;;;GAUG;AACH,gDAAuD,IAAS,EAAE;IAChE,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IAEtC,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;IACvC,IAAI,YAAY,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,CAAC;QAChD,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,SAAc,EAAE,EAAE,CAAC;YAC1C,IAAI,SAAS,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;gBACzC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAC9C,IAAI,kBAAkB,CAAC,aAAa,EAAE,YAAY,CAAC,EAAE,CAAC;oBACpD,WAAW,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;gBACjC,CAAC;YACH,CAAC;QAAA,CACF,CAAC,CAAC;IACL,CAAC;IAED,OAAO,WAAW,CAAC;AAAA,CACpB","sourcesContent":["/**\n * Determines if the given identifier name corresponds to a client from the\n * given package.\n */\nfunction isIdentifierClient(identifierName: string, packageName: string): boolean {\n // If the identifier ends with \"Client\", include it in the set.\n if (identifierName.endsWith('Client')) {\n return true;\n }\n\n // If the identifier matches the package name directly, include it in the set.\n const clientName = packageName.replace('@aws-sdk/client-', '');\n const packageIdentifier = clientName.replaceAll('-', '').toLowerCase();\n if (identifierName.toLowerCase() === packageIdentifier) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Retrieves the names of AWS clients specified by the given import declaration.\n *\n * For instance, if the import declaration is:\n *\n * ```ts\n * import { S3, S3Client } from '@aws-sdk/client-s3';\n * ```\n *\n * then this function will return a set containing the strings \"S3\" and \"S3Client\".\n */\nexport function getAwsClientNamesFromImportDeclaration(node: any) {\n const clientNames = new Set<string>();\n\n const importSource = node.source.value;\n if (importSource.startsWith('@aws-sdk/client-')) {\n node.specifiers.forEach((specifier: any) => {\n if (specifier.type === 'ImportSpecifier') {\n const specifierName = specifier.imported.name;\n if (isIdentifierClient(specifierName, importSource)) {\n clientNames.add(specifierName);\n }\n }\n });\n }\n\n return clientNames;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prairielearn/eslint-plugin",
3
- "version": "3.0.1",
3
+ "version": "3.1.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/PrairieLearn/PrairieLearn.git",
@@ -24,7 +24,7 @@
24
24
  "@types/node": "^24.10.9",
25
25
  "@typescript-eslint/rule-tester": "^8.54.0",
26
26
  "@typescript-eslint/types": "^8.54.0",
27
- "@typescript/native-preview": "^7.0.0-dev.20260130.1",
27
+ "@typescript/native-preview": "^7.0.0-dev.20260203.1",
28
28
  "@vitest/coverage-v8": "^4.0.18",
29
29
  "tsx": "^4.21.0",
30
30
  "vitest": "^4.0.18"
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';
2
2
  import awsClientSharedConfig from './rules/aws-client-shared-config.js';
3
3
  import jsxNoDollarInterpolation from './rules/jsx-no-dollar-interpolation.js';
4
+ import noCurrentTargetInCallback from './rules/no-current-target-in-callback.js';
4
5
  import noUnusedSqlBlocks from './rules/no-unused-sql-blocks.js';
5
6
  import safeDbTypes from './rules/safe-db-types.js';
6
7
 
@@ -8,6 +9,7 @@ export const rules = {
8
9
  'aws-client-mandatory-config': awsClientMandatoryConfig,
9
10
  'aws-client-shared-config': awsClientSharedConfig,
10
11
  'jsx-no-dollar-interpolation': jsxNoDollarInterpolation,
12
+ 'no-current-target-in-callback': noCurrentTargetInCallback,
11
13
  'no-unused-sql-blocks': noUnusedSqlBlocks,
12
14
  'safe-db-types': safeDbTypes,
13
15
  };
@@ -0,0 +1,177 @@
1
+ import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils';
2
+
3
+ /**
4
+ * This rule detects when `event.currentTarget` is accessed inside a nested
5
+ * callback function within a React event handler. This is problematic because
6
+ * React may execute callbacks (like those passed to setState) asynchronously,
7
+ * at which point `currentTarget` may already be nullified.
8
+ *
9
+ * Bad:
10
+ * ```tsx
11
+ * onChange={(e) => setChecks((c) => ({ ...c, value: e.currentTarget.checked }))}
12
+ * ```
13
+ *
14
+ * Good:
15
+ * ```tsx
16
+ * onChange={({ currentTarget }) => setChecks((c) => ({ ...c, value: currentTarget.checked }))}
17
+ * ```
18
+ */
19
+ export default ESLintUtils.RuleCreator.withoutDocs({
20
+ meta: {
21
+ type: 'problem',
22
+ messages: {
23
+ noCurrentTargetInCallback:
24
+ '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.',
25
+ },
26
+ schema: [],
27
+ },
28
+ defaultOptions: [],
29
+
30
+ create(context) {
31
+ // Track event handler parameters and their scopes
32
+ const eventHandlerParams = new Map<TSESTree.Node, TSESTree.Identifier>();
33
+
34
+ /** Check if a node is inside a nested function relative to the event handler */
35
+ function isInsideNestedFunction(
36
+ node: TSESTree.Node,
37
+ eventHandlerFunction: TSESTree.Node,
38
+ ): boolean {
39
+ let current: TSESTree.Node | undefined = node.parent;
40
+ let foundNestedFunction = false;
41
+
42
+ while (current && current !== eventHandlerFunction) {
43
+ if (
44
+ current.type === 'ArrowFunctionExpression' ||
45
+ current.type === 'FunctionExpression' ||
46
+ current.type === 'FunctionDeclaration'
47
+ ) {
48
+ foundNestedFunction = true;
49
+ }
50
+ current = current.parent;
51
+ }
52
+
53
+ return foundNestedFunction && current === eventHandlerFunction;
54
+ }
55
+
56
+ /** Find the function that contains this node */
57
+ function findContainingFunction(
58
+ node: TSESTree.Node,
59
+ ): TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | null {
60
+ let current: TSESTree.Node | undefined = node.parent;
61
+
62
+ while (current) {
63
+ if (current.type === 'ArrowFunctionExpression' || current.type === 'FunctionExpression') {
64
+ return current;
65
+ }
66
+ current = current.parent;
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ /** Check if a function is a JSX event handler */
73
+ function isJSXEventHandler(
74
+ func: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
75
+ ): boolean {
76
+ const parent = func.parent;
77
+
78
+ // Check for JSX attribute like onChange={...}
79
+ if (parent?.type === 'JSXExpressionContainer') {
80
+ const jsxAttribute = parent.parent;
81
+ if (jsxAttribute?.type === 'JSXAttribute') {
82
+ const attrName = jsxAttribute.name.type === 'JSXIdentifier' ? jsxAttribute.name.name : '';
83
+ // Match common event handler patterns
84
+ return /^on[A-Z]/.test(attrName);
85
+ }
86
+ }
87
+
88
+ return false;
89
+ }
90
+
91
+ /** Get the first parameter of an event handler if it looks like an event */
92
+ function getEventParameter(
93
+ func: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
94
+ ): TSESTree.Identifier | null {
95
+ const firstParam = func.params[0];
96
+
97
+ if (!firstParam) return null;
98
+
99
+ // Simple identifier parameter like (e) => ...
100
+ if (firstParam.type === 'Identifier') {
101
+ return firstParam;
102
+ }
103
+
104
+ return null;
105
+ }
106
+
107
+ return {
108
+ // When we enter a JSX event handler, track its event parameter
109
+ 'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression': (
110
+ node: TSESTree.ArrowFunctionExpression,
111
+ ) => {
112
+ if (isJSXEventHandler(node)) {
113
+ const eventParam = getEventParameter(node);
114
+ if (eventParam) {
115
+ eventHandlerParams.set(node, eventParam);
116
+ }
117
+ }
118
+ },
119
+ 'JSXAttribute > JSXExpressionContainer > FunctionExpression': (
120
+ node: TSESTree.FunctionExpression,
121
+ ) => {
122
+ if (isJSXEventHandler(node)) {
123
+ const eventParam = getEventParameter(node);
124
+ if (eventParam) {
125
+ eventHandlerParams.set(node, eventParam);
126
+ }
127
+ }
128
+ },
129
+
130
+ // Check for .currentTarget access
131
+ MemberExpression(node) {
132
+ // Only check for .currentTarget property access
133
+ if (node.property.type !== 'Identifier' || node.property.name !== 'currentTarget') {
134
+ return;
135
+ }
136
+
137
+ // Check if the object is an identifier (like `e` in `e.currentTarget`)
138
+ if (node.object.type !== 'Identifier') {
139
+ return;
140
+ }
141
+
142
+ const objectName = node.object.name;
143
+
144
+ // Find the containing function
145
+ const containingFunction = findContainingFunction(node);
146
+ if (!containingFunction) return;
147
+
148
+ // Check each tracked event handler to see if this access is problematic
149
+ for (const [eventHandler, eventParam] of eventHandlerParams) {
150
+ // Check if this is accessing the event parameter
151
+ if (objectName !== eventParam.name) continue;
152
+
153
+ // Check if the access is inside a nested function within the event handler
154
+ if (isInsideNestedFunction(node, eventHandler)) {
155
+ context.report({
156
+ node,
157
+ messageId: 'noCurrentTargetInCallback',
158
+ });
159
+ return;
160
+ }
161
+ }
162
+ },
163
+
164
+ // Clean up when leaving event handlers
165
+ 'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression:exit': (
166
+ node: TSESTree.ArrowFunctionExpression,
167
+ ) => {
168
+ eventHandlerParams.delete(node);
169
+ },
170
+ 'JSXAttribute > JSXExpressionContainer > FunctionExpression:exit': (
171
+ node: TSESTree.FunctionExpression,
172
+ ) => {
173
+ eventHandlerParams.delete(node);
174
+ },
175
+ };
176
+ },
177
+ });
@@ -332,19 +332,27 @@ function isTypeInAllowlist(typeName: string, allowlist: (string | RegExp)[]): bo
332
332
  /**
333
333
  * Check the props of a component for unsafe types from db-types.ts
334
334
  */
335
- function checkComponentProps(
335
+ function checkComponentProps({
336
+ context,
337
+ typeChecker,
338
+ componentSymbol,
339
+ tsComponentNode,
340
+ jsxElement,
341
+ reportNode,
342
+ allowlist,
343
+ }: {
336
344
  context: ReturnType<typeof ESLintUtils.RuleCreator.withoutDocs>['create'] extends (
337
345
  context: infer C,
338
346
  ) => any
339
347
  ? C
340
- : never,
341
- typeChecker: ts.TypeChecker,
342
- componentSymbol: ts.Symbol,
343
- tsComponentNode: ts.Node,
344
- jsxElement: TSESTree.JSXElement,
345
- reportNode: TSESTree.Node,
346
- allowlist: (string | RegExp)[],
347
- ): void {
348
+ : never;
349
+ typeChecker: ts.TypeChecker;
350
+ componentSymbol: ts.Symbol;
351
+ tsComponentNode: ts.Node;
352
+ jsxElement: TSESTree.JSXElement;
353
+ reportNode: TSESTree.Node;
354
+ allowlist: (string | RegExp)[];
355
+ }): void {
348
356
  const childOpeningElement = jsxElement.openingElement;
349
357
 
350
358
  // Get the component's type (function or class component)
@@ -543,15 +551,15 @@ export default ESLintUtils.RuleCreator.withoutDocs<
543
551
 
544
552
  if (!componentSymbol) return;
545
553
 
546
- checkComponentProps(
554
+ checkComponentProps({
547
555
  context,
548
556
  typeChecker,
549
557
  componentSymbol,
550
- tsChildNode,
551
- child,
552
- child,
558
+ tsComponentNode: tsChildNode,
559
+ jsxElement: child,
560
+ reportNode: child,
553
561
  allowlist,
554
- );
562
+ });
555
563
  },
556
564
 
557
565
  CallExpression(node) {
@@ -578,15 +586,15 @@ export default ESLintUtils.RuleCreator.withoutDocs<
578
586
 
579
587
  if (!componentSymbol) return;
580
588
 
581
- checkComponentProps(
589
+ checkComponentProps({
582
590
  context,
583
591
  typeChecker,
584
592
  componentSymbol,
585
- tsElementNode,
593
+ tsComponentNode: tsElementNode,
586
594
  jsxElement,
587
- node,
595
+ reportNode: node,
588
596
  allowlist,
589
- );
597
+ });
590
598
  },
591
599
  };
592
600
  },
@@ -0,0 +1,107 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+ import { afterAll, describe, it } from 'vitest';
3
+
4
+ import rule from '../rules/no-current-target-in-callback.js';
5
+
6
+ RuleTester.afterAll = afterAll;
7
+ RuleTester.describe = describe;
8
+ RuleTester.it = it;
9
+
10
+ const ruleTester = new RuleTester({
11
+ languageOptions: {
12
+ parserOptions: {
13
+ ecmaFeatures: {
14
+ jsx: true,
15
+ },
16
+ },
17
+ },
18
+ });
19
+
20
+ ruleTester.run('no-current-target-in-callback', rule, {
21
+ valid: [
22
+ // Direct access to currentTarget (no nesting) is fine
23
+ {
24
+ code: '<input onChange={(e) => console.log(e.currentTarget.value)} />',
25
+ },
26
+ // Destructuring at the handler level is the recommended pattern
27
+ {
28
+ code: '<input onChange={({ currentTarget }) => setChecks((c) => ({ ...c, value: currentTarget.checked }))} />',
29
+ },
30
+ // Using a local variable is fine
31
+ {
32
+ code: `<input onChange={(e) => {
33
+ const target = e.currentTarget;
34
+ setChecks((c) => ({ ...c, value: target.checked }));
35
+ }} />`,
36
+ },
37
+ // Non-event handlers are not checked
38
+ {
39
+ code: '<Component render={(e) => setFoo(() => e.currentTarget)} />',
40
+ },
41
+ // Direct setState without callback
42
+ {
43
+ code: '<input onChange={(e) => setChecks({ value: e.currentTarget.checked })} />',
44
+ },
45
+ // Event target (not currentTarget) - different issue, not covered by this rule
46
+ {
47
+ code: '<input onChange={(e) => setChecks((c) => ({ ...c, value: e.target.checked }))} />',
48
+ },
49
+ ],
50
+ invalid: [
51
+ // Basic case: accessing e.currentTarget inside setState callback
52
+ {
53
+ code: '<input onChange={(e) => setChecks((c) => ({ ...c, value: e.currentTarget.checked }))} />',
54
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
55
+ },
56
+ // Multiple accesses
57
+ {
58
+ code: `<input onChange={(e) => setChecks((c) => ({
59
+ ...c,
60
+ value: e.currentTarget.checked,
61
+ name: e.currentTarget.name
62
+ }))} />`,
63
+ errors: [
64
+ { messageId: 'noCurrentTargetInCallback' },
65
+ { messageId: 'noCurrentTargetInCallback' },
66
+ ],
67
+ },
68
+ // Different event parameter names
69
+ {
70
+ code: '<input onChange={(event) => setChecks((c) => ({ ...c, value: event.currentTarget.checked }))} />',
71
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
72
+ },
73
+ {
74
+ code: '<input onChange={(evt) => setChecks((c) => ({ ...c, value: evt.currentTarget.checked }))} />',
75
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
76
+ },
77
+ // onClick handler
78
+ {
79
+ code: '<button onClick={(e) => setFoo((prev) => ({ ...prev, clicked: e.currentTarget.id }))} />',
80
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
81
+ },
82
+ // Nested arrow function in array method
83
+ {
84
+ code: '<input onChange={(e) => items.map(() => e.currentTarget.value)} />',
85
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
86
+ },
87
+ // Function expression callback
88
+ {
89
+ code: '<input onChange={(e) => setChecks(function(c) { return { ...c, value: e.currentTarget.checked }; })} />',
90
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
91
+ },
92
+ // Deeply nested
93
+ {
94
+ code: '<input onChange={(e) => outer(() => inner(() => e.currentTarget.value))} />',
95
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
96
+ },
97
+ // With block body
98
+ {
99
+ code: `<input onChange={(e) => {
100
+ setChecks((c) => {
101
+ return { ...c, value: e.currentTarget.checked };
102
+ });
103
+ }} />`,
104
+ errors: [{ messageId: 'noCurrentTargetInCallback' }],
105
+ },
106
+ ],
107
+ });
package/src/utils.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Determines if the given identifier name corresponds to a client from the
3
3
  * given package.
4
4
  */
5
- export function isIdentifierClient(identifierName: string, packageName: string): boolean {
5
+ function isIdentifierClient(identifierName: string, packageName: string): boolean {
6
6
  // If the identifier ends with "Client", include it in the set.
7
7
  if (identifierName.endsWith('Client')) {
8
8
  return true;