@prairielearn/eslint-plugin 3.1.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/rules/aws-client-mandatory-config.js.map +1 -1
  7. package/dist/rules/aws-client-shared-config.js.map +1 -1
  8. package/dist/rules/jsx-no-dollar-interpolation.js.map +1 -1
  9. package/dist/rules/no-current-target-in-callback.d.ts +4 -4
  10. package/dist/rules/no-current-target-in-callback.d.ts.map +1 -1
  11. package/dist/rules/no-current-target-in-callback.js +189 -34
  12. package/dist/rules/no-current-target-in-callback.js.map +1 -1
  13. package/dist/rules/no-hydrate-reslocals.d.ts +4 -0
  14. package/dist/rules/no-hydrate-reslocals.d.ts.map +1 -0
  15. package/dist/rules/no-hydrate-reslocals.js +96 -0
  16. package/dist/rules/no-hydrate-reslocals.js.map +1 -0
  17. package/dist/rules/no-unused-sql-blocks.js.map +1 -1
  18. package/dist/rules/safe-db-types.js.map +1 -1
  19. package/dist/tests/no-current-target-in-callback.test.d.ts.map +1 -1
  20. package/dist/tests/no-current-target-in-callback.test.js +67 -10
  21. package/dist/tests/no-current-target-in-callback.test.js.map +1 -1
  22. package/dist/tests/no-hydrate-reslocals.test.d.ts +2 -0
  23. package/dist/tests/no-hydrate-reslocals.test.d.ts.map +1 -0
  24. package/dist/tests/no-hydrate-reslocals.test.js +91 -0
  25. package/dist/tests/no-hydrate-reslocals.test.js.map +1 -0
  26. package/dist/utils.js.map +1 -1
  27. package/package.json +11 -8
  28. package/src/index.ts +2 -0
  29. package/src/rules/no-current-target-in-callback.ts +273 -39
  30. package/src/rules/no-hydrate-reslocals.ts +102 -0
  31. package/src/tests/no-current-target-in-callback.test.ts +67 -10
  32. package/src/tests/no-hydrate-reslocals.test.ts +89 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @prairielearn/eslint-plugin
2
2
 
3
+ ## 4.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 240b216: Add a new rule `@prairielearn/no-hydrate-reslocals` that forbids passing `resLocals` or `locals` (or spreading `res.locals`, `resLocals`, or `locals`) onto a component rendered inside `<Hydrate>` or passed to `hydrateHtml(...)`. All props on a hydrated component are serialized and sent to the client, so these patterns would leak the full server-side locals.
8
+
9
+ ### Patch Changes
10
+
11
+ - aaeb317: Expand the `no-current-target-in-callback` rule to also flag `event.currentTarget` accesses after `await` in React event handlers.
12
+ - b6e03e9: Upgrade dependencies
13
+
14
+ ## 4.0.0
15
+
16
+ ### Major Changes
17
+
18
+ - 3c4799a: Upgrade to ESLint v10
19
+
20
+ ### Patch Changes
21
+
22
+ - 3c4799a: Upgrade all JavaScript dependencies
23
+
3
24
  ## 3.1.1
4
25
 
5
26
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export declare const rules: {
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
5
  'no-current-target-in-callback': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noCurrentTargetInCallback", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
6
+ 'no-hydrate-reslocals': import("@typescript-eslint/utils/ts-eslint").RuleModule<"forbiddenProp" | "forbiddenSpread", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
6
7
  'no-unused-sql-blocks': import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedSqlBlock", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
7
8
  'safe-db-types': import("@typescript-eslint/utils/ts-eslint").RuleModule<"spreadAttributes" | "unsafeTypes", [({
8
9
  allowDbTypes?: (string | RegExp)[] | undefined;
@@ -1 +1 @@
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"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,KAAK;;;;;;;;;;CAQjB,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 noHydrateResLocals from './rules/no-hydrate-reslocals.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-hydrate-reslocals': noHydrateResLocals,\n 'no-unused-sql-blocks': noUnusedSqlBlocks,\n 'safe-db-types': safeDbTypes,\n};\n"]}
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ const aws_client_mandatory_config_js_1 = __importDefault(require("./rules/aws-cl
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
10
  const no_current_target_in_callback_js_1 = __importDefault(require("./rules/no-current-target-in-callback.js"));
11
+ const no_hydrate_reslocals_js_1 = __importDefault(require("./rules/no-hydrate-reslocals.js"));
11
12
  const no_unused_sql_blocks_js_1 = __importDefault(require("./rules/no-unused-sql-blocks.js"));
12
13
  const safe_db_types_js_1 = __importDefault(require("./rules/safe-db-types.js"));
13
14
  exports.rules = {
@@ -15,6 +16,7 @@ exports.rules = {
15
16
  'aws-client-shared-config': aws_client_shared_config_js_1.default,
16
17
  'jsx-no-dollar-interpolation': jsx_no_dollar_interpolation_js_1.default,
17
18
  'no-current-target-in-callback': no_current_target_in_callback_js_1.default,
19
+ 'no-hydrate-reslocals': no_hydrate_reslocals_js_1.default,
18
20
  'no-unused-sql-blocks': no_unused_sql_blocks_js_1.default,
19
21
  'safe-db-types': safe_db_types_js_1.default,
20
22
  };
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,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"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAAA,4GAA8E;AAC9E,sGAAwE;AACxE,4GAA8E;AAC9E,gHAAiF;AACjF,8FAAiE;AACjE,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,iCAAkB;IAC1C,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 noHydrateResLocals from './rules/no-hydrate-reslocals.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-hydrate-reslocals': noHydrateResLocals,\n 'no-unused-sql-blocks': noUnusedSqlBlocks,\n 'safe-db-types': safeDbTypes,\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"aws-client-mandatory-config.js","sourceRoot":"","sources":["../../src/rules/aws-client-mandatory-config.ts"],"names":[],"mappings":";;AAAA,oDAAuD;AAEvD,0CAAqE;AAErE;;;;;;GAMG;kBACY,mBAAW,CAAC,WAAW,CAAC,WAAW,CAAC;IACjD,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE;YACR,aAAa,EAAE,0DAA0D;SAC1E;QACD,MAAM,EAAE,EAAE;KACX;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO,EAAE;QACd,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAE,CAAC;QAEnC,OAAO;YACL,iCAAiC;YACjC,iBAAiB,CAAC,IAAI,EAAE;gBACtB,MAAM,WAAW,GAAG,IAAA,iDAAsC,EAAC,IAAI,CAAC,CAAC;gBACjE,WAAW,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,gBAAgB,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC;YAAA,CACvE;YACD,aAAa,CAAC,IAAI,EAAE;gBAClB,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,YAAY,IAAI,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;oBAChF,gEAAgE;oBAChE,uDAAuD;oBACvD,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;wBAChC,OAAO,CAAC,MAAM,CAAC;4BACb,IAAI;4BACJ,SAAS,EAAE,eAAe;4BAC1B,IAAI,EAAE;gCACJ,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI;6BAC7B;yBACF,CAAC,CAAC;wBACH,OAAO;oBACT,CAAC;gBACH,CAAC;YAAA,CACF;SACF,CAAC;IAAA,CACH;CACF,CAAC","sourcesContent":["import { ESLintUtils } from '@typescript-eslint/utils';\n\nimport { getAwsClientNamesFromImportDeclaration } from '../utils.js';\n\n/**\n * This rule enforces that we always explicitly provide a config to AWS clients.\n * This helps ensure that we always construct a client with a specific region.\n *\n * This rules works in tandem with `aws-client-shared-config` to ensure that\n * we're properly configuring AWS SDK clients.\n */\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n missingConfig: '{{clientName}} must be constructed with a config object.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const awsClientImports = new Set();\n\n return {\n // Handle `import ...` statements\n ImportDeclaration(node) {\n const clientNames = getAwsClientNamesFromImportDeclaration(node);\n clientNames.forEach((clientName) => awsClientImports.add(clientName));\n },\n NewExpression(node) {\n if (node.callee.type === 'Identifier' && awsClientImports.has(node.callee.name)) {\n // We're constructing an AWS client. Ensure that the call has at\n // least one argument corresponding to a config object.\n if (node.arguments.length === 0) {\n context.report({\n node,\n messageId: 'missingConfig',\n data: {\n clientName: node.callee.name,\n },\n });\n return;\n }\n }\n },\n };\n },\n});\n"]}
1
+ {"version":3,"file":"aws-client-mandatory-config.js","sourceRoot":"","sources":["../../src/rules/aws-client-mandatory-config.ts"],"names":[],"mappings":";;AAAA,oDAAuD;AAEvD,0CAAqE;AAErE;;;;;;GAMG;kBACY,mBAAW,CAAC,WAAW,CAAC,WAAW,CAAC;IACjD,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE;YACR,aAAa,EAAE,0DAA0D;SAC1E;QACD,MAAM,EAAE,EAAE;KACX;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO;QACZ,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAE,CAAC;QAEnC,OAAO;YACL,iCAAiC;YACjC,iBAAiB,CAAC,IAAI;gBACpB,MAAM,WAAW,GAAG,IAAA,iDAAsC,EAAC,IAAI,CAAC,CAAC;gBACjE,WAAW,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,gBAAgB,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC;YACxE,CAAC;YACD,aAAa,CAAC,IAAI;gBAChB,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,YAAY,IAAI,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;oBAChF,gEAAgE;oBAChE,uDAAuD;oBACvD,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;wBAChC,OAAO,CAAC,MAAM,CAAC;4BACb,IAAI;4BACJ,SAAS,EAAE,eAAe;4BAC1B,IAAI,EAAE;gCACJ,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI;6BAC7B;yBACF,CAAC,CAAC;wBACH,OAAO;oBACT,CAAC;gBACH,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["import { ESLintUtils } from '@typescript-eslint/utils';\n\nimport { getAwsClientNamesFromImportDeclaration } from '../utils.js';\n\n/**\n * This rule enforces that we always explicitly provide a config to AWS clients.\n * This helps ensure that we always construct a client with a specific region.\n *\n * This rules works in tandem with `aws-client-shared-config` to ensure that\n * we're properly configuring AWS SDK clients.\n */\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n missingConfig: '{{clientName}} must be constructed with a config object.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const awsClientImports = new Set();\n\n return {\n // Handle `import ...` statements\n ImportDeclaration(node) {\n const clientNames = getAwsClientNamesFromImportDeclaration(node);\n clientNames.forEach((clientName) => awsClientImports.add(clientName));\n },\n NewExpression(node) {\n if (node.callee.type === 'Identifier' && awsClientImports.has(node.callee.name)) {\n // We're constructing an AWS client. Ensure that the call has at\n // least one argument corresponding to a config object.\n if (node.arguments.length === 0) {\n context.report({\n node,\n messageId: 'missingConfig',\n data: {\n clientName: node.callee.name,\n },\n });\n return;\n }\n }\n },\n };\n },\n});\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"aws-client-shared-config.js","sourceRoot":"","sources":["../../src/rules/aws-client-shared-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { ESLintUtils } from '@typescript-eslint/utils';\n\nimport { getAwsClientNamesFromImportDeclaration } from '../utils.js';\n\n/**\n * This ESLint rules enforces that we always provide a \"shared\" config to AWS\n * clients.\n *\n * This rule is extremely opinionated: it checks that the first argument to an\n * AWS client constructor consists of a function call to a function named\n * `makeAwsClientConfig()` (or `makeS3ClientConfig()` for S3 clients). This\n * is our convention to ensure that all clients will reuse credential providers,\n * which is important for ensuring that we don't overload IMDS with requests\n * for credentials if we construct a lot of clients in rapid succession.\n *\n * This is perhaps less than ideal, but the risk of misconfiguring a client is\n * high enough that we err towards being extremely prescriptive about how we\n * configure them.\n *\n * This rules works in tandem with `aws-client-mandatory-config` to ensure that\n * we're properly configuring AWS SDK clients.\n */\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n improperConfig:\n 'Config for {{clientName}} must be obtained by calling {{desiredConfigFunctionName}}().',\n unknownConfig: 'Unknown config provided to AWS client.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const awsClientImports = new Set<string>();\n\n return {\n // Handle `import ...` statements\n ImportDeclaration(node) {\n const clientNames = getAwsClientNamesFromImportDeclaration(node);\n clientNames.forEach((clientName) => awsClientImports.add(clientName));\n },\n NewExpression(node) {\n if (node.callee.type === 'Identifier' && awsClientImports.has(node.callee.name)) {\n // We're constructing an AWS client. Ensure that the first argument\n // comes from one of our config providers.\n\n if (node.arguments.length === 0) {\n // There is no argument to check. If the `aws-client-mandatory-config`\n // rule is enabled, it will catch this case.\n return;\n }\n\n let desiredConfigFunctionName = 'makeAwsClientConfig';\n\n // Special-case: S3 client.\n if (node.callee.name === 'S3Client' || node.callee.name === 'S3') {\n desiredConfigFunctionName = 'makeS3ClientConfig';\n }\n\n const configArgument = node.arguments[0];\n if (configArgument.type !== 'CallExpression') {\n context.report({\n node,\n messageId: 'improperConfig',\n data: {\n clientName: node.callee.name,\n desiredConfigFunctionName,\n },\n });\n return;\n }\n\n // Handle member calls to the function.\n if (\n configArgument.callee.type === 'MemberExpression' &&\n configArgument.callee.property.type === 'Identifier'\n ) {\n const functionName = configArgument.callee.property.name;\n if (functionName !== desiredConfigFunctionName) {\n context.report({\n node,\n messageId: 'improperConfig',\n data: {\n clientName: node.callee.name,\n desiredConfigFunctionName,\n },\n });\n }\n return;\n }\n\n if (configArgument.callee.type === 'Identifier') {\n const functionName = configArgument.callee.name;\n if (functionName !== desiredConfigFunctionName) {\n context.report({\n node,\n messageId: 'improperConfig',\n data: {\n clientName: node.callee.name,\n desiredConfigFunctionName,\n },\n });\n }\n return;\n }\n\n context.report({\n node,\n messageId: 'unknownConfig',\n });\n }\n },\n };\n },\n});\n"]}
1
+ {"version":3,"file":"aws-client-shared-config.js","sourceRoot":"","sources":["../../src/rules/aws-client-shared-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { ESLintUtils } from '@typescript-eslint/utils';\n\nimport { getAwsClientNamesFromImportDeclaration } from '../utils.js';\n\n/**\n * This ESLint rules enforces that we always provide a \"shared\" config to AWS\n * clients.\n *\n * This rule is extremely opinionated: it checks that the first argument to an\n * AWS client constructor consists of a function call to a function named\n * `makeAwsClientConfig()` (or `makeS3ClientConfig()` for S3 clients). This\n * is our convention to ensure that all clients will reuse credential providers,\n * which is important for ensuring that we don't overload IMDS with requests\n * for credentials if we construct a lot of clients in rapid succession.\n *\n * This is perhaps less than ideal, but the risk of misconfiguring a client is\n * high enough that we err towards being extremely prescriptive about how we\n * configure them.\n *\n * This rules works in tandem with `aws-client-mandatory-config` to ensure that\n * we're properly configuring AWS SDK clients.\n */\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n improperConfig:\n 'Config for {{clientName}} must be obtained by calling {{desiredConfigFunctionName}}().',\n unknownConfig: 'Unknown config provided to AWS client.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const awsClientImports = new Set<string>();\n\n return {\n // Handle `import ...` statements\n ImportDeclaration(node) {\n const clientNames = getAwsClientNamesFromImportDeclaration(node);\n clientNames.forEach((clientName) => awsClientImports.add(clientName));\n },\n NewExpression(node) {\n if (node.callee.type === 'Identifier' && awsClientImports.has(node.callee.name)) {\n // We're constructing an AWS client. Ensure that the first argument\n // comes from one of our config providers.\n\n if (node.arguments.length === 0) {\n // There is no argument to check. If the `aws-client-mandatory-config`\n // rule is enabled, it will catch this case.\n return;\n }\n\n let desiredConfigFunctionName = 'makeAwsClientConfig';\n\n // Special-case: S3 client.\n if (node.callee.name === 'S3Client' || node.callee.name === 'S3') {\n desiredConfigFunctionName = 'makeS3ClientConfig';\n }\n\n const configArgument = node.arguments[0];\n if (configArgument.type !== 'CallExpression') {\n context.report({\n node,\n messageId: 'improperConfig',\n data: {\n clientName: node.callee.name,\n desiredConfigFunctionName,\n },\n });\n return;\n }\n\n // Handle member calls to the function.\n if (\n configArgument.callee.type === 'MemberExpression' &&\n configArgument.callee.property.type === 'Identifier'\n ) {\n const functionName = configArgument.callee.property.name;\n if (functionName !== desiredConfigFunctionName) {\n context.report({\n node,\n messageId: 'improperConfig',\n data: {\n clientName: node.callee.name,\n desiredConfigFunctionName,\n },\n });\n }\n return;\n }\n\n if (configArgument.callee.type === 'Identifier') {\n const functionName = configArgument.callee.name;\n if (functionName !== desiredConfigFunctionName) {\n context.report({\n node,\n messageId: 'improperConfig',\n data: {\n clientName: node.callee.name,\n desiredConfigFunctionName,\n },\n });\n }\n return;\n }\n\n context.report({\n node,\n messageId: 'unknownConfig',\n });\n }\n },\n };\n },\n});\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"jsx-no-dollar-interpolation.js","sourceRoot":"","sources":["../../src/rules/jsx-no-dollar-interpolation.ts"],"names":[],"mappings":";;AAAA,oDAAuD;AAEvD;;;;;;;;;GASG;kBACY,mBAAW,CAAC,WAAW,CAAC,WAAW,CAAC;IACjD,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE;YACR,6BAA6B,EAAE,yDAAyD;SACzF;QACD,MAAM,EAAE,EAAE;KACX;IACD,cAAc,EAAE,EAAE;IAElB,MAAM,CAAC,OAAO,EAAE;QACd,OAAO;YACL,UAAU,CAAC,IAAI,EAAE;gBACf,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC;oBACtC,qEAAqE;oBACrE,IAAI,KAAK,KAAK,CAAC;wBAAE,OAAO;oBAExB,qDAAqD;oBACrD,IAAI,KAAK,CAAC,IAAI,KAAK,wBAAwB;wBAAE,OAAO;oBAEpD,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;oBAE/C,qDAAqD;oBACrD,IAAI,aAAa,CAAC,IAAI,KAAK,SAAS;wBAAE,OAAO;oBAE7C,oDAAoD;oBACpD,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC;wBAAE,OAAO;oBAEzD,gEAAgE;oBAChE,6DAA6D;oBAC7D,wBAAwB;oBACxB,MAAM,KAAK,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;oBAC1E,MAAM,SAAS,GAAG,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;oBACvD,MAAM,WAAW,GAAG,KAAK,GAAG,SAAS,CAAC;oBACtC,MAAM,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;oBAEvE,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI;wBACJ,GAAG,EAAE;4BACH,KAAK,EAAE,cAAc;4BACrB,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG;yBACnB;wBACD,SAAS,EAAE,+BAA+B;qBAC3C,CAAC,CAAC;gBAAA,CACJ,CAAC,CAAC;YAAA,CACJ;SACF,CAAC;IAAA,CACH;CACF,CAAC","sourcesContent":["import { ESLintUtils } from '@typescript-eslint/utils';\n\n/**\n * This rule will report things that look like template string interpolations\n * that were improperly converted to JSX. For example, the following code will\n * trigger an error:\n *\n * ```tsx\n * const a = <div>${message}</div>;\n * const b = <div>$ {message}</div>;\n * ```\n */\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n dollarInterpolationNotAllowed: 'Interpolation with a dollar sign is not allowed in JSX.',\n },\n schema: [],\n },\n defaultOptions: [],\n\n create(context) {\n return {\n JSXElement(node) {\n node.children.forEach((child, index) => {\n // Skip the first child since it can't be preceded by a JSXText node.\n if (index === 0) return;\n\n // Skip anything that's not a JSXExpressionContainer.\n if (child.type !== 'JSXExpressionContainer') return;\n\n const previousChild = node.children[index - 1];\n\n // Skip nodes that aren't preceded by a JSXText node.\n if (previousChild.type !== 'JSXText') return;\n\n // Skip nodes that aren't preceded by a dollar sign.\n if (!previousChild.value.trimEnd().endsWith('$')) return;\n\n // Determine the range of characters that should be reported. We\n // include the dollar sign, any following whitespace, and the\n // expression container.\n const start = context.sourceCode.getIndexFromLoc(previousChild.loc.start);\n const lastIndex = previousChild.value.lastIndexOf('$');\n const dollarStart = start + lastIndex;\n const dollarStartLoc = context.sourceCode.getLocFromIndex(dollarStart);\n\n context.report({\n node,\n loc: {\n start: dollarStartLoc,\n end: child.loc.end,\n },\n messageId: 'dollarInterpolationNotAllowed',\n });\n });\n },\n };\n },\n});\n"]}
1
+ {"version":3,"file":"jsx-no-dollar-interpolation.js","sourceRoot":"","sources":["../../src/rules/jsx-no-dollar-interpolation.ts"],"names":[],"mappings":";;AAAA,oDAAuD;AAEvD;;;;;;;;;GASG;kBACY,mBAAW,CAAC,WAAW,CAAC,WAAW,CAAC;IACjD,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE;YACR,6BAA6B,EAAE,yDAAyD;SACzF;QACD,MAAM,EAAE,EAAE;KACX;IACD,cAAc,EAAE,EAAE;IAElB,MAAM,CAAC,OAAO;QACZ,OAAO;YACL,UAAU,CAAC,IAAI;gBACb,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;oBACrC,qEAAqE;oBACrE,IAAI,KAAK,KAAK,CAAC;wBAAE,OAAO;oBAExB,qDAAqD;oBACrD,IAAI,KAAK,CAAC,IAAI,KAAK,wBAAwB;wBAAE,OAAO;oBAEpD,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;oBAE/C,qDAAqD;oBACrD,IAAI,aAAa,CAAC,IAAI,KAAK,SAAS;wBAAE,OAAO;oBAE7C,oDAAoD;oBACpD,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC;wBAAE,OAAO;oBAEzD,gEAAgE;oBAChE,6DAA6D;oBAC7D,wBAAwB;oBACxB,MAAM,KAAK,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;oBAC1E,MAAM,SAAS,GAAG,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;oBACvD,MAAM,WAAW,GAAG,KAAK,GAAG,SAAS,CAAC;oBACtC,MAAM,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;oBAEvE,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI;wBACJ,GAAG,EAAE;4BACH,KAAK,EAAE,cAAc;4BACrB,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG;yBACnB;wBACD,SAAS,EAAE,+BAA+B;qBAC3C,CAAC,CAAC;gBACL,CAAC,CAAC,CAAC;YACL,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["import { ESLintUtils } from '@typescript-eslint/utils';\n\n/**\n * This rule will report things that look like template string interpolations\n * that were improperly converted to JSX. For example, the following code will\n * trigger an error:\n *\n * ```tsx\n * const a = <div>${message}</div>;\n * const b = <div>$ {message}</div>;\n * ```\n */\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n dollarInterpolationNotAllowed: 'Interpolation with a dollar sign is not allowed in JSX.',\n },\n schema: [],\n },\n defaultOptions: [],\n\n create(context) {\n return {\n JSXElement(node) {\n node.children.forEach((child, index) => {\n // Skip the first child since it can't be preceded by a JSXText node.\n if (index === 0) return;\n\n // Skip anything that's not a JSXExpressionContainer.\n if (child.type !== 'JSXExpressionContainer') return;\n\n const previousChild = node.children[index - 1];\n\n // Skip nodes that aren't preceded by a JSXText node.\n if (previousChild.type !== 'JSXText') return;\n\n // Skip nodes that aren't preceded by a dollar sign.\n if (!previousChild.value.trimEnd().endsWith('$')) return;\n\n // Determine the range of characters that should be reported. We\n // include the dollar sign, any following whitespace, and the\n // expression container.\n const start = context.sourceCode.getIndexFromLoc(previousChild.loc.start);\n const lastIndex = previousChild.value.lastIndexOf('$');\n const dollarStart = start + lastIndex;\n const dollarStartLoc = context.sourceCode.getLocFromIndex(dollarStart);\n\n context.report({\n node,\n loc: {\n start: dollarStartLoc,\n end: child.loc.end,\n },\n messageId: 'dollarInterpolationNotAllowed',\n });\n });\n },\n };\n },\n});\n"]}
@@ -1,9 +1,9 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
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.
3
+ * This rule detects when `event.currentTarget` is accessed after an `await` or
4
+ * inside a callback passed to a likely deferred API within a React event
5
+ * handler. Those callbacks may run after the handler returns, at which point
6
+ * `currentTarget` may already be nullified.
7
7
  *
8
8
  * Bad:
9
9
  * ```tsx
@@ -1 +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"]}
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,wBAwYG","sourcesContent":["import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils';\n\n/**\n * This rule detects when `event.currentTarget` is accessed after an `await` or\n * inside a callback passed to a likely deferred API within a React event\n * handler. Those callbacks may run after the handler returns, at which point\n * `currentTarget` may already be nullified.\n *\n * Bad:\n * ```tsx\n * onChange={(e) => setChecks((c) => ({ ...c, value: e.currentTarget.checked }))}\n * ```\n *\n * Good:\n * ```tsx\n * onChange={({ currentTarget }) => setChecks((c) => ({ ...c, value: currentTarget.checked }))}\n * ```\n */\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n noCurrentTargetInCallback:\n 'Accessing event.currentTarget after an async boundary may fail because currentTarget can be nullified before the access runs. Destructure currentTarget at the start of the event handler instead.',\n },\n schema: [],\n },\n defaultOptions: [],\n\n create(context) {\n const trackedEventFunctions = new Map<TSESTree.Node, TSESTree.Identifier>();\n const visitorKeys = context.sourceCode.visitorKeys;\n\n function isFunctionLike(\n node: TSESTree.Node,\n ): node is\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration {\n return (\n node.type === 'ArrowFunctionExpression' ||\n node.type === 'FunctionExpression' ||\n node.type === 'FunctionDeclaration'\n );\n }\n\n function isNode(value: unknown): value is TSESTree.Node {\n return typeof value === 'object' && value !== null && 'type' in value;\n }\n\n function getChildNodes(node: TSESTree.Node): TSESTree.Node[] {\n const children: TSESTree.Node[] = [];\n\n for (const key of visitorKeys[node.type] ?? []) {\n const value: unknown = node[key as keyof TSESTree.Node];\n\n if (Array.isArray(value)) {\n for (const child of value) {\n if (isNode(child)) {\n children.push(child);\n }\n }\n } else if (isNode(value)) {\n children.push(value);\n }\n }\n\n return children;\n }\n\n function isKnownDeferredCallee(callee: TSESTree.Expression | TSESTree.Super): boolean {\n if (callee.type === 'Super') {\n return false;\n }\n\n if (callee.type === 'Identifier') {\n return (\n // e.g. setTimeout, setInterval, setImmediate, etc.\n /^set[A-Z0-9_]/.test(callee.name) ||\n callee.name === 'queueMicrotask' ||\n callee.name === 'requestAnimationFrame' ||\n callee.name === 'requestIdleCallback'\n );\n }\n\n if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {\n return (\n callee.property.name === 'then' ||\n callee.property.name === 'catch' ||\n callee.property.name === 'finally'\n );\n }\n\n return false;\n }\n\n function isKnownEventBoundaryCallback(\n callExpression: TSESTree.CallExpression,\n callbackIndex: number,\n ): boolean {\n if (callbackIndex !== 0) {\n return false;\n }\n\n if (\n callExpression.callee.type === 'Identifier' &&\n callExpression.callee.name === 'handleSubmit'\n ) {\n // react-hook-form invokes this callback after its own submit handling,\n // so treat it as an event boundary like other deferred callbacks.\n return true;\n }\n\n return false;\n }\n\n /**\n * Check if a node is inside a nested function that is passed to a callback\n * API that may invoke it after the event handler returns.\n */\n function isInsideDeferredCallback(\n node: TSESTree.Node,\n eventHandlerFunction: TSESTree.Node,\n ): boolean {\n // Check if the event handler itself is in a deferred/boundary context\n const handlerParent = eventHandlerFunction.parent;\n if (handlerParent?.type === 'CallExpression') {\n const callbackIndex = handlerParent.arguments.indexOf(\n eventHandlerFunction as TSESTree.CallExpressionArgument,\n );\n if (\n callbackIndex !== -1 &&\n (isKnownDeferredCallee(handlerParent.callee) ||\n isKnownEventBoundaryCallback(handlerParent, callbackIndex))\n ) {\n return true;\n }\n }\n\n let current: TSESTree.Node | undefined = node;\n\n while (current && current !== eventHandlerFunction) {\n if (isFunctionLike(current)) {\n const parent = current.parent;\n if (current.type !== 'FunctionDeclaration' && parent?.type === 'CallExpression') {\n const callbackIndex = parent.arguments.indexOf(current);\n if (\n callbackIndex !== -1 &&\n (isKnownDeferredCallee(parent.callee) ||\n isKnownEventBoundaryCallback(parent, callbackIndex))\n ) {\n return true;\n }\n }\n }\n current = current.parent;\n }\n\n return false;\n }\n\n /** Check if the function contains an await before this access, ignoring nested functions */\n function hasPriorAwaitInSameFunction(\n node: TSESTree.Node,\n eventHandlerFunction: TSESTree.Node,\n ): boolean {\n if (\n !('body' in eventHandlerFunction) ||\n !eventHandlerFunction.body ||\n Array.isArray(eventHandlerFunction.body)\n ) {\n return false;\n }\n\n let foundPriorAwait = false;\n\n const visit = (current: TSESTree.Node) => {\n if (foundPriorAwait || current.range[0] >= node.range[0]) {\n return;\n }\n\n if (current !== eventHandlerFunction && isFunctionLike(current)) {\n return;\n }\n\n if (current.type === 'AwaitExpression') {\n foundPriorAwait = true;\n return;\n }\n\n for (const child of getChildNodes(current)) {\n visit(child);\n if (foundPriorAwait) {\n return;\n }\n }\n };\n\n visit(eventHandlerFunction.body);\n return foundPriorAwait;\n }\n\n /** Find the function that contains this node */\n function findContainingFunction(\n node: TSESTree.Node,\n ):\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration\n | null {\n let current: TSESTree.Node | undefined = node.parent;\n\n while (current) {\n if (isFunctionLike(current)) {\n return current;\n }\n current = current.parent;\n }\n\n return null;\n }\n\n /** Check if a function is a JSX event handler */\n function isJSXEventHandler(\n func:\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration,\n ): boolean {\n const parent = func.parent;\n\n // Check for JSX attribute like onChange={...}\n if (parent?.type === 'JSXExpressionContainer') {\n const jsxAttribute = parent.parent;\n if (jsxAttribute?.type === 'JSXAttribute') {\n const attrName = jsxAttribute.name.type === 'JSXIdentifier' ? jsxAttribute.name.name : '';\n // Match common event handler patterns\n return /^on[A-Z]/.test(attrName);\n }\n }\n\n return false;\n }\n\n /** Get the first parameter of an event handler if it looks like an event */\n function getEventParameter(\n func:\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration,\n ): TSESTree.Identifier | null {\n const firstParam = func.params[0];\n\n if (!firstParam) return null;\n\n // Simple identifier parameter like (e) => ...\n if (firstParam.type === 'Identifier') {\n return firstParam;\n }\n\n return null;\n }\n\n function isReactEventType(typeAnnotation: TSESTree.TypeNode): boolean {\n if (typeAnnotation.type === 'TSTypeReference') {\n if (\n typeAnnotation.typeName.type === 'TSQualifiedName' &&\n typeAnnotation.typeName.left.type === 'Identifier' &&\n typeAnnotation.typeName.left.name === 'React' &&\n typeAnnotation.typeName.right.type === 'Identifier' &&\n typeAnnotation.typeName.right.name.endsWith('Event')\n ) {\n return true;\n }\n\n if (\n typeAnnotation.typeName.type === 'Identifier' &&\n (typeAnnotation.typeName.name === 'SyntheticEvent' ||\n typeAnnotation.typeName.name.endsWith('Event'))\n ) {\n return true;\n }\n }\n\n return false;\n }\n\n function isTypedReactEventHandler(\n func:\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration,\n ): boolean {\n const firstParam = func.params[0];\n if (!firstParam || firstParam.type !== 'Identifier' || !firstParam.typeAnnotation) {\n return false;\n }\n\n return isReactEventType(firstParam.typeAnnotation.typeAnnotation);\n }\n\n function trackIfEventHandler(\n node:\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration,\n ) {\n if (isJSXEventHandler(node) || isTypedReactEventHandler(node)) {\n const eventParam = getEventParameter(node);\n if (eventParam) {\n trackedEventFunctions.set(node, eventParam);\n }\n return;\n }\n\n // react-hook-form: handleSubmit((_data, e) => { ... })\n // The event is the second parameter, and the entire callback is deferred.\n const parent = node.parent;\n if (\n parent?.type === 'CallExpression' &&\n parent.arguments[0] === node &&\n isKnownEventBoundaryCallback(parent, 0)\n ) {\n const secondParam = node.params[1];\n if (secondParam?.type === 'Identifier') {\n trackedEventFunctions.set(node, secondParam);\n }\n }\n }\n\n return {\n // When we enter a JSX event handler, track its event parameter\n 'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression': (\n node: TSESTree.ArrowFunctionExpression,\n ) => {\n trackIfEventHandler(node);\n },\n 'JSXAttribute > JSXExpressionContainer > FunctionExpression': (\n node: TSESTree.FunctionExpression,\n ) => {\n trackIfEventHandler(node);\n },\n ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression) {\n trackIfEventHandler(node);\n },\n FunctionExpression(node: TSESTree.FunctionExpression) {\n trackIfEventHandler(node);\n },\n FunctionDeclaration(node: TSESTree.FunctionDeclaration) {\n trackIfEventHandler(node);\n },\n\n // Check for .currentTarget access\n MemberExpression(node) {\n // Only check for .currentTarget property access\n if (node.property.type !== 'Identifier' || node.property.name !== 'currentTarget') {\n return;\n }\n\n // Check if the object is an identifier (like `e` in `e.currentTarget`)\n if (node.object.type !== 'Identifier') {\n return;\n }\n\n const objectName = node.object.name;\n\n // Find the containing function\n const containingFunction = findContainingFunction(node);\n if (!containingFunction) return;\n\n // Check each tracked event handler to see if this access is problematic\n for (const [eventHandler, eventParam] of trackedEventFunctions) {\n // Check if this is accessing the event parameter\n if (objectName !== eventParam.name) continue;\n\n if (\n isInsideDeferredCallback(node, eventHandler) ||\n hasPriorAwaitInSameFunction(node, eventHandler)\n ) {\n context.report({\n node,\n messageId: 'noCurrentTargetInCallback',\n });\n return;\n }\n }\n },\n\n // Clean up when leaving event handlers\n 'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression:exit': (\n node: TSESTree.ArrowFunctionExpression,\n ) => {\n trackedEventFunctions.delete(node);\n },\n 'JSXAttribute > JSXExpressionContainer > FunctionExpression:exit': (\n node: TSESTree.FunctionExpression,\n ) => {\n trackedEventFunctions.delete(node);\n },\n 'ArrowFunctionExpression:exit'(node: TSESTree.ArrowFunctionExpression) {\n trackedEventFunctions.delete(node);\n },\n 'FunctionExpression:exit'(node: TSESTree.FunctionExpression) {\n trackedEventFunctions.delete(node);\n },\n 'FunctionDeclaration:exit'(node: TSESTree.FunctionDeclaration) {\n trackedEventFunctions.delete(node);\n },\n };\n },\n});\n"]}
@@ -2,10 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const utils_1 = require("@typescript-eslint/utils");
4
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.
5
+ * This rule detects when `event.currentTarget` is accessed after an `await` or
6
+ * inside a callback passed to a likely deferred API within a React event
7
+ * handler. Those callbacks may run after the handler returns, at which point
8
+ * `currentTarget` may already be nullified.
9
9
  *
10
10
  * Bad:
11
11
  * ```tsx
@@ -21,33 +21,136 @@ exports.default = utils_1.ESLintUtils.RuleCreator.withoutDocs({
21
21
  meta: {
22
22
  type: 'problem',
23
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.',
24
+ noCurrentTargetInCallback: 'Accessing event.currentTarget after an async boundary may fail because currentTarget can be nullified before the access runs. Destructure currentTarget at the start of the event handler instead.',
25
25
  },
26
26
  schema: [],
27
27
  },
28
28
  defaultOptions: [],
29
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;
30
+ const trackedEventFunctions = new Map();
31
+ const visitorKeys = context.sourceCode.visitorKeys;
32
+ function isFunctionLike(node) {
33
+ return (node.type === 'ArrowFunctionExpression' ||
34
+ node.type === 'FunctionExpression' ||
35
+ node.type === 'FunctionDeclaration');
36
+ }
37
+ function isNode(value) {
38
+ return typeof value === 'object' && value !== null && 'type' in value;
39
+ }
40
+ function getChildNodes(node) {
41
+ const children = [];
42
+ for (const key of visitorKeys[node.type] ?? []) {
43
+ const value = node[key];
44
+ if (Array.isArray(value)) {
45
+ for (const child of value) {
46
+ if (isNode(child)) {
47
+ children.push(child);
48
+ }
49
+ }
50
+ }
51
+ else if (isNode(value)) {
52
+ children.push(value);
53
+ }
54
+ }
55
+ return children;
56
+ }
57
+ function isKnownDeferredCallee(callee) {
58
+ if (callee.type === 'Super') {
59
+ return false;
60
+ }
61
+ if (callee.type === 'Identifier') {
62
+ return (
63
+ // e.g. setTimeout, setInterval, setImmediate, etc.
64
+ /^set[A-Z0-9_]/.test(callee.name) ||
65
+ callee.name === 'queueMicrotask' ||
66
+ callee.name === 'requestAnimationFrame' ||
67
+ callee.name === 'requestIdleCallback');
68
+ }
69
+ if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
70
+ return (callee.property.name === 'then' ||
71
+ callee.property.name === 'catch' ||
72
+ callee.property.name === 'finally');
73
+ }
74
+ return false;
75
+ }
76
+ function isKnownEventBoundaryCallback(callExpression, callbackIndex) {
77
+ if (callbackIndex !== 0) {
78
+ return false;
79
+ }
80
+ if (callExpression.callee.type === 'Identifier' &&
81
+ callExpression.callee.name === 'handleSubmit') {
82
+ // react-hook-form invokes this callback after its own submit handling,
83
+ // so treat it as an event boundary like other deferred callbacks.
84
+ return true;
85
+ }
86
+ return false;
87
+ }
88
+ /**
89
+ * Check if a node is inside a nested function that is passed to a callback
90
+ * API that may invoke it after the event handler returns.
91
+ */
92
+ function isInsideDeferredCallback(node, eventHandlerFunction) {
93
+ // Check if the event handler itself is in a deferred/boundary context
94
+ const handlerParent = eventHandlerFunction.parent;
95
+ if (handlerParent?.type === 'CallExpression') {
96
+ const callbackIndex = handlerParent.arguments.indexOf(eventHandlerFunction);
97
+ if (callbackIndex !== -1 &&
98
+ (isKnownDeferredCallee(handlerParent.callee) ||
99
+ isKnownEventBoundaryCallback(handlerParent, callbackIndex))) {
100
+ return true;
101
+ }
102
+ }
103
+ let current = node;
36
104
  while (current && current !== eventHandlerFunction) {
37
- if (current.type === 'ArrowFunctionExpression' ||
38
- current.type === 'FunctionExpression' ||
39
- current.type === 'FunctionDeclaration') {
40
- foundNestedFunction = true;
105
+ if (isFunctionLike(current)) {
106
+ const parent = current.parent;
107
+ if (current.type !== 'FunctionDeclaration' && parent?.type === 'CallExpression') {
108
+ const callbackIndex = parent.arguments.indexOf(current);
109
+ if (callbackIndex !== -1 &&
110
+ (isKnownDeferredCallee(parent.callee) ||
111
+ isKnownEventBoundaryCallback(parent, callbackIndex))) {
112
+ return true;
113
+ }
114
+ }
41
115
  }
42
116
  current = current.parent;
43
117
  }
44
- return foundNestedFunction && current === eventHandlerFunction;
118
+ return false;
119
+ }
120
+ /** Check if the function contains an await before this access, ignoring nested functions */
121
+ function hasPriorAwaitInSameFunction(node, eventHandlerFunction) {
122
+ if (!('body' in eventHandlerFunction) ||
123
+ !eventHandlerFunction.body ||
124
+ Array.isArray(eventHandlerFunction.body)) {
125
+ return false;
126
+ }
127
+ let foundPriorAwait = false;
128
+ const visit = (current) => {
129
+ if (foundPriorAwait || current.range[0] >= node.range[0]) {
130
+ return;
131
+ }
132
+ if (current !== eventHandlerFunction && isFunctionLike(current)) {
133
+ return;
134
+ }
135
+ if (current.type === 'AwaitExpression') {
136
+ foundPriorAwait = true;
137
+ return;
138
+ }
139
+ for (const child of getChildNodes(current)) {
140
+ visit(child);
141
+ if (foundPriorAwait) {
142
+ return;
143
+ }
144
+ }
145
+ };
146
+ visit(eventHandlerFunction.body);
147
+ return foundPriorAwait;
45
148
  }
46
149
  /** Find the function that contains this node */
47
150
  function findContainingFunction(node) {
48
151
  let current = node.parent;
49
152
  while (current) {
50
- if (current.type === 'ArrowFunctionExpression' || current.type === 'FunctionExpression') {
153
+ if (isFunctionLike(current)) {
51
154
  return current;
52
155
  }
53
156
  current = current.parent;
@@ -79,23 +182,66 @@ exports.default = utils_1.ESLintUtils.RuleCreator.withoutDocs({
79
182
  }
80
183
  return null;
81
184
  }
185
+ function isReactEventType(typeAnnotation) {
186
+ if (typeAnnotation.type === 'TSTypeReference') {
187
+ if (typeAnnotation.typeName.type === 'TSQualifiedName' &&
188
+ typeAnnotation.typeName.left.type === 'Identifier' &&
189
+ typeAnnotation.typeName.left.name === 'React' &&
190
+ typeAnnotation.typeName.right.type === 'Identifier' &&
191
+ typeAnnotation.typeName.right.name.endsWith('Event')) {
192
+ return true;
193
+ }
194
+ if (typeAnnotation.typeName.type === 'Identifier' &&
195
+ (typeAnnotation.typeName.name === 'SyntheticEvent' ||
196
+ typeAnnotation.typeName.name.endsWith('Event'))) {
197
+ return true;
198
+ }
199
+ }
200
+ return false;
201
+ }
202
+ function isTypedReactEventHandler(func) {
203
+ const firstParam = func.params[0];
204
+ if (!firstParam || firstParam.type !== 'Identifier' || !firstParam.typeAnnotation) {
205
+ return false;
206
+ }
207
+ return isReactEventType(firstParam.typeAnnotation.typeAnnotation);
208
+ }
209
+ function trackIfEventHandler(node) {
210
+ if (isJSXEventHandler(node) || isTypedReactEventHandler(node)) {
211
+ const eventParam = getEventParameter(node);
212
+ if (eventParam) {
213
+ trackedEventFunctions.set(node, eventParam);
214
+ }
215
+ return;
216
+ }
217
+ // react-hook-form: handleSubmit((_data, e) => { ... })
218
+ // The event is the second parameter, and the entire callback is deferred.
219
+ const parent = node.parent;
220
+ if (parent?.type === 'CallExpression' &&
221
+ parent.arguments[0] === node &&
222
+ isKnownEventBoundaryCallback(parent, 0)) {
223
+ const secondParam = node.params[1];
224
+ if (secondParam?.type === 'Identifier') {
225
+ trackedEventFunctions.set(node, secondParam);
226
+ }
227
+ }
228
+ }
82
229
  return {
83
230
  // When we enter a JSX event handler, track its event parameter
84
231
  'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression': (node) => {
85
- if (isJSXEventHandler(node)) {
86
- const eventParam = getEventParameter(node);
87
- if (eventParam) {
88
- eventHandlerParams.set(node, eventParam);
89
- }
90
- }
232
+ trackIfEventHandler(node);
91
233
  },
92
234
  'JSXAttribute > JSXExpressionContainer > FunctionExpression': (node) => {
93
- if (isJSXEventHandler(node)) {
94
- const eventParam = getEventParameter(node);
95
- if (eventParam) {
96
- eventHandlerParams.set(node, eventParam);
97
- }
98
- }
235
+ trackIfEventHandler(node);
236
+ },
237
+ ArrowFunctionExpression(node) {
238
+ trackIfEventHandler(node);
239
+ },
240
+ FunctionExpression(node) {
241
+ trackIfEventHandler(node);
242
+ },
243
+ FunctionDeclaration(node) {
244
+ trackIfEventHandler(node);
99
245
  },
100
246
  // Check for .currentTarget access
101
247
  MemberExpression(node) {
@@ -113,12 +259,12 @@ exports.default = utils_1.ESLintUtils.RuleCreator.withoutDocs({
113
259
  if (!containingFunction)
114
260
  return;
115
261
  // Check each tracked event handler to see if this access is problematic
116
- for (const [eventHandler, eventParam] of eventHandlerParams) {
262
+ for (const [eventHandler, eventParam] of trackedEventFunctions) {
117
263
  // Check if this is accessing the event parameter
118
264
  if (objectName !== eventParam.name)
119
265
  continue;
120
- // Check if the access is inside a nested function within the event handler
121
- if (isInsideNestedFunction(node, eventHandler)) {
266
+ if (isInsideDeferredCallback(node, eventHandler) ||
267
+ hasPriorAwaitInSameFunction(node, eventHandler)) {
122
268
  context.report({
123
269
  node,
124
270
  messageId: 'noCurrentTargetInCallback',
@@ -129,10 +275,19 @@ exports.default = utils_1.ESLintUtils.RuleCreator.withoutDocs({
129
275
  },
130
276
  // Clean up when leaving event handlers
131
277
  'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression:exit': (node) => {
132
- eventHandlerParams.delete(node);
278
+ trackedEventFunctions.delete(node);
133
279
  },
134
280
  'JSXAttribute > JSXExpressionContainer > FunctionExpression:exit': (node) => {
135
- eventHandlerParams.delete(node);
281
+ trackedEventFunctions.delete(node);
282
+ },
283
+ 'ArrowFunctionExpression:exit'(node) {
284
+ trackedEventFunctions.delete(node);
285
+ },
286
+ 'FunctionExpression:exit'(node) {
287
+ trackedEventFunctions.delete(node);
288
+ },
289
+ 'FunctionDeclaration:exit'(node) {
290
+ trackedEventFunctions.delete(node);
136
291
  },
137
292
  };
138
293
  },
@@ -1 +1 @@
1
- {"version":3,"file":"no-current-target-in-callback.js","sourceRoot":"","sources":["../../src/rules/no-current-target-in-callback.ts"],"names":[],"mappings":"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","sourcesContent":["import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils';\n\n/**\n * This rule detects when `event.currentTarget` is accessed inside a nested\n * callback function within a React event handler. This is problematic because\n * React may execute callbacks (like those passed to setState) asynchronously,\n * at which point `currentTarget` may already be nullified.\n *\n * Bad:\n * ```tsx\n * onChange={(e) => setChecks((c) => ({ ...c, value: e.currentTarget.checked }))}\n * ```\n *\n * Good:\n * ```tsx\n * onChange={({ currentTarget }) => setChecks((c) => ({ ...c, value: currentTarget.checked }))}\n * ```\n */\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n noCurrentTargetInCallback:\n 'Accessing event.currentTarget inside a callback may fail because currentTarget can be nullified before the callback runs. Destructure currentTarget at the start of the event handler instead.',\n },\n schema: [],\n },\n defaultOptions: [],\n\n create(context) {\n // Track event handler parameters and their scopes\n const eventHandlerParams = new Map<TSESTree.Node, TSESTree.Identifier>();\n\n /** Check if a node is inside a nested function relative to the event handler */\n function isInsideNestedFunction(\n node: TSESTree.Node,\n eventHandlerFunction: TSESTree.Node,\n ): boolean {\n let current: TSESTree.Node | undefined = node.parent;\n let foundNestedFunction = false;\n\n while (current && current !== eventHandlerFunction) {\n if (\n current.type === 'ArrowFunctionExpression' ||\n current.type === 'FunctionExpression' ||\n current.type === 'FunctionDeclaration'\n ) {\n foundNestedFunction = true;\n }\n current = current.parent;\n }\n\n return foundNestedFunction && current === eventHandlerFunction;\n }\n\n /** Find the function that contains this node */\n function findContainingFunction(\n node: TSESTree.Node,\n ): TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | null {\n let current: TSESTree.Node | undefined = node.parent;\n\n while (current) {\n if (current.type === 'ArrowFunctionExpression' || current.type === 'FunctionExpression') {\n return current;\n }\n current = current.parent;\n }\n\n return null;\n }\n\n /** Check if a function is a JSX event handler */\n function isJSXEventHandler(\n func: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,\n ): boolean {\n const parent = func.parent;\n\n // Check for JSX attribute like onChange={...}\n if (parent?.type === 'JSXExpressionContainer') {\n const jsxAttribute = parent.parent;\n if (jsxAttribute?.type === 'JSXAttribute') {\n const attrName = jsxAttribute.name.type === 'JSXIdentifier' ? jsxAttribute.name.name : '';\n // Match common event handler patterns\n return /^on[A-Z]/.test(attrName);\n }\n }\n\n return false;\n }\n\n /** Get the first parameter of an event handler if it looks like an event */\n function getEventParameter(\n func: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,\n ): TSESTree.Identifier | null {\n const firstParam = func.params[0];\n\n if (!firstParam) return null;\n\n // Simple identifier parameter like (e) => ...\n if (firstParam.type === 'Identifier') {\n return firstParam;\n }\n\n return null;\n }\n\n return {\n // When we enter a JSX event handler, track its event parameter\n 'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression': (\n node: TSESTree.ArrowFunctionExpression,\n ) => {\n if (isJSXEventHandler(node)) {\n const eventParam = getEventParameter(node);\n if (eventParam) {\n eventHandlerParams.set(node, eventParam);\n }\n }\n },\n 'JSXAttribute > JSXExpressionContainer > FunctionExpression': (\n node: TSESTree.FunctionExpression,\n ) => {\n if (isJSXEventHandler(node)) {\n const eventParam = getEventParameter(node);\n if (eventParam) {\n eventHandlerParams.set(node, eventParam);\n }\n }\n },\n\n // Check for .currentTarget access\n MemberExpression(node) {\n // Only check for .currentTarget property access\n if (node.property.type !== 'Identifier' || node.property.name !== 'currentTarget') {\n return;\n }\n\n // Check if the object is an identifier (like `e` in `e.currentTarget`)\n if (node.object.type !== 'Identifier') {\n return;\n }\n\n const objectName = node.object.name;\n\n // Find the containing function\n const containingFunction = findContainingFunction(node);\n if (!containingFunction) return;\n\n // Check each tracked event handler to see if this access is problematic\n for (const [eventHandler, eventParam] of eventHandlerParams) {\n // Check if this is accessing the event parameter\n if (objectName !== eventParam.name) continue;\n\n // Check if the access is inside a nested function within the event handler\n if (isInsideNestedFunction(node, eventHandler)) {\n context.report({\n node,\n messageId: 'noCurrentTargetInCallback',\n });\n return;\n }\n }\n },\n\n // Clean up when leaving event handlers\n 'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression:exit': (\n node: TSESTree.ArrowFunctionExpression,\n ) => {\n eventHandlerParams.delete(node);\n },\n 'JSXAttribute > JSXExpressionContainer > FunctionExpression:exit': (\n node: TSESTree.FunctionExpression,\n ) => {\n eventHandlerParams.delete(node);\n },\n };\n },\n});\n"]}
1
+ {"version":3,"file":"no-current-target-in-callback.js","sourceRoot":"","sources":["../../src/rules/no-current-target-in-callback.ts"],"names":[],"mappings":"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","sourcesContent":["import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils';\n\n/**\n * This rule detects when `event.currentTarget` is accessed after an `await` or\n * inside a callback passed to a likely deferred API within a React event\n * handler. Those callbacks may run after the handler returns, at which point\n * `currentTarget` may already be nullified.\n *\n * Bad:\n * ```tsx\n * onChange={(e) => setChecks((c) => ({ ...c, value: e.currentTarget.checked }))}\n * ```\n *\n * Good:\n * ```tsx\n * onChange={({ currentTarget }) => setChecks((c) => ({ ...c, value: currentTarget.checked }))}\n * ```\n */\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n noCurrentTargetInCallback:\n 'Accessing event.currentTarget after an async boundary may fail because currentTarget can be nullified before the access runs. Destructure currentTarget at the start of the event handler instead.',\n },\n schema: [],\n },\n defaultOptions: [],\n\n create(context) {\n const trackedEventFunctions = new Map<TSESTree.Node, TSESTree.Identifier>();\n const visitorKeys = context.sourceCode.visitorKeys;\n\n function isFunctionLike(\n node: TSESTree.Node,\n ): node is\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration {\n return (\n node.type === 'ArrowFunctionExpression' ||\n node.type === 'FunctionExpression' ||\n node.type === 'FunctionDeclaration'\n );\n }\n\n function isNode(value: unknown): value is TSESTree.Node {\n return typeof value === 'object' && value !== null && 'type' in value;\n }\n\n function getChildNodes(node: TSESTree.Node): TSESTree.Node[] {\n const children: TSESTree.Node[] = [];\n\n for (const key of visitorKeys[node.type] ?? []) {\n const value: unknown = node[key as keyof TSESTree.Node];\n\n if (Array.isArray(value)) {\n for (const child of value) {\n if (isNode(child)) {\n children.push(child);\n }\n }\n } else if (isNode(value)) {\n children.push(value);\n }\n }\n\n return children;\n }\n\n function isKnownDeferredCallee(callee: TSESTree.Expression | TSESTree.Super): boolean {\n if (callee.type === 'Super') {\n return false;\n }\n\n if (callee.type === 'Identifier') {\n return (\n // e.g. setTimeout, setInterval, setImmediate, etc.\n /^set[A-Z0-9_]/.test(callee.name) ||\n callee.name === 'queueMicrotask' ||\n callee.name === 'requestAnimationFrame' ||\n callee.name === 'requestIdleCallback'\n );\n }\n\n if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {\n return (\n callee.property.name === 'then' ||\n callee.property.name === 'catch' ||\n callee.property.name === 'finally'\n );\n }\n\n return false;\n }\n\n function isKnownEventBoundaryCallback(\n callExpression: TSESTree.CallExpression,\n callbackIndex: number,\n ): boolean {\n if (callbackIndex !== 0) {\n return false;\n }\n\n if (\n callExpression.callee.type === 'Identifier' &&\n callExpression.callee.name === 'handleSubmit'\n ) {\n // react-hook-form invokes this callback after its own submit handling,\n // so treat it as an event boundary like other deferred callbacks.\n return true;\n }\n\n return false;\n }\n\n /**\n * Check if a node is inside a nested function that is passed to a callback\n * API that may invoke it after the event handler returns.\n */\n function isInsideDeferredCallback(\n node: TSESTree.Node,\n eventHandlerFunction: TSESTree.Node,\n ): boolean {\n // Check if the event handler itself is in a deferred/boundary context\n const handlerParent = eventHandlerFunction.parent;\n if (handlerParent?.type === 'CallExpression') {\n const callbackIndex = handlerParent.arguments.indexOf(\n eventHandlerFunction as TSESTree.CallExpressionArgument,\n );\n if (\n callbackIndex !== -1 &&\n (isKnownDeferredCallee(handlerParent.callee) ||\n isKnownEventBoundaryCallback(handlerParent, callbackIndex))\n ) {\n return true;\n }\n }\n\n let current: TSESTree.Node | undefined = node;\n\n while (current && current !== eventHandlerFunction) {\n if (isFunctionLike(current)) {\n const parent = current.parent;\n if (current.type !== 'FunctionDeclaration' && parent?.type === 'CallExpression') {\n const callbackIndex = parent.arguments.indexOf(current);\n if (\n callbackIndex !== -1 &&\n (isKnownDeferredCallee(parent.callee) ||\n isKnownEventBoundaryCallback(parent, callbackIndex))\n ) {\n return true;\n }\n }\n }\n current = current.parent;\n }\n\n return false;\n }\n\n /** Check if the function contains an await before this access, ignoring nested functions */\n function hasPriorAwaitInSameFunction(\n node: TSESTree.Node,\n eventHandlerFunction: TSESTree.Node,\n ): boolean {\n if (\n !('body' in eventHandlerFunction) ||\n !eventHandlerFunction.body ||\n Array.isArray(eventHandlerFunction.body)\n ) {\n return false;\n }\n\n let foundPriorAwait = false;\n\n const visit = (current: TSESTree.Node) => {\n if (foundPriorAwait || current.range[0] >= node.range[0]) {\n return;\n }\n\n if (current !== eventHandlerFunction && isFunctionLike(current)) {\n return;\n }\n\n if (current.type === 'AwaitExpression') {\n foundPriorAwait = true;\n return;\n }\n\n for (const child of getChildNodes(current)) {\n visit(child);\n if (foundPriorAwait) {\n return;\n }\n }\n };\n\n visit(eventHandlerFunction.body);\n return foundPriorAwait;\n }\n\n /** Find the function that contains this node */\n function findContainingFunction(\n node: TSESTree.Node,\n ):\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration\n | null {\n let current: TSESTree.Node | undefined = node.parent;\n\n while (current) {\n if (isFunctionLike(current)) {\n return current;\n }\n current = current.parent;\n }\n\n return null;\n }\n\n /** Check if a function is a JSX event handler */\n function isJSXEventHandler(\n func:\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration,\n ): boolean {\n const parent = func.parent;\n\n // Check for JSX attribute like onChange={...}\n if (parent?.type === 'JSXExpressionContainer') {\n const jsxAttribute = parent.parent;\n if (jsxAttribute?.type === 'JSXAttribute') {\n const attrName = jsxAttribute.name.type === 'JSXIdentifier' ? jsxAttribute.name.name : '';\n // Match common event handler patterns\n return /^on[A-Z]/.test(attrName);\n }\n }\n\n return false;\n }\n\n /** Get the first parameter of an event handler if it looks like an event */\n function getEventParameter(\n func:\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration,\n ): TSESTree.Identifier | null {\n const firstParam = func.params[0];\n\n if (!firstParam) return null;\n\n // Simple identifier parameter like (e) => ...\n if (firstParam.type === 'Identifier') {\n return firstParam;\n }\n\n return null;\n }\n\n function isReactEventType(typeAnnotation: TSESTree.TypeNode): boolean {\n if (typeAnnotation.type === 'TSTypeReference') {\n if (\n typeAnnotation.typeName.type === 'TSQualifiedName' &&\n typeAnnotation.typeName.left.type === 'Identifier' &&\n typeAnnotation.typeName.left.name === 'React' &&\n typeAnnotation.typeName.right.type === 'Identifier' &&\n typeAnnotation.typeName.right.name.endsWith('Event')\n ) {\n return true;\n }\n\n if (\n typeAnnotation.typeName.type === 'Identifier' &&\n (typeAnnotation.typeName.name === 'SyntheticEvent' ||\n typeAnnotation.typeName.name.endsWith('Event'))\n ) {\n return true;\n }\n }\n\n return false;\n }\n\n function isTypedReactEventHandler(\n func:\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration,\n ): boolean {\n const firstParam = func.params[0];\n if (!firstParam || firstParam.type !== 'Identifier' || !firstParam.typeAnnotation) {\n return false;\n }\n\n return isReactEventType(firstParam.typeAnnotation.typeAnnotation);\n }\n\n function trackIfEventHandler(\n node:\n | TSESTree.ArrowFunctionExpression\n | TSESTree.FunctionExpression\n | TSESTree.FunctionDeclaration,\n ) {\n if (isJSXEventHandler(node) || isTypedReactEventHandler(node)) {\n const eventParam = getEventParameter(node);\n if (eventParam) {\n trackedEventFunctions.set(node, eventParam);\n }\n return;\n }\n\n // react-hook-form: handleSubmit((_data, e) => { ... })\n // The event is the second parameter, and the entire callback is deferred.\n const parent = node.parent;\n if (\n parent?.type === 'CallExpression' &&\n parent.arguments[0] === node &&\n isKnownEventBoundaryCallback(parent, 0)\n ) {\n const secondParam = node.params[1];\n if (secondParam?.type === 'Identifier') {\n trackedEventFunctions.set(node, secondParam);\n }\n }\n }\n\n return {\n // When we enter a JSX event handler, track its event parameter\n 'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression': (\n node: TSESTree.ArrowFunctionExpression,\n ) => {\n trackIfEventHandler(node);\n },\n 'JSXAttribute > JSXExpressionContainer > FunctionExpression': (\n node: TSESTree.FunctionExpression,\n ) => {\n trackIfEventHandler(node);\n },\n ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression) {\n trackIfEventHandler(node);\n },\n FunctionExpression(node: TSESTree.FunctionExpression) {\n trackIfEventHandler(node);\n },\n FunctionDeclaration(node: TSESTree.FunctionDeclaration) {\n trackIfEventHandler(node);\n },\n\n // Check for .currentTarget access\n MemberExpression(node) {\n // Only check for .currentTarget property access\n if (node.property.type !== 'Identifier' || node.property.name !== 'currentTarget') {\n return;\n }\n\n // Check if the object is an identifier (like `e` in `e.currentTarget`)\n if (node.object.type !== 'Identifier') {\n return;\n }\n\n const objectName = node.object.name;\n\n // Find the containing function\n const containingFunction = findContainingFunction(node);\n if (!containingFunction) return;\n\n // Check each tracked event handler to see if this access is problematic\n for (const [eventHandler, eventParam] of trackedEventFunctions) {\n // Check if this is accessing the event parameter\n if (objectName !== eventParam.name) continue;\n\n if (\n isInsideDeferredCallback(node, eventHandler) ||\n hasPriorAwaitInSameFunction(node, eventHandler)\n ) {\n context.report({\n node,\n messageId: 'noCurrentTargetInCallback',\n });\n return;\n }\n }\n },\n\n // Clean up when leaving event handlers\n 'JSXAttribute > JSXExpressionContainer > ArrowFunctionExpression:exit': (\n node: TSESTree.ArrowFunctionExpression,\n ) => {\n trackedEventFunctions.delete(node);\n },\n 'JSXAttribute > JSXExpressionContainer > FunctionExpression:exit': (\n node: TSESTree.FunctionExpression,\n ) => {\n trackedEventFunctions.delete(node);\n },\n 'ArrowFunctionExpression:exit'(node: TSESTree.ArrowFunctionExpression) {\n trackedEventFunctions.delete(node);\n },\n 'FunctionExpression:exit'(node: TSESTree.FunctionExpression) {\n trackedEventFunctions.delete(node);\n },\n 'FunctionDeclaration:exit'(node: TSESTree.FunctionDeclaration) {\n trackedEventFunctions.delete(node);\n },\n };\n },\n});\n"]}
@@ -0,0 +1,4 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ declare const _default: ESLintUtils.RuleModule<"forbiddenProp" | "forbiddenSpread", [], unknown, ESLintUtils.RuleListener>;
3
+ export default _default;
4
+ //# sourceMappingURL=no-hydrate-reslocals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-hydrate-reslocals.d.ts","sourceRoot":"","sources":["../../src/rules/no-hydrate-reslocals.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC;;AA4CtE,wBAyDG","sourcesContent":["import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils';\n\n/**\n * All props on a hydrated component are serialized with superjson and shipped\n * to the browser. Passing `resLocals` or `locals` (or spreading them) would\n * leak the entire server-side `res.locals` object — CSRF tokens, auth data,\n * authz objects, full DB rows, etc.\n *\n * This rule forbids those prop names (and `{...res.locals}` / `{...resLocals}`\n * / `{...locals}` spreads) on the child of `<Hydrate>` and on the JSX argument\n * to `hydrateHtml(...)`. Extract the specific fields you need (e.g. via\n * `extractPageContext`) and pass them as individual props instead.\n */\n\nconst HYDRATE_COMPONENT_NAME = 'Hydrate';\nconst HYDRATE_FUNCTION_NAME = 'hydrateHtml';\nconst FORBIDDEN_NAMES = new Set(['resLocals', 'locals']);\n\nfunction extractSingleJsxChild(children: TSESTree.JSXChild[]): TSESTree.JSXElement | null {\n const nonWhitespace = children.filter((child) => {\n if (child.type === 'JSXText') return child.value.trim().length > 0;\n return true;\n });\n if (nonWhitespace.length !== 1 || nonWhitespace[0].type !== 'JSXElement') {\n return null;\n }\n return nonWhitespace[0];\n}\n\nfunction spreadArgumentDescribesResLocals(argument: TSESTree.Expression): boolean {\n if (argument.type === 'Identifier') {\n return FORBIDDEN_NAMES.has(argument.name);\n }\n if (\n argument.type === 'MemberExpression' &&\n !argument.computed &&\n argument.property.type === 'Identifier' &&\n argument.property.name === 'locals'\n ) {\n return true;\n }\n return false;\n}\n\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n forbiddenProp:\n 'Do not pass \"{{name}}\" to a hydrated component; props are serialized and sent to the client, so this would leak the full server-side res.locals. Pass the specific fields the component needs instead.',\n forbiddenSpread:\n 'Do not spread res.locals onto a hydrated component; props are serialized and sent to the client. Pass the specific fields the component needs instead.',\n },\n schema: [],\n },\n defaultOptions: [],\n\n create(context) {\n function checkHydratedElement(element: TSESTree.JSXElement) {\n for (const attribute of element.openingElement.attributes) {\n if (attribute.type === 'JSXAttribute') {\n if (attribute.name.type === 'JSXIdentifier' && FORBIDDEN_NAMES.has(attribute.name.name)) {\n context.report({\n node: attribute,\n messageId: 'forbiddenProp',\n data: { name: attribute.name.name },\n });\n }\n } else if (attribute.type === 'JSXSpreadAttribute') {\n if (spreadArgumentDescribesResLocals(attribute.argument)) {\n context.report({\n node: attribute,\n messageId: 'forbiddenSpread',\n });\n }\n }\n }\n }\n\n return {\n JSXElement(node) {\n const opening = node.openingElement.name;\n if (opening.type !== 'JSXIdentifier' || opening.name !== HYDRATE_COMPONENT_NAME) return;\n\n const child = extractSingleJsxChild(node.children);\n if (!child) return;\n\n checkHydratedElement(child);\n },\n\n CallExpression(node) {\n if (node.callee.type !== 'Identifier' || node.callee.name !== HYDRATE_FUNCTION_NAME) return;\n if (node.arguments.length === 0) return;\n\n const arg = node.arguments[0];\n if (arg.type !== 'JSXElement') return;\n\n checkHydratedElement(arg);\n },\n };\n },\n});\n"]}