@prairielearn/eslint-plugin 4.1.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/index.d.ts +15 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -21
- package/dist/index.js.map +1 -1
- package/dist/rules/aws-client-mandatory-config.d.ts.map +1 -1
- package/dist/rules/aws-client-mandatory-config.js +4 -6
- package/dist/rules/aws-client-mandatory-config.js.map +1 -1
- package/dist/rules/aws-client-shared-config.d.ts.map +1 -1
- package/dist/rules/aws-client-shared-config.js +4 -6
- package/dist/rules/aws-client-shared-config.js.map +1 -1
- package/dist/rules/html-no-duplicate-id.d.ts +10 -0
- package/dist/rules/html-no-duplicate-id.d.ts.map +1 -0
- package/dist/rules/html-no-duplicate-id.js +54 -0
- package/dist/rules/html-no-duplicate-id.js.map +1 -0
- package/dist/rules/jsx-no-dollar-interpolation.d.ts.map +1 -1
- package/dist/rules/jsx-no-dollar-interpolation.js +2 -4
- package/dist/rules/jsx-no-dollar-interpolation.js.map +1 -1
- package/dist/rules/no-current-target-in-callback.d.ts.map +1 -1
- package/dist/rules/no-current-target-in-callback.js +2 -4
- package/dist/rules/no-current-target-in-callback.js.map +1 -1
- package/dist/rules/no-hydrate-reslocals.d.ts.map +1 -1
- package/dist/rules/no-hydrate-reslocals.js +2 -4
- package/dist/rules/no-hydrate-reslocals.js.map +1 -1
- package/dist/rules/no-unused-sql-blocks.d.ts.map +1 -1
- package/dist/rules/no-unused-sql-blocks.js +4 -39
- package/dist/rules/no-unused-sql-blocks.js.map +1 -1
- package/dist/rules/require-trpc-permission-middleware.d.ts +4 -0
- package/dist/rules/require-trpc-permission-middleware.d.ts.map +1 -0
- package/dist/rules/require-trpc-permission-middleware.js +133 -0
- package/dist/rules/require-trpc-permission-middleware.js.map +1 -0
- package/dist/rules/safe-db-types.d.ts +1 -1
- package/dist/rules/safe-db-types.d.ts.map +1 -1
- package/dist/rules/safe-db-types.js +6 -40
- package/dist/rules/safe-db-types.js.map +1 -1
- package/dist/tests/aws-client-mandatory-config.test.d.ts.map +1 -1
- package/dist/tests/aws-client-mandatory-config.test.js +8 -13
- package/dist/tests/aws-client-mandatory-config.test.js.map +1 -1
- package/dist/tests/aws-client-shared-config.test.d.ts.map +1 -1
- package/dist/tests/aws-client-shared-config.test.js +8 -13
- package/dist/tests/aws-client-shared-config.test.js.map +1 -1
- package/dist/tests/html-no-duplicate-id.test.d.ts +2 -0
- package/dist/tests/html-no-duplicate-id.test.d.ts.map +1 -0
- package/dist/tests/html-no-duplicate-id.test.js +54 -0
- package/dist/tests/html-no-duplicate-id.test.js.map +1 -0
- package/dist/tests/jsx-no-dollar-interpolation.test.d.ts.map +1 -1
- package/dist/tests/jsx-no-dollar-interpolation.test.js +8 -13
- package/dist/tests/jsx-no-dollar-interpolation.test.js.map +1 -1
- package/dist/tests/no-current-target-in-callback.test.js +8 -13
- package/dist/tests/no-current-target-in-callback.test.js.map +1 -1
- package/dist/tests/no-hydrate-reslocals.test.d.ts.map +1 -1
- package/dist/tests/no-hydrate-reslocals.test.js +8 -13
- package/dist/tests/no-hydrate-reslocals.test.js.map +1 -1
- package/dist/tests/require-trpc-permission-middleware.test.d.ts +2 -0
- package/dist/tests/require-trpc-permission-middleware.test.d.ts.map +1 -0
- package/dist/tests/require-trpc-permission-middleware.test.js +106 -0
- package/dist/tests/require-trpc-permission-middleware.test.js.map +1 -0
- package/dist/utils.js +1 -4
- package/dist/utils.js.map +1 -1
- package/package.json +11 -10
- package/src/index.ts +7 -1
- package/src/rules/html-no-duplicate-id.ts +76 -0
- package/src/rules/require-trpc-permission-middleware.ts +168 -0
- package/src/tests/aws-client-mandatory-config.test.ts +1 -1
- package/src/tests/aws-client-shared-config.test.ts +1 -1
- package/src/tests/html-no-duplicate-id.test.ts +57 -0
- package/src/tests/jsx-no-dollar-interpolation.test.ts +1 -1
- package/src/tests/no-hydrate-reslocals.test.ts +1 -1
- package/src/tests/require-trpc-permission-middleware.test.ts +109 -0
- package/tsconfig.json +1 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @prairielearn/eslint-plugin
|
|
2
2
|
|
|
3
|
+
## 4.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9717de9: Add a new rule `@prairielearn/html-no-duplicate-id`. It's a variant of `@html-eslint/no-duplicate-id` that ignores tags whose names start with `pl-`, since the `id` attribute on PrairieLearn elements (e.g. `<pl-sketch-tool id="fd">`) is an element-scoped identifier consumed by the parent element, not a DOM id. It also ignores `id` values that contain mustache template parameters.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 3927197: Update dependencies
|
|
12
|
+
|
|
13
|
+
## 4.2.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- f83d484: Add `require-trpc-permission-middleware` rule that flags any `t.procedure` chain missing a permission middleware (`requireCoursePermission*`, `requireCourseInstancePermission*`, or `requireAdministrator`). The rule is wired up automatically for files under `**/src/trpc/**/*.ts`.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 382dbd8: Bump dependencies
|
|
22
|
+
|
|
3
23
|
## 4.1.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
declare const _default: {
|
|
2
|
+
rules: {
|
|
3
|
+
'aws-client-mandatory-config': import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingConfig", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
4
|
+
'aws-client-shared-config': import("@typescript-eslint/utils/ts-eslint").RuleModule<"improperConfig" | "unknownConfig", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
5
|
+
'html-no-duplicate-id': import("@typescript-eslint/utils/ts-eslint").RuleModule<"duplicateId", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
6
|
+
'jsx-no-dollar-interpolation': import("@typescript-eslint/utils/ts-eslint").RuleModule<"dollarInterpolationNotAllowed", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
7
|
+
'no-current-target-in-callback': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noCurrentTargetInCallback", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
8
|
+
'no-hydrate-reslocals': import("@typescript-eslint/utils/ts-eslint").RuleModule<"forbiddenProp" | "forbiddenSpread", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
9
|
+
'no-unused-sql-blocks': import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedSqlBlock", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
10
|
+
'require-trpc-permission-middleware': import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingPermissionMiddleware", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
11
|
+
'safe-db-types': import("@typescript-eslint/utils/ts-eslint").RuleModule<"spreadAttributes" | "unsafeTypes", [({
|
|
12
|
+
allowDbTypes?: (string | RegExp)[];
|
|
13
|
+
} | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
14
|
+
};
|
|
11
15
|
};
|
|
16
|
+
export default _default;
|
|
12
17
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"","sourcesContent":["import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';\nimport awsClientSharedConfig from './rules/aws-client-shared-config.js';\nimport htmlNoDuplicateId from './rules/html-no-duplicate-id.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 requireTrpcPermissionMiddleware from './rules/require-trpc-permission-middleware.js';\nimport safeDbTypes from './rules/safe-db-types.js';\n\nconst rules = {\n 'aws-client-mandatory-config': awsClientMandatoryConfig,\n 'aws-client-shared-config': awsClientSharedConfig,\n 'html-no-duplicate-id': htmlNoDuplicateId,\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 'require-trpc-permission-middleware': requireTrpcPermissionMiddleware,\n 'safe-db-types': safeDbTypes,\n};\n\nexport default { rules };\n"]}
|
package/dist/index.js
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'no-unused-sql-blocks': no_unused_sql_blocks_js_1.default,
|
|
21
|
-
'safe-db-types': safe_db_types_js_1.default,
|
|
1
|
+
import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';
|
|
2
|
+
import awsClientSharedConfig from './rules/aws-client-shared-config.js';
|
|
3
|
+
import htmlNoDuplicateId from './rules/html-no-duplicate-id.js';
|
|
4
|
+
import jsxNoDollarInterpolation from './rules/jsx-no-dollar-interpolation.js';
|
|
5
|
+
import noCurrentTargetInCallback from './rules/no-current-target-in-callback.js';
|
|
6
|
+
import noHydrateResLocals from './rules/no-hydrate-reslocals.js';
|
|
7
|
+
import noUnusedSqlBlocks from './rules/no-unused-sql-blocks.js';
|
|
8
|
+
import requireTrpcPermissionMiddleware from './rules/require-trpc-permission-middleware.js';
|
|
9
|
+
import safeDbTypes from './rules/safe-db-types.js';
|
|
10
|
+
const rules = {
|
|
11
|
+
'aws-client-mandatory-config': awsClientMandatoryConfig,
|
|
12
|
+
'aws-client-shared-config': awsClientSharedConfig,
|
|
13
|
+
'html-no-duplicate-id': htmlNoDuplicateId,
|
|
14
|
+
'jsx-no-dollar-interpolation': jsxNoDollarInterpolation,
|
|
15
|
+
'no-current-target-in-callback': noCurrentTargetInCallback,
|
|
16
|
+
'no-hydrate-reslocals': noHydrateResLocals,
|
|
17
|
+
'no-unused-sql-blocks': noUnusedSqlBlocks,
|
|
18
|
+
'require-trpc-permission-middleware': requireTrpcPermissionMiddleware,
|
|
19
|
+
'safe-db-types': safeDbTypes,
|
|
22
20
|
};
|
|
21
|
+
export default { rules };
|
|
23
22
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,wBAAwB,MAAM,wCAAwC,CAAC;AAC9E,OAAO,qBAAqB,MAAM,qCAAqC,CAAC;AACxE,OAAO,iBAAiB,MAAM,iCAAiC,CAAC;AAChE,OAAO,wBAAwB,MAAM,wCAAwC,CAAC;AAC9E,OAAO,yBAAyB,MAAM,0CAA0C,CAAC;AACjF,OAAO,kBAAkB,MAAM,iCAAiC,CAAC;AACjE,OAAO,iBAAiB,MAAM,iCAAiC,CAAC;AAChE,OAAO,+BAA+B,MAAM,+CAA+C,CAAC;AAC5F,OAAO,WAAW,MAAM,0BAA0B,CAAC;AAEnD,MAAM,KAAK,GAAG;IACZ,6BAA6B,EAAE,wBAAwB;IACvD,0BAA0B,EAAE,qBAAqB;IACjD,sBAAsB,EAAE,iBAAiB;IACzC,6BAA6B,EAAE,wBAAwB;IACvD,+BAA+B,EAAE,yBAAyB;IAC1D,sBAAsB,EAAE,kBAAkB;IAC1C,sBAAsB,EAAE,iBAAiB;IACzC,oCAAoC,EAAE,+BAA+B;IACrE,eAAe,EAAE,WAAW;CAC7B,CAAC;AAEF,eAAe,EAAE,KAAK,EAAE,CAAC","sourcesContent":["import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';\nimport awsClientSharedConfig from './rules/aws-client-shared-config.js';\nimport htmlNoDuplicateId from './rules/html-no-duplicate-id.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 requireTrpcPermissionMiddleware from './rules/require-trpc-permission-middleware.js';\nimport safeDbTypes from './rules/safe-db-types.js';\n\nconst rules = {\n 'aws-client-mandatory-config': awsClientMandatoryConfig,\n 'aws-client-shared-config': awsClientSharedConfig,\n 'html-no-duplicate-id': htmlNoDuplicateId,\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 'require-trpc-permission-middleware': requireTrpcPermissionMiddleware,\n 'safe-db-types': safeDbTypes,\n};\n\nexport default { rules };\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aws-client-mandatory-config.d.ts","sourceRoot":"","sources":["../../src/rules/aws-client-mandatory-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAIvD;;;;;;GAMG
|
|
1
|
+
{"version":3,"file":"aws-client-mandatory-config.d.ts","sourceRoot":"","sources":["../../src/rules/aws-client-mandatory-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAIvD;;;;;;GAMG","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,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const utils_1 = require("@typescript-eslint/utils");
|
|
4
|
-
const utils_js_1 = require("../utils.js");
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
import { getAwsClientNamesFromImportDeclaration } from '../utils.js';
|
|
5
3
|
/**
|
|
6
4
|
* This rule enforces that we always explicitly provide a config to AWS clients.
|
|
7
5
|
* This helps ensure that we always construct a client with a specific region.
|
|
@@ -9,7 +7,7 @@ const utils_js_1 = require("../utils.js");
|
|
|
9
7
|
* This rules works in tandem with `aws-client-shared-config` to ensure that
|
|
10
8
|
* we're properly configuring AWS SDK clients.
|
|
11
9
|
*/
|
|
12
|
-
|
|
10
|
+
export default ESLintUtils.RuleCreator.withoutDocs({
|
|
13
11
|
meta: {
|
|
14
12
|
type: 'problem',
|
|
15
13
|
messages: {
|
|
@@ -23,7 +21,7 @@ exports.default = utils_1.ESLintUtils.RuleCreator.withoutDocs({
|
|
|
23
21
|
return {
|
|
24
22
|
// Handle `import ...` statements
|
|
25
23
|
ImportDeclaration(node) {
|
|
26
|
-
const clientNames =
|
|
24
|
+
const clientNames = getAwsClientNamesFromImportDeclaration(node);
|
|
27
25
|
clientNames.forEach((clientName) => awsClientImports.add(clientName));
|
|
28
26
|
},
|
|
29
27
|
NewExpression(node) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aws-client-mandatory-config.js","sourceRoot":"","sources":["../../src/rules/aws-client-mandatory-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"aws-client-mandatory-config.js","sourceRoot":"","sources":["../../src/rules/aws-client-mandatory-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD,OAAO,EAAE,sCAAsC,EAAE,MAAM,aAAa,CAAC;AAErE;;;;;;GAMG;AACH,eAAe,WAAW,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,sCAAsC,CAAC,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,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.d.ts","sourceRoot":"","sources":["../../src/rules/aws-client-shared-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAIvD;;;;;;;;;;;;;;;;;GAiBG
|
|
1
|
+
{"version":3,"file":"aws-client-shared-config.d.ts","sourceRoot":"","sources":["../../src/rules/aws-client-shared-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAIvD;;;;;;;;;;;;;;;;;GAiBG","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,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const utils_1 = require("@typescript-eslint/utils");
|
|
4
|
-
const utils_js_1 = require("../utils.js");
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
import { getAwsClientNamesFromImportDeclaration } from '../utils.js';
|
|
5
3
|
/**
|
|
6
4
|
* This ESLint rules enforces that we always provide a "shared" config to AWS
|
|
7
5
|
* clients.
|
|
@@ -20,7 +18,7 @@ const utils_js_1 = require("../utils.js");
|
|
|
20
18
|
* This rules works in tandem with `aws-client-mandatory-config` to ensure that
|
|
21
19
|
* we're properly configuring AWS SDK clients.
|
|
22
20
|
*/
|
|
23
|
-
|
|
21
|
+
export default ESLintUtils.RuleCreator.withoutDocs({
|
|
24
22
|
meta: {
|
|
25
23
|
type: 'problem',
|
|
26
24
|
messages: {
|
|
@@ -35,7 +33,7 @@ exports.default = utils_1.ESLintUtils.RuleCreator.withoutDocs({
|
|
|
35
33
|
return {
|
|
36
34
|
// Handle `import ...` statements
|
|
37
35
|
ImportDeclaration(node) {
|
|
38
|
-
const clientNames =
|
|
36
|
+
const clientNames = getAwsClientNamesFromImportDeclaration(node);
|
|
39
37
|
clientNames.forEach((clientName) => awsClientImports.add(clientName));
|
|
40
38
|
},
|
|
41
39
|
NewExpression(node) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aws-client-shared-config.js","sourceRoot":"","sources":["../../src/rules/aws-client-shared-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"aws-client-shared-config.js","sourceRoot":"","sources":["../../src/rules/aws-client-shared-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD,OAAO,EAAE,sCAAsC,EAAE,MAAM,aAAa,CAAC;AAErE;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAe,WAAW,CAAC,WAAW,CAAC,WAAW,CAAC;IACjD,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE;YACR,cAAc,EACZ,wFAAwF;YAC1F,aAAa,EAAE,wCAAwC;SACxD;QACD,MAAM,EAAE,EAAE;KACX;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO;QACZ,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAU,CAAC;QAE3C,OAAO;YACL,iCAAiC;YACjC,iBAAiB,CAAC,IAAI;gBACpB,MAAM,WAAW,GAAG,sCAAsC,CAAC,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,mEAAmE;oBACnE,0CAA0C;oBAE1C,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;wBAChC,sEAAsE;wBACtE,4CAA4C;wBAC5C,OAAO;oBACT,CAAC;oBAED,IAAI,yBAAyB,GAAG,qBAAqB,CAAC;oBAEtD,2BAA2B;oBAC3B,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,UAAU,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;wBACjE,yBAAyB,GAAG,oBAAoB,CAAC;oBACnD,CAAC;oBAED,MAAM,cAAc,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;oBACzC,IAAI,cAAc,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;wBAC7C,OAAO,CAAC,MAAM,CAAC;4BACb,IAAI;4BACJ,SAAS,EAAE,gBAAgB;4BAC3B,IAAI,EAAE;gCACJ,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI;gCAC5B,yBAAyB;6BAC1B;yBACF,CAAC,CAAC;wBACH,OAAO;oBACT,CAAC;oBAED,uCAAuC;oBACvC,IACE,cAAc,CAAC,MAAM,CAAC,IAAI,KAAK,kBAAkB;wBACjD,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,KAAK,YAAY,EACpD,CAAC;wBACD,MAAM,YAAY,GAAG,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;wBACzD,IAAI,YAAY,KAAK,yBAAyB,EAAE,CAAC;4BAC/C,OAAO,CAAC,MAAM,CAAC;gCACb,IAAI;gCACJ,SAAS,EAAE,gBAAgB;gCAC3B,IAAI,EAAE;oCACJ,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI;oCAC5B,yBAAyB;iCAC1B;6BACF,CAAC,CAAC;wBACL,CAAC;wBACD,OAAO;oBACT,CAAC;oBAED,IAAI,cAAc,CAAC,MAAM,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;wBAChD,MAAM,YAAY,GAAG,cAAc,CAAC,MAAM,CAAC,IAAI,CAAC;wBAChD,IAAI,YAAY,KAAK,yBAAyB,EAAE,CAAC;4BAC/C,OAAO,CAAC,MAAM,CAAC;gCACb,IAAI;gCACJ,SAAS,EAAE,gBAAgB;gCAC3B,IAAI,EAAE;oCACJ,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI;oCAC5B,yBAAyB;iCAC1B;6BACF,CAAC,CAAC;wBACL,CAAC;wBACD,OAAO;oBACT,CAAC;oBAED,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI;wBACJ,SAAS,EAAE,eAAe;qBAC3B,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC","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"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Variant of `@html-eslint/no-duplicate-id` that ignores tags whose names start
|
|
4
|
+
* with `pl-`. The `id` attribute on PrairieLearn elements (e.g. `pl-sketch-tool
|
|
5
|
+
* id="fd"`) is not a DOM id, it's an element-specific identifier consumed by
|
|
6
|
+
* the parent element, so duplicates across separate parents are legal.
|
|
7
|
+
*/
|
|
8
|
+
declare const _default: ESLintUtils.RuleModule<"duplicateId", [], unknown, ESLintUtils.RuleListener>;
|
|
9
|
+
export default _default;
|
|
10
|
+
//# sourceMappingURL=html-no-duplicate-id.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-no-duplicate-id.d.ts","sourceRoot":"","sources":["../../src/rules/html-no-duplicate-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAiCvD;;;;;GAKG","sourcesContent":["import { ESLintUtils } from '@typescript-eslint/utils';\n\ninterface AttributeValueNode {\n type: 'AttributeValue';\n value: string;\n parts?: { type: string }[];\n}\n\ninterface AttributeKeyNode {\n type: 'AttributeKey';\n value: string;\n}\n\ninterface AttributeNode {\n type: 'Attribute';\n key: AttributeKeyNode;\n value?: AttributeValueNode;\n}\n\ninterface TagNode {\n type: 'Tag';\n name: string;\n attributes: AttributeNode[];\n}\n\nfunction findIdAttr(node: TagNode): AttributeNode | undefined {\n return node.attributes.find((attr) => attr.key && attr.key.value.toLowerCase() === 'id');\n}\n\nfunction hasTemplatePart(node: AttributeValueNode): boolean {\n return node.parts?.some((part) => part.type === 'Template') ?? false;\n}\n\n/**\n * Variant of `@html-eslint/no-duplicate-id` that ignores tags whose names start\n * with `pl-`. The `id` attribute on PrairieLearn elements (e.g. `pl-sketch-tool\n * id=\"fd\"`) is not a DOM id, it's an element-specific identifier consumed by\n * the parent element, so duplicates across separate parents are legal.\n */\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n duplicateId: \"The id '{{id}}' is duplicated.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const idAttrs = new Map<string, AttributeValueNode[]>();\n\n return {\n Tag(node: TagNode) {\n if (node.name.toLowerCase().startsWith('pl-')) return;\n const idAttr = findIdAttr(node);\n if (!idAttr?.value) return;\n if (hasTemplatePart(idAttr.value)) return;\n const list = idAttrs.get(idAttr.value.value) ?? [];\n list.push(idAttr.value);\n idAttrs.set(idAttr.value.value, list);\n },\n 'Document:exit'() {\n for (const attrs of idAttrs.values()) {\n if (attrs.length <= 1) continue;\n for (const attr of attrs) {\n context.report({\n node: attr as never,\n data: { id: attr.value },\n messageId: 'duplicateId',\n });\n }\n }\n },\n };\n },\n});\n"]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
function findIdAttr(node) {
|
|
3
|
+
return node.attributes.find((attr) => attr.key && attr.key.value.toLowerCase() === 'id');
|
|
4
|
+
}
|
|
5
|
+
function hasTemplatePart(node) {
|
|
6
|
+
return node.parts?.some((part) => part.type === 'Template') ?? false;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Variant of `@html-eslint/no-duplicate-id` that ignores tags whose names start
|
|
10
|
+
* with `pl-`. The `id` attribute on PrairieLearn elements (e.g. `pl-sketch-tool
|
|
11
|
+
* id="fd"`) is not a DOM id, it's an element-specific identifier consumed by
|
|
12
|
+
* the parent element, so duplicates across separate parents are legal.
|
|
13
|
+
*/
|
|
14
|
+
export default ESLintUtils.RuleCreator.withoutDocs({
|
|
15
|
+
meta: {
|
|
16
|
+
type: 'problem',
|
|
17
|
+
messages: {
|
|
18
|
+
duplicateId: "The id '{{id}}' is duplicated.",
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
},
|
|
22
|
+
defaultOptions: [],
|
|
23
|
+
create(context) {
|
|
24
|
+
const idAttrs = new Map();
|
|
25
|
+
return {
|
|
26
|
+
Tag(node) {
|
|
27
|
+
if (node.name.toLowerCase().startsWith('pl-'))
|
|
28
|
+
return;
|
|
29
|
+
const idAttr = findIdAttr(node);
|
|
30
|
+
if (!idAttr?.value)
|
|
31
|
+
return;
|
|
32
|
+
if (hasTemplatePart(idAttr.value))
|
|
33
|
+
return;
|
|
34
|
+
const list = idAttrs.get(idAttr.value.value) ?? [];
|
|
35
|
+
list.push(idAttr.value);
|
|
36
|
+
idAttrs.set(idAttr.value.value, list);
|
|
37
|
+
},
|
|
38
|
+
'Document:exit'() {
|
|
39
|
+
for (const attrs of idAttrs.values()) {
|
|
40
|
+
if (attrs.length <= 1)
|
|
41
|
+
continue;
|
|
42
|
+
for (const attr of attrs) {
|
|
43
|
+
context.report({
|
|
44
|
+
node: attr,
|
|
45
|
+
data: { id: attr.value },
|
|
46
|
+
messageId: 'duplicateId',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
//# sourceMappingURL=html-no-duplicate-id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-no-duplicate-id.js","sourceRoot":"","sources":["../../src/rules/html-no-duplicate-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAyBvD,SAAS,UAAU,CAAC,IAAa;IAC/B,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,IAAI,CAAC,CAAC;AAC3F,CAAC;AAED,SAAS,eAAe,CAAC,IAAwB;IAC/C,OAAO,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,UAAU,CAAC,IAAI,KAAK,CAAC;AACvE,CAAC;AAED;;;;;GAKG;AACH,eAAe,WAAW,CAAC,WAAW,CAAC,WAAW,CAAC;IACjD,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE;YACR,WAAW,EAAE,gCAAgC;SAC9C;QACD,MAAM,EAAE,EAAE;KACX;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO;QACZ,MAAM,OAAO,GAAG,IAAI,GAAG,EAAgC,CAAC;QAExD,OAAO;YACL,GAAG,CAAC,IAAa;gBACf,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC;oBAAE,OAAO;gBACtD,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;gBAChC,IAAI,CAAC,MAAM,EAAE,KAAK;oBAAE,OAAO;gBAC3B,IAAI,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC;oBAAE,OAAO;gBAC1C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;gBACnD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACxB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACxC,CAAC;YACD,eAAe;gBACb,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;oBACrC,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC;wBAAE,SAAS;oBAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;wBACzB,OAAO,CAAC,MAAM,CAAC;4BACb,IAAI,EAAE,IAAa;4BACnB,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,KAAK,EAAE;4BACxB,SAAS,EAAE,aAAa;yBACzB,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC","sourcesContent":["import { ESLintUtils } from '@typescript-eslint/utils';\n\ninterface AttributeValueNode {\n type: 'AttributeValue';\n value: string;\n parts?: { type: string }[];\n}\n\ninterface AttributeKeyNode {\n type: 'AttributeKey';\n value: string;\n}\n\ninterface AttributeNode {\n type: 'Attribute';\n key: AttributeKeyNode;\n value?: AttributeValueNode;\n}\n\ninterface TagNode {\n type: 'Tag';\n name: string;\n attributes: AttributeNode[];\n}\n\nfunction findIdAttr(node: TagNode): AttributeNode | undefined {\n return node.attributes.find((attr) => attr.key && attr.key.value.toLowerCase() === 'id');\n}\n\nfunction hasTemplatePart(node: AttributeValueNode): boolean {\n return node.parts?.some((part) => part.type === 'Template') ?? false;\n}\n\n/**\n * Variant of `@html-eslint/no-duplicate-id` that ignores tags whose names start\n * with `pl-`. The `id` attribute on PrairieLearn elements (e.g. `pl-sketch-tool\n * id=\"fd\"`) is not a DOM id, it's an element-specific identifier consumed by\n * the parent element, so duplicates across separate parents are legal.\n */\nexport default ESLintUtils.RuleCreator.withoutDocs({\n meta: {\n type: 'problem',\n messages: {\n duplicateId: \"The id '{{id}}' is duplicated.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const idAttrs = new Map<string, AttributeValueNode[]>();\n\n return {\n Tag(node: TagNode) {\n if (node.name.toLowerCase().startsWith('pl-')) return;\n const idAttr = findIdAttr(node);\n if (!idAttr?.value) return;\n if (hasTemplatePart(idAttr.value)) return;\n const list = idAttrs.get(idAttr.value.value) ?? [];\n list.push(idAttr.value);\n idAttrs.set(idAttr.value.value, list);\n },\n 'Document:exit'() {\n for (const attrs of idAttrs.values()) {\n if (attrs.length <= 1) continue;\n for (const attr of attrs) {\n context.report({\n node: attr as never,\n data: { id: attr.value },\n messageId: 'duplicateId',\n });\n }\n }\n },\n };\n },\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsx-no-dollar-interpolation.d.ts","sourceRoot":"","sources":["../../src/rules/jsx-no-dollar-interpolation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD;;;;;;;;;GASG
|
|
1
|
+
{"version":3,"file":"jsx-no-dollar-interpolation.d.ts","sourceRoot":"","sources":["../../src/rules/jsx-no-dollar-interpolation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD;;;;;;;;;GASG","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,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const utils_1 = require("@typescript-eslint/utils");
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
4
2
|
/**
|
|
5
3
|
* This rule will report things that look like template string interpolations
|
|
6
4
|
* that were improperly converted to JSX. For example, the following code will
|
|
@@ -11,7 +9,7 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
11
9
|
* const b = <div>$ {message}</div>;
|
|
12
10
|
* ```
|
|
13
11
|
*/
|
|
14
|
-
|
|
12
|
+
export default ESLintUtils.RuleCreator.withoutDocs({
|
|
15
13
|
meta: {
|
|
16
14
|
type: 'problem',
|
|
17
15
|
messages: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsx-no-dollar-interpolation.js","sourceRoot":"","sources":["../../src/rules/jsx-no-dollar-interpolation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"jsx-no-dollar-interpolation.js","sourceRoot":"","sources":["../../src/rules/jsx-no-dollar-interpolation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD;;;;;;;;;GASG;AACH,eAAe,WAAW,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,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 +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,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"]}
|
|
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","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"]}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const utils_1 = require("@typescript-eslint/utils");
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
4
2
|
/**
|
|
5
3
|
* This rule detects when `event.currentTarget` is accessed after an `await` or
|
|
6
4
|
* inside a callback passed to a likely deferred API within a React event
|
|
@@ -17,7 +15,7 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
17
15
|
* onChange={({ currentTarget }) => setChecks((c) => ({ ...c, value: currentTarget.checked }))}
|
|
18
16
|
* ```
|
|
19
17
|
*/
|
|
20
|
-
|
|
18
|
+
export default ESLintUtils.RuleCreator.withoutDocs({
|
|
21
19
|
meta: {
|
|
22
20
|
type: 'problem',
|
|
23
21
|
messages: {
|