Repository: PrairieLearn/PrairieLearn
Pull request #11499: Introduce jsx-no-dollar-interpolation ESLint rule
Author: login: nwalters512
Created: 2025-03-05T00:51:29Z | Merged: 2025-03-05T18:21:35Z

I'm lifting this out of #10944. I kept messing this up while working on that PR. In anticipation of more conversion to JSX, I'd like to have tooling in place to make sure we don't further mess this up.

As part of this, I updated our existing ESLint rules to be fully type-safe with the help of some packages published by the `@typescript-eslint` folks. I also added tests to the existing rules.


Diff:
diff --git a/.changeset/early-pianos-draw.md b/.changeset/early-pianos-draw.md
new file mode 100644
index 00000000000..3d7487cdc9b
--- /dev/null
+++ b/.changeset/early-pianos-draw.md
@@ -0,0 +1,5 @@
+---
+'@prairielearn/eslint-plugin': minor
+---
+
+Introduce `jsx-no-dollar-interpolation` rule
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index a660f0487a5..1d7c39f52ca 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -90,6 +90,7 @@ module.exports = {
     // These rules are implemented in `packages/eslint-plugin-prairielearn`.
     '@prairielearn/aws-client-mandatory-config': 'error',
     '@prairielearn/aws-client-shared-config': 'error',
+    '@prairielearn/jsx-no-dollar-interpolation': 'error',
 
     '@typescript-eslint/consistent-type-imports': ['error', { fixStyle: 'inline-type-imports' }],
 
diff --git a/packages/eslint-plugin-prairielearn/.mocharc.cjs b/packages/eslint-plugin-prairielearn/.mocharc.cjs
new file mode 100644
index 00000000000..803ee6ebc40
--- /dev/null
+++ b/packages/eslint-plugin-prairielearn/.mocharc.cjs
@@ -0,0 +1,3 @@
+module.exports = {
+  require: ['tsx'],
+};
diff --git a/packages/eslint-plugin-prairielearn/package.json b/packages/eslint-plugin-prairielearn/package.json
index afd906ca557..9295abc8b1e 100644
--- a/packages/eslint-plugin-prairielearn/package.json
+++ b/packages/eslint-plugin-prairielearn/package.json
@@ -9,11 +9,33 @@
   },
   "scripts": {
     "build": "tsc",
-    "dev": "tsc --watch --preserveWatchOutput"
+    "dev": "tsc --watch --preserveWatchOutput",
+    "test": "c8 mocha src/**/*.test.ts"
   },
   "devDependencies": {
     "@prairielearn/tsconfig": "workspace:^",
+    "@types/chai": "^5.0.1",
+    "@types/mocha": "^10.0.10",
     "@types/node": "^20.17.23",
+    "@typescript-eslint/rule-tester": "^8.26.0",
+    "c8": "^10.1.3",
+    "chai": "^5.2.0",
+    "mocha": "^11.1.0",
+    "tsx": "^4.19.3",
     "typescript": "^5.8.2"
+  },
+  "dependencies": {
+    "@typescript-eslint/utils": "^8.26.0"
+  },
+  "c8": {
+    "reporter": [
+      "html",
+      "text-summary",
+      "cobertura"
+    ],
+    "all": true,
+    "include": [
+      "src/**"
+    ]
   }
 }
diff --git a/packages/eslint-plugin-prairielearn/src/index.ts b/packages/eslint-plugin-prairielearn/src/index.ts
index 01c15fe791c..b6903d54ea4 100644
--- a/packages/eslint-plugin-prairielearn/src/index.ts
+++ b/packages/eslint-plugin-prairielearn/src/index.ts
@@ -1,7 +1,9 @@
 import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';
 import awsClientSharedConfig from './rules/aws-client-shared-config.js';
+import jsxNoDollarInterpolation from './rules/jsx-no-dollar-interpolation.js';
 
 export const rules = {
   'aws-client-mandatory-config': awsClientMandatoryConfig,
   'aws-client-shared-config': awsClientSharedConfig,
+  'jsx-no-dollar-interpolation': jsxNoDollarInterpolation,
 };
diff --git a/packages/eslint-plugin-prairielearn/src/rules/aws-client-mandatory-config.ts b/packages/eslint-plugin-prairielearn/src/rules/aws-client-mandatory-config.ts
index 2f6e3d8c13c..07d55fefb3e 100644
--- a/packages/eslint-plugin-prairielearn/src/rules/aws-client-mandatory-config.ts
+++ b/packages/eslint-plugin-prairielearn/src/rules/aws-client-mandatory-config.ts
@@ -1,3 +1,5 @@
+import { ESLintUtils } from '@typescript-eslint/utils';
+
 import { getAwsClientNamesFromImportDeclaration } from '../utils.js';
 
 /**
@@ -7,24 +9,35 @@ import { getAwsClientNamesFromImportDeclaration } from '../utils.js';
  * This rules works in tandem with `aws-client-shared-config` to ensure that
  * we're properly configuring AWS SDK clients.
  */
-export default {
-  create(context: any) {
+export default ESLintUtils.RuleCreator.withoutDocs({
+  meta: {
+    type: 'problem',
+    messages: {
+      missingConfig: '{{clientName}} must be constructed with a config object.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
     const awsClientImports = new Set();
 
     return {
       // Handle `import ...` statements
-      ImportDeclaration(node: any) {
+      ImportDeclaration(node) {
         const clientNames = getAwsClientNamesFromImportDeclaration(node);
         clientNames.forEach((clientName) => awsClientImports.add(clientName));
       },
-      NewExpression(node: any) {
+      NewExpression(node) {
         if (node.callee.type === 'Identifier' && awsClientImports.has(node.callee.name)) {
           // We're constructing an AWS client. Ensure that the call has at
           // least one argument corresponding to a config object.
           if (node.arguments.length === 0) {
             context.report({
               node,
-              message: `${node.callee.name} must be constructed with a config object.`,
+              messageId: 'missingConfig',
+              data: {
+                clientName: node.callee.name,
+              },
             });
             return;
           }
@@ -32,4 +45,4 @@ export default {
       },
     };
   },
-};
+});
diff --git a/packages/eslint-plugin-prairielearn/src/rules/aws-client-shared-config.ts b/packages/eslint-plugin-prairielearn/src/rules/aws-client-shared-config.ts
index fc8bbaba8d4..13a9fdb75bd 100644
--- a/packages/eslint-plugin-prairielearn/src/rules/aws-client-shared-config.ts
+++ b/packages/eslint-plugin-prairielearn/src/rules/aws-client-shared-config.ts
@@ -1,3 +1,5 @@
+import { ESLintUtils } from '@typescript-eslint/utils';
+
 import { getAwsClientNamesFromImportDeclaration } from '../utils.js';
 
 /**
@@ -18,17 +20,27 @@ import { getAwsClientNamesFromImportDeclaration } from '../utils.js';
  * This rules works in tandem with `aws-client-mandatory-config` to ensure that
  * we're properly configuring AWS SDK clients.
  */
-export default {
-  create(context: any) {
+export default ESLintUtils.RuleCreator.withoutDocs({
+  meta: {
+    type: 'problem',
+    messages: {
+      improperConfig:
+        'Config for {{clientName}} must be obtained by calling {{desiredConfigFunctionName}}().',
+      unknownConfig: 'Unknown config provided to AWS client.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+  create(context) {
     const awsClientImports = new Set<string>();
 
     return {
       // Handle `import ...` statements
-      ImportDeclaration(node: any) {
+      ImportDeclaration(node) {
         const clientNames = getAwsClientNamesFromImportDeclaration(node);
         clientNames.forEach((clientName) => awsClientImports.add(clientName));
       },
-      NewExpression(node: any) {
+      NewExpression(node) {
         if (node.callee.type === 'Identifier' && awsClientImports.has(node.callee.name)) {
           // We're constructing an AWS client. Ensure that the first argument
           // comes from one of our config providers.
@@ -50,18 +62,29 @@ export default {
           if (configArgument.type !== 'CallExpression') {
             context.report({
               node,
-              message: `Config for ${node.callee.name} must be obtained by calling ${desiredConfigFunctionName}().`,
+              messageId: 'improperConfig',
+              data: {
+                clientName: node.callee.name,
+                desiredConfigFunctionName,
+              },
             });
             return;
           }
 
           // Handle member calls to the function.
-          if (configArgument.callee.type === 'MemberExpression') {
+          if (
+            configArgument.callee.type === 'MemberExpression' &&
+            configArgument.callee.property.type === 'Identifier'
+          ) {
             const functionName = configArgument.callee.property.name;
             if (functionName !== desiredConfigFunctionName) {
               context.report({
                 node,
-                message: `Config for ${node.callee.name} must be obtained by calling ${desiredConfigFunctionName}().`,
+                messageId: 'improperConfig',
+                data: {
+                  clientName: node.callee.name,
+                  desiredConfigFunctionName,
+                },
               });
             }
             return;
@@ -72,7 +95,11 @@ export default {
             if (functionName !== desiredConfigFunctionName) {
               context.report({
                 node,
-                message: `Config for ${node.callee.name} must be obtained by calling ${desiredConfigFunctionName}().`,
+                messageId: 'improperConfig',
+                data: {
+                  clientName: node.callee.name,
+                  desiredConfigFunctionName,
+                },
               });
             }
             return;
@@ -80,10 +107,10 @@ export default {
 
           context.report({
             node,
-            message: 'Unknown config provided to AWS client.',
+            messageId: 'unknownConfig',
           });
         }
       },
     };
   },
-};
+});
diff --git a/packages/eslint-plugin-prairielearn/src/rules/jsx-no-dollar-interpolation.ts b/packages/eslint-plugin-prairielearn/src/rules/jsx-no-dollar-interpolation.ts
new file mode 100644
index 00000000000..ccd814c86d6
--- /dev/null
+++ b/packages/eslint-plugin-prairielearn/src/rules/jsx-no-dollar-interpolation.ts
@@ -0,0 +1,61 @@
+import { ESLintUtils } from '@typescript-eslint/utils';
+
+/**
+ * This rule will report things that look like template string interpolations
+ * that were improperly converted to JSX. For example, the following code will
+ * trigger an error:
+ *
+ * ```tsx
+ * const a = <div>${message}</div>;
+ * const b = <div>$ {message}</div>;
+ * ```
+ */
+export default ESLintUtils.RuleCreator.withoutDocs({
+  meta: {
+    type: 'problem',
+    messages: {
+      dollarInterpolationNotAllowed: 'Interpolation with a dollar sign is not allowed in JSX.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+
+  create(context) {
+    return {
+      JSXElement(node) {
+        node.children.forEach((child, index) => {
+          // Skip the first child since it can't be preceded by a JSXText node.
+          if (index === 0) return;
+
+          // Skip anything that's not a JSXExpressionContainer.
+          if (child.type !== 'JSXExpressionContainer') return;
+
+          const previousChild = node.children[index - 1];
+
+          // Skip nodes that aren't preceded by a JSXText node.
+          if (previousChild.type !== 'JSXText') return;
+
+          // Skip nodes that aren't preceded by a dollar sign.
+          if (!previousChild.value.trimEnd().endsWith('$')) return;
+
+          // Determine the range of characters that should be reported. We
+          // include the dollar sign, any following whitespace, and the
+          // expression container.
+          const start = context.sourceCode.getIndexFromLoc(previousChild.loc.start);
+          const lastIndex = previousChild.value.lastIndexOf('$');
+          const dollarStart = start + lastIndex;
+          const dollarStartLoc = context.sourceCode.getLocFromIndex(dollarStart);
+
+          context.report({
+            node,
+            loc: {
+              start: dollarStartLoc,
+              end: child.loc.end,
+            },
+            messageId: 'dollarInterpolationNotAllowed',
+          });
+        });
+      },
+    };
+  },
+});
diff --git a/packages/eslint-plugin-prairielearn/src/tests/aws-client-mandatory-config.test.ts b/packages/eslint-plugin-prairielearn/src/tests/aws-client-mandatory-config.test.ts
new file mode 100644
index 00000000000..ade51198b71
--- /dev/null
+++ b/packages/eslint-plugin-prairielearn/src/tests/aws-client-mandatory-config.test.ts
@@ -0,0 +1,30 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+
+import rule from '../rules/aws-client-mandatory-config';
+
+RuleTester.afterAll = after;
+
+const ruleTester = new RuleTester();
+
+ruleTester.run('aws-client-mandatory-config', rule, {
+  valid: [
+    {
+      code: "import { S3 } from '@aws-sdk/client-s3'; new S3({ region: 'us-east-2' })",
+    },
+    {
+      code: "import { S3Client } from '@aws-sdk/client-s3'; new S3Client({ region: 'us-east-2' })",
+    },
+    {
+      code: "import { EC2 } from '@aws-sdk/client-ec2'; new EC2({ region: 'us-east-2' })",
+    },
+    {
+      code: "import { EC2Client } from '@aws-sdk/client-ec2'; new EC2Client({ region: 'us-east-2' })",
+    },
+  ],
+  invalid: [
+    {
+      code: "import { S3 } from '@aws-sdk/client-s3'; new S3()",
+      errors: [{ messageId: 'missingConfig' }],
+    },
+  ],
+});
diff --git a/packages/eslint-plugin-prairielearn/src/tests/aws-client-shared-config.test.ts b/packages/eslint-plugin-prairielearn/src/tests/aws-client-shared-config.test.ts
new file mode 100644
index 00000000000..1529690b358
--- /dev/null
+++ b/packages/eslint-plugin-prairielearn/src/tests/aws-client-shared-config.test.ts
@@ -0,0 +1,41 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+
+import rule from '../rules/aws-client-shared-config';
+
+RuleTester.afterAll = after;
+
+const ruleTester = new RuleTester();
+
+ruleTester.run('aws-client-shared-config', rule, {
+  valid: [
+    {
+      code: "import { S3 } from '@aws-sdk/client-s3'; new S3(makeS3ClientConfig());",
+    },
+    {
+      code: "import { S3Client } from '@aws-sdk/client-s3'; new S3Client(makeS3ClientConfig());",
+    },
+    {
+      code: "import { EC2 } from '@aws-sdk/client-ec2'; new EC2(makeAwsClientConfig());",
+    },
+    {
+      code: "import { EC2Client } from '@aws-sdk/client-ec2'; new EC2Client(makeAwsClientConfig());",
+    },
+    {
+      code: "import { EC2 } from '@aws-sdk/client-ec2'; new EC2(aws.makeAwsClientConfig());",
+    },
+  ],
+  invalid: [
+    {
+      code: "import { S3 } from '@aws-sdk/client-s3'; new S3({ region: 'us-east-2' });",
+      errors: [{ messageId: 'improperConfig' }],
+    },
+    {
+      code: "import { EC2 } from '@aws-sdk/client-ec2'; new EC2({ region: 'us-east-2' });",
+      errors: [{ messageId: 'improperConfig' }],
+    },
+    {
+      code: "import { S3 } from '@aws-sdk/client-s3'; new S3(wrongFunction());",
+      errors: [{ messageId: 'improperConfig' }],
+    },
+  ],
+});
diff --git a/packages/eslint-plugin-prairielearn/src/tests/jsx-no-dollar-interpolation.test.ts b/packages/eslint-plugin-prairielearn/src/tests/jsx-no-dollar-interpolation.test.ts
new file mode 100644
index 00000000000..5ee2ce3d0de
--- /dev/null
+++ b/packages/eslint-plugin-prairielearn/src/tests/jsx-no-dollar-interpolation.test.ts
@@ -0,0 +1,59 @@
+import { RuleTester } from '@typescript-eslint/rule-tester';
+
+import rule from '../rules/jsx-no-dollar-interpolation';
+
+RuleTester.afterAll = after;
+
+const ruleTester = new RuleTester({
+  languageOptions: {
+    parserOptions: {
+      ecmaFeatures: {
+        jsx: true,
+      },
+    },
+  },
+});
+
+ruleTester.run('jsx-no-dollar-interpolation', rule, {
+  valid: [
+    {
+      code: '<div>hello</div>',
+    },
+    {
+      code: '<div>100$</div>',
+    },
+    {
+      code: '<div>$100</div>',
+    },
+    {
+      code: '<div>$</div>',
+    },
+  ],
+  invalid: [
+    {
+      // eslint-disable-next-line no-template-curly-in-string
+      code: '<div>${message}</div>',
+      errors: [
+        {
+          messageId: 'dollarInterpolationNotAllowed',
+          line: 1,
+          column: 6,
+          endLine: 1,
+          endColumn: 16,
+        },
+      ],
+    },
+    {
+      code: '<div>$ {message}</div>',
+      errors: [
+        {
+          messageId: 'dollarInterpolationNotAllowed',
+          line: 1,
+          column: 6,
+          endLine: 1,
+          endColumn: 17,
+        },
+      ],
+    },
+  ],
+});
diff --git a/packages/tsconfig/tsconfig.package-cjs.json b/packages/tsconfig/tsconfig.package-cjs.json
index 5b785fc3dd7..828a00de5c1 100644
--- a/packages/tsconfig/tsconfig.package-cjs.json
+++ b/packages/tsconfig/tsconfig.package-cjs.json
@@ -1,8 +1,8 @@
 {
   "extends": "./tsconfig.package.json",
   "compilerOptions": {
-    "module": "CommonJS",
-    "moduleResolution": "Node",
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext",
     "esModuleInterop": false,
     "allowSyntheticDefaultImports": false,
     "verbatimModuleSyntax": false
diff --git a/yarn.lock b/yarn.lock
index 19385e99cfc..b7fca56b7ce 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3349,7 +3349,15 @@ __metadata:
   resolution: "@prairielearn/eslint-plugin@workspace:packages/eslint-plugin-prairielearn"
   dependencies:
     "@prairielearn/tsconfig": "workspace:^"
+    "@types/chai": "npm:^5.0.1"
+    "@types/mocha": "npm:^10.0.10"
     "@types/node": "npm:^20.17.23"
+    "@typescript-eslint/rule-tester": "npm:^8.26.0"
+    "@typescript-eslint/utils": "npm:^8.26.0"
+    c8: "npm:^10.1.3"
+    chai: "npm:^5.2.0"
+    mocha: "npm:^11.1.0"
+    tsx: "npm:^4.19.3"
     typescript: "npm:^5.8.2"
   languageName: unknown
   linkType: soft
@@ -6196,7 +6204,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/parser@npm:^8.26.0":
+"@typescript-eslint/parser@npm:8.26.0, @typescript-eslint/parser@npm:^8.26.0":
   version: 8.26.0
   resolution: "@typescript-eslint/parser@npm:8.26.0"
   dependencies:
@@ -6212,6 +6220,23 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@typescript-eslint/rule-tester@npm:^8.26.0":
+  version: 8.26.0
+  resolution: "@typescript-eslint/rule-tester@npm:8.26.0"
+  dependencies:
+    "@typescript-eslint/parser": "npm:8.26.0"
+    "@typescript-eslint/typescript-estree": "npm:8.26.0"
+    "@typescript-eslint/utils": "npm:8.26.0"
+    ajv: "npm:^6.12.6"
+    json-stable-stringify-without-jsonify: "npm:^1.0.1"
+    lodash.merge: "npm:4.6.2"
+    semver: "npm:^7.6.0"
+  peerDependencies:
+    eslint: ^8.57.0 || ^9.0.0
+  checksum: 10c0/5ebb77de548396349859bffec7aaf06bc076cabe294a9fc0c9d7e564c770b1e090a0f86a19312a339589ec833149913a8572d89d4ed53db8c8feb7e9635ac4cd
+  languageName: node
+  linkType: hard
+
 "@typescript-eslint/scope-manager@npm:8.26.0, @typescript-eslint/scope-manager@npm:^8.1.0, @typescript-eslint/scope-manager@npm:^8.25.0":
   version: 8.26.0
   resolution: "@typescript-eslint/scope-manager@npm:8.26.0"
@@ -6262,7 +6287,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/utils@npm:8.26.0, @typescript-eslint/utils@npm:^8.1.0, @typescript-eslint/utils@npm:^8.25.0":
+"@typescript-eslint/utils@npm:8.26.0, @typescript-eslint/utils@npm:^8.1.0, @typescript-eslint/utils@npm:^8.25.0, @typescript-eslint/utils@npm:^8.26.0":
   version: 8.26.0
   resolution: "@typescript-eslint/utils@npm:8.26.0"
   dependencies:
@@ -6456,7 +6481,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"ajv@npm:^6.12.4":
+"ajv@npm:^6.12.4, ajv@npm:^6.12.6":
   version: 6.12.6
   resolution: "ajv@npm:6.12.6"
   dependencies:
@@ -11526,7 +11551,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"lodash.merge@npm:^4.6.2":
+"lodash.merge@npm:4.6.2, lodash.merge@npm:^4.6.2":
   version: 4.6.2
   resolution: "lodash.merge@npm:4.6.2"
   checksum: 10c0/402fa16a1edd7538de5b5903a90228aa48eb5533986ba7fa26606a49db2572bf414ff73a2c9f5d5fd36b31c46a5d5c7e1527749c07cbcf965ccff5fbdf32c506


Reviews:
user: login: nwalters512
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T00:53:34Z
user: login: nwalters512
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T00:54:34Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (8)</summary><blockquote>

<details>
<summary>packages/eslint-plugin-prairielearn/src/tests/aws-client-mandatory-config.test.ts (1)</summary><blockquote>

`1-24`: **Consider adding tests for other AWS clients**

The current tests only verify the rule's behavior with the S3 client. It would be beneficial to add test cases for other AWS clients that the rule is intended to enforce.

```diff
// Add additional test cases for other AWS clients
ruleTester.run('aws-client-mandatory-config', rule, {
  valid: [
    {
      code: "import { S3 } from '@aws-sdk/client-s3'; new S3({ region: 'us-west-2' })",
    },
    {
      code: "import { S3 } from '@aws-sdk/client-s3'; new S3({ region: 'us-west-2', accessKeyId: 'foo' })",
    },
+   {
+     code: "import { DynamoDB } from '@aws-sdk/client-dynamodb'; new DynamoDB({ region: 'us-west-2' })",
+   },
+   {
+     code: "import { SQS } from '@aws-sdk/client-sqs'; new SQS({ region: 'us-west-2' })",
+   },
  ],
  invalid: [
    {
      code: "import { S3 } from '@aws-sdk/client-s3'; new S3()",
      errors: [{ messageId: 'missingConfig' }],
    },
+   {
+     code: "import { DynamoDB } from '@aws-sdk/client-dynamodb'; new DynamoDB()",
+     errors: [{ messageId: 'missingConfig' }],
+   },
+   {
+     code: "import { SQS } from '@aws-sdk/client-sqs'; new SQS()",
+     errors: [{ messageId: 'missingConfig' }],
+   },
  ],
});
```

</blockquote></details>
<details>
<summary>packages/eslint-plugin-prairielearn/src/tests/aws-client-shared-config.test.ts (1)</summary><blockquote>

`1-31`: **Well-structured test suite with appropriate test cases.**

The test file is well-organized and covers both valid and invalid scenarios for AWS client configurations. The valid cases properly test the allowed configurations (makeS3ClientConfig for S3 and makeAwsClientConfig for EC2), while the invalid cases check for inline configuration objects and incorrect function calls.



Consider adding a test case for an S3Client constructor to complement the S3 test, since your rule implementation handles both cases:

```diff
  valid: [
    {
      code: "import { S3 } from '@aws-sdk/client-s3'; new S3(makeS3ClientConfig());",
    },
+   {
+     code: "import { S3Client } from '@aws-sdk/client-s3'; new S3Client(makeS3ClientConfig());",
+   },
    {
      code: "import { EC2 } from '@aws-sdk/client-ec2'; new EC2(makeAwsClientConfig());",
    },
```

</blockquote></details>
<details>
<summary>packages/eslint-plugin-prairielearn/src/tests/jsx-no-dollar-interpolation.test.ts (1)</summary><blockquote>

`1-40`: **Comprehensive test coverage for JSX dollar interpolation rule.**

The test cases effectively cover the main scenarios for this rule - valid JSX content without interpolation patterns, and invalid cases for both explicit template literals and suspicious patterns. Good use of eslint-disable comment for the template literal test case.



Consider adding an additional test case for a more complex JSX structure with nested elements to ensure the rule works correctly in all contexts:

```diff
  valid: [
    {
      code: '<div>hello</div>',
    },
    {
      code: '<div>100$</div>',
    },
    {
      code: '<div>$100</div>',
    },
+   {
+     code: '<div><span>Valid $amount dollars</span></div>',
+   },
  ],
```

</blockquote></details>
<details>
<summary>packages/eslint-plugin-prairielearn/src/rules/aws-client-mandatory-config.ts (3)</summary><blockquote>

`1-48`: **Good modernization of the ESLint rule implementation.**

The conversion to use `ESLintUtils.RuleCreator.withoutDocs` improves type safety and maintainability. The message template with variable replacement is a cleaner approach than constructing message strings inline.



While the rule works well, you could enhance type safety further by explicitly typing the context parameter:

```diff
-  create(context) {
+  create(context: Readonly<import('@typescript-eslint/utils').TSESLint.RuleContext<'missingConfig', []>>) {
```

This would give you better editor completion and type checking for context methods.

---

`20-21`: **Empty defaultOptions array can be optimized.**

Since you're not using any options for this rule, you can make this more explicit.


```diff
-  defaultOptions: [],
+  defaultOptions: [] as const, // Explicitly indicate no options are expected
```

---

`33-41`: **Consider including node name in error report data.**

The current error message includes the client name, but you might want to provide more context for the error location.


```diff
  context.report({
    node,
    messageId: 'missingConfig',
    data: {
      clientName: node.callee.name,
+     nodeName: node.type,
    },
  });
```

Then update your message template to use this additional information if needed.

</blockquote></details>
<details>
<summary>packages/eslint-plugin-prairielearn/src/rules/aws-client-shared-config.ts (1)</summary><blockquote>

`108-111`: **Consider providing more context in unknown config error message.**

The current error message for unknown config types is quite generic and doesn't give developers much guidance.


```diff
  context.report({
    node,
    messageId: 'unknownConfig',
+   data: {
+     clientName: node.callee.name,
+     configType: configArgument.type,
+   },
  });
```

Then update your message template:

```diff
  messages: {
    improperConfig:
      'Config for {{clientName}} must be obtained by calling {{desiredConfigFunctionName}}().',
-   unknownConfig: 'Unknown config provided to AWS client.',
+   unknownConfig: 'Unknown config type ({{configType}}) provided to {{clientName}}.',
  },
```

</blockquote></details>
<details>
<summary>packages/eslint-plugin-prairielearn/src/rules/jsx-no-dollar-interpolation.ts (1)</summary><blockquote>

`24-69`: **Consider handling additional edge cases in the detection logic.**

The detection logic is solid for the common cases, but there are potential edge cases to consider:

1. What if there are multiple dollar signs in the JSXText node?
2. What about nested JSX elements where the parent element has text ending with a dollar sign?
3. How about cases where the code has comments between the dollar sign and the expression?


One solution for handling the multiple dollar signs case would be to check if any non-whitespace content exists between the dollar sign and the end of the text:

```diff
- if (previousChild.value.endsWith('$')) {
+ const textAfterDollar = previousChild.value.substring(lastIndex + 1);
+ if (previousChild.value.endsWith('$')) {
```

Additionally, consider adding a test for nodes that have legitimate dollar signs in the text that aren't attempting interpolation.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f7958f5cbd66fd7232cec5055c0c5501d168e83d and ac0d336491cfc6ea22caeab9a29904542c77daab.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `yarn.lock` is excluded by `!**/yarn.lock`, `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (11)</summary>

* `.eslintrc.cjs` (1 hunks)
* `packages/eslint-plugin-prairielearn/.mocharc.cjs` (1 hunks)
* `packages/eslint-plugin-prairielearn/package.json` (1 hunks)
* `packages/eslint-plugin-prairielearn/src/index.ts` (1 hunks)
* `packages/eslint-plugin-prairielearn/src/rules/aws-client-mandatory-config.ts` (2 hunks)
* `packages/eslint-plugin-prairielearn/src/rules/aws-client-shared-config.ts` (4 hunks)
* `packages/eslint-plugin-prairielearn/src/rules/jsx-no-dollar-interpolation.ts` (1 hunks)
* `packages/eslint-plugin-prairielearn/src/tests/aws-client-mandatory-config.test.ts` (1 hunks)
* `packages/eslint-plugin-prairielearn/src/tests/aws-client-shared-config.test.ts` (1 hunks)
* `packages/eslint-plugin-prairielearn/src/tests/jsx-no-dollar-interpolation.test.ts` (1 hunks)
* `packages/tsconfig/tsconfig.package-cjs.json` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* packages/eslint-plugin-prairielearn/.mocharc.cjs

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: build-core-images
* GitHub Check: native-checks

</details>

<details>
<summary>🔇 Additional comments (22)</summary><blockquote>

<details>
<summary>packages/eslint-plugin-prairielearn/src/index.ts (2)</summary>

`3-3`: **Proper import of the new ESLint rule**

The import statement correctly brings in the new jsx-no-dollar-interpolation rule implementation.

---

`8-8`: **Clean integration of the new rule**

The new rule is properly registered in the exported rules object following the same naming pattern as existing rules.

</details>
<details>
<summary>.eslintrc.cjs (1)</summary>

`93-93`: **Appropriate configuration for the new rule**

The new JSX-specific rule is properly configured at the error level, consistent with other custom rules in this section. This will properly enforce the prevention of dollar sign interpolation within JSX syntax as intended by the PR.

</details>
<details>
<summary>packages/tsconfig/tsconfig.package-cjs.json (1)</summary>

`4-5`: <details>
<summary>❓ Verification inconclusive</summary>

**Module resolution update to NodeNext**

You've updated both the `module` and `moduleResolution` settings from CommonJS/Node to NodeNext. This is an appropriate modernization that aligns with current TypeScript best practices for Node.js projects.

This change is particularly relevant for ESM compatibility and will help with proper path resolution in newer TypeScript/Node versions, which is important for ESLint plugins that need to resolve imports correctly.


---

🏁 Script executed:

```shell
#!/bin/bash
# Check if any existing code might rely on CommonJS-specific behavior that could break

# Look for potential issues with the module resolution change
echo "Checking for potential compatibility issues with NodeNext module resolution..."
grep -r "require\.resolve\|__dirname\|__filename" --include="*.ts" --include="*.js" packages/eslint-plugin-prairielearn/
```

Length of output: 289

---



</details>

**Module Resolution Update to NodeNext – Verification Outcome**

The update in `packages/tsconfig/tsconfig.package-cjs.json` to use `"module": "NodeNext"` and `"moduleResolution": "NodeNext"` is consistent with modern TypeScript practices for Node.js and improves ESM compatibility. A grep search in `packages/eslint-plugin-prairielearn` for CommonJS-specific patterns (like `__dirname`, `__filename`, and `require.resolve`) returned no hits. However, since the output was empty—which makes the inference somewhat low quality—please perform a manual verification across the broader codebase to ensure that there are no lingering dependencies on CommonJS-specific features.

- **File:** `packages/tsconfig/tsconfig.package-cjs.json`
- **Lines 4-5:** The settings now read:
  ```
      "module": "NodeNext",
      "moduleResolution": "NodeNext",
  ```
- **Verification Note:** No issues were detected in the ESLint plugin package; however, double-check other parts of the repository for any potential CommonJS reliance that might be affected by this change.

</details>
<details>
<summary>packages/eslint-plugin-prairielearn/src/tests/aws-client-mandatory-config.test.ts (3)</summary>

`1-2`: **Appropriate test setup with TypeScript-eslint tools**

Good choice using the TypeScript-specific RuleTester from @typescript-eslint/rule-tester instead of the standard ESLint one, as it will properly handle TypeScript syntax in the test cases.

---

`5-5`: **Proper Mocha integration**

Setting `RuleTester.afterAll = after` correctly integrates with Mocha's test lifecycle hooks.

---

`9-24`: **Comprehensive test cases for the AWS client rule**

The test suite effectively covers both valid and invalid scenarios:
- Valid cases include providing required configuration parameters
- Invalid case correctly tests for the error when configuration is missing

This ensures the rule will correctly flag AWS client instantiations that lack necessary configuration parameters.

</details>
<details>
<summary>packages/eslint-plugin-prairielearn/src/tests/aws-client-shared-config.test.ts (1)</summary>

`5-5`: <details>
<summary>❓ Verification inconclusive</summary>

**Ensure Mocha's `after` is properly imported or declared.**

The code refers to Mocha's `after` hook, but there's no import statement for it. This might cause runtime errors if the `after` function isn't globally available.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if 'after' is imported or declared somewhere in the test setup
rg -i --type ts "import .* after" packages/eslint-plugin-prairielearn
# Also check if there's a test setup file that might declare global Mocha functions
find packages/eslint-plugin-prairielearn -name "*.setup.ts" -o -name "*.setup.js" -o -name "*.config.js"
```

Length of output: 178

---



</details>

**Action Required: Confirm Mocha Global Declaration**

The test file references Mocha’s `after` hook without an explicit import or setup declaration. Our initial searches did not reveal any import statements or dedicated test setup files that define Mocha’s globals. Please verify whether your testing environment automatically provides `after` as a global—typically ensured by including `"mocha"` in your project’s TypeScript types or by a global setup script. If not, consider explicitly importing it (e.g., `import { after } from 'mocha';`) or adding appropriate global declarations to avoid potential runtime errors.

- File under review: `packages/eslint-plugin-prairielearn/src/tests/aws-client-shared-config.test.ts` (line 5)

</details>
<details>
<summary>packages/eslint-plugin-prairielearn/src/tests/jsx-no-dollar-interpolation.test.ts (1)</summary>

`5-5`: **Ensure Mocha's `after` is properly imported or declared here as well.**

Similar to the previous file, this test also refers to Mocha's `after` hook without an import statement.

</details>
<details>
<summary>packages/eslint-plugin-prairielearn/src/rules/aws-client-shared-config.ts (3)</summary>

`1-34`: **Well-structured type-safe rule implementation.**

The conversion to `ESLintUtils.RuleCreator.withoutDocs` and the addition of proper metadata significantly improves the maintainability of this rule. The message templating with clear IDs is also a good practice.

---

`35-43`: **Import detection logic is sound and well-implemented.**

The approach of tracking AWS client imports and then checking their usage is appropriate for this rule's scope.

---

`75-91`: **Improved type checking for MemberExpression handling.**

The updated condition handling for member expressions now properly checks the property type, which makes the code more robust against potential AST variations.

</details>
<details>
<summary>packages/eslint-plugin-prairielearn/src/rules/jsx-no-dollar-interpolation.ts (5)</summary>

`1-22`: **ESLint rule creation and metadata looks good.**

The rule definition is well-structured with clear metadata, appropriate problem type, and descriptive error messages for both certain and suspected cases of improper dollar sign interpolation in JSX.

---

`3-12`: **Documentation is clear and helpful.**

The JSDoc comment provides a clear explanation of the rule's purpose with concrete examples, which is excellent for developer understanding.

---

`39-43`: **Good use of source code location APIs.**

Using the ESLint source code utilities to accurately pinpoint the location of the dollar sign for error reporting is excellent. This will provide precise error messages in the developer's editor.

---

`44-65`: **Well-structured conditional logic with clear comments.**

The code clearly distinguishes between different cases (adjacent dollar sign vs. dollar sign with whitespace) and provides different message IDs accordingly. The comments explaining each case enhance readability.

---

`26-38`: **Early returns improve code readability.**

The multiple early returns for cases that don't need processing (first child, non-expression containers, non-text preceding nodes) follow good practices for keeping the logic clean and maintainable.

</details>
<details>
<summary>packages/eslint-plugin-prairielearn/package.json (5)</summary>

`12-13`: **Test script addition is appropriate.**

The addition of the test script using c8 and mocha is a good practice for ensuring rule quality through automated testing.

---

`17-25`: **Good choice of testing dependencies.**

The added development dependencies provide a robust testing framework:
- Chai and Mocha for testing
- TypeScript types for better developer experience
- The ESLint rule tester specifically designed for testing ESLint rules
- c8 for code coverage

These are all appropriate choices for testing an ESLint plugin.

---

`27-29`: **Appropriate direct dependency on @typescript-eslint/utils.**

Moving this from devDependencies to dependencies is correct since the rule implementation directly imports and uses it at runtime.

---

`30-40`: **Comprehensive code coverage configuration.**

The c8 configuration is well set up with multiple reporters (HTML for visual inspection, text-summary for quick feedback, and cobertura for CI integration), and properly scoped to include relevant source files.

---

`16-26`: <details>
<summary>✅ Verification successful</summary>

**Verify TypeScript and ESLint version compatibility.**

The added dependencies use relatively high versions (e.g., typescript-eslint v8.26.0). Ensure these are compatible with the project's ESLint and TypeScript versions to avoid unexpected issues.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check package.json in the root project to ensure compatibility
echo "Checking ESLint and TypeScript versions in root package.json..."
grep -E '"eslint"|"typescript"' package.json || echo "Root package.json not found or doesn't contain ESLint/TypeScript"

# Check if there are any peer dependencies for the installed packages
echo "Checking peer dependencies for @typescript-eslint/utils..."
npm info @typescript-eslint/utils peerDependencies --json

echo "Checking peer dependencies for @typescript-eslint/rule-tester..."
npm info @typescript-eslint/rule-tester peerDependencies --json
```

Length of output: 733

---



</details>

**Compatibility Verified: ESLint and TypeScript versions meet required peer dependency ranges.**

The root package.json specifies:
- **ESLint:** version "^8.57.1" — which satisfies the peer dependency range "^8.57.0 || ^9.0.0".
- **TypeScript:** version "^5.8.2" — which is within the required range ">=4.8.4 <5.9.0" (thus compatible with @typescript-eslint/utils).

No issues were found with the dependency versions in use, and the high version numbers in the added dependencies are confirmed to be compatible.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T00:57:21Z
user: login: mwest1066
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T17:37:36Z
user: login: mwest1066
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T17:38:31Z
user: login: mwest1066
body: I didn't run the code but it all looks good.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-05T17:38:54Z
user: login: nwalters512
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T17:49:22Z

Review comments:
diff_hunk: @@ -0,0 +1,70 @@
+import { ESLintUtils } from '@typescript-eslint/utils';
+
+/**
+ * This rule will report things that look like template string interpolations
+ * that were improperly converted to JSX. For example, the following code will
+ * trigger an error:
+ *
+ * ```tsx
+ * const a = <div>${message}</div>;
+ * const b = <div>$ {message}</div>;
+ * ```
+ */
+export default ESLintUtils.RuleCreator.withoutDocs({
+  meta: {
+    type: 'problem',
+    messages: {
+      dollarInterpolationNotAllowed: 'Interpolation with a dollar sign is not allowed in JSX.',
+      dollarInterpolationSuspect: 'This may be an improperly formatted string interpolation.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+
+  create(context) {
+    return {
+      JSXElement(node) {
+        node.children.forEach((child, index) => {
+          // Skip the first child since it can't be preceded by a JSXText node.
+          if (index === 0) return;
+
+          // Skip anything that's not a JSXExpressionContainer.
+          if (child.type !== 'JSXExpressionContainer') return;
+
+          const previousChild = node.children[index - 1];
+
+          // Skip nodes that aren't preceded by a JSXText node.
+          if (previousChild.type !== 'JSXText') return;
+
+          const start = context.sourceCode.getIndexFromLoc(previousChild.loc.start);
+          const lastIndex = previousChild.value.lastIndexOf('$');
+          const dollarStart = start + lastIndex;
+          const dollarStartLoc = context.sourceCode.getLocFromIndex(dollarStart);
+
+          if (previousChild.value.endsWith('$')) {
+            // Handle unambiguous cases where the dollar sign is adjacent to the curly braces.
+            context.report({
+              node,
+              loc: {
+                start: dollarStartLoc,
+                end: child.loc.end,
+              },
+              messageId: 'dollarInterpolationNotAllowed',
+            });
+          } else if (previousChild.value.trimEnd().endsWith('$')) {
+            // Handle ambiguous cases where the dollar sign is not adjacent to the curly braces,
+            // which may be the case if Prettier has formatted the code.
+            context.report({
+              node,
+              loc: {
+                start: dollarStartLoc,
+                end: child.loc.end,
+              },
+              messageId: 'dollarInterpolationSuspect',
+            });
+          }
path: packages/eslint-plugin-prairielearn/src/rules/jsx-no-dollar-interpolation.ts
user: login: nwalters512
body: At first I thought it was useful to differentiate between these cases, but I'm not sure anymore. See the tests for concrete examples of what the difference is.
created_at: 2025-03-05T00:53:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,6 +90,7 @@ module.exports = {
     // These rules are implemented in `packages/eslint-plugin-prairielearn`.
     '@prairielearn/aws-client-mandatory-config': 'error',
     '@prairielearn/aws-client-shared-config': 'error',
+    '@prairielearn/jsx-no-dollar-interpolation': 'error',
path: .eslintrc.cjs
user: login: nwalters512
body: I'm happy to bikeshed on the name.
created_at: 2025-03-05T00:54:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,6 +90,7 @@ module.exports = {
     // These rules are implemented in `packages/eslint-plugin-prairielearn`.
     '@prairielearn/aws-client-mandatory-config': 'error',
     '@prairielearn/aws-client-shared-config': 'error',
+    '@prairielearn/jsx-no-dollar-interpolation': 'error',
path: .eslintrc.cjs
user: login: mwest1066
body: Sounds fine to me.
created_at: 2025-03-05T17:37:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,70 @@
+import { ESLintUtils } from '@typescript-eslint/utils';
+
+/**
+ * This rule will report things that look like template string interpolations
+ * that were improperly converted to JSX. For example, the following code will
+ * trigger an error:
+ *
+ * ```tsx
+ * const a = <div>${message}</div>;
+ * const b = <div>$ {message}</div>;
+ * ```
+ */
+export default ESLintUtils.RuleCreator.withoutDocs({
+  meta: {
+    type: 'problem',
+    messages: {
+      dollarInterpolationNotAllowed: 'Interpolation with a dollar sign is not allowed in JSX.',
+      dollarInterpolationSuspect: 'This may be an improperly formatted string interpolation.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+
+  create(context) {
+    return {
+      JSXElement(node) {
+        node.children.forEach((child, index) => {
+          // Skip the first child since it can't be preceded by a JSXText node.
+          if (index === 0) return;
+
+          // Skip anything that's not a JSXExpressionContainer.
+          if (child.type !== 'JSXExpressionContainer') return;
+
+          const previousChild = node.children[index - 1];
+
+          // Skip nodes that aren't preceded by a JSXText node.
+          if (previousChild.type !== 'JSXText') return;
+
+          const start = context.sourceCode.getIndexFromLoc(previousChild.loc.start);
+          const lastIndex = previousChild.value.lastIndexOf('$');
+          const dollarStart = start + lastIndex;
+          const dollarStartLoc = context.sourceCode.getLocFromIndex(dollarStart);
+
+          if (previousChild.value.endsWith('$')) {
+            // Handle unambiguous cases where the dollar sign is adjacent to the curly braces.
+            context.report({
+              node,
+              loc: {
+                start: dollarStartLoc,
+                end: child.loc.end,
+              },
+              messageId: 'dollarInterpolationNotAllowed',
+            });
+          } else if (previousChild.value.trimEnd().endsWith('$')) {
+            // Handle ambiguous cases where the dollar sign is not adjacent to the curly braces,
+            // which may be the case if Prettier has formatted the code.
+            context.report({
+              node,
+              loc: {
+                start: dollarStartLoc,
+                end: child.loc.end,
+              },
+              messageId: 'dollarInterpolationSuspect',
+            });
+          }
path: packages/eslint-plugin-prairielearn/src/rules/jsx-no-dollar-interpolation.ts
user: login: mwest1066
body: I understand the difference but I agree with you that we don't care.
created_at: 2025-03-05T17:38:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,70 @@
+import { ESLintUtils } from '@typescript-eslint/utils';
+
+/**
+ * This rule will report things that look like template string interpolations
+ * that were improperly converted to JSX. For example, the following code will
+ * trigger an error:
+ *
+ * ```tsx
+ * const a = <div>${message}</div>;
+ * const b = <div>$ {message}</div>;
+ * ```
+ */
+export default ESLintUtils.RuleCreator.withoutDocs({
+  meta: {
+    type: 'problem',
+    messages: {
+      dollarInterpolationNotAllowed: 'Interpolation with a dollar sign is not allowed in JSX.',
+      dollarInterpolationSuspect: 'This may be an improperly formatted string interpolation.',
+    },
+    schema: [],
+  },
+  defaultOptions: [],
+
+  create(context) {
+    return {
+      JSXElement(node) {
+        node.children.forEach((child, index) => {
+          // Skip the first child since it can't be preceded by a JSXText node.
+          if (index === 0) return;
+
+          // Skip anything that's not a JSXExpressionContainer.
+          if (child.type !== 'JSXExpressionContainer') return;
+
+          const previousChild = node.children[index - 1];
+
+          // Skip nodes that aren't preceded by a JSXText node.
+          if (previousChild.type !== 'JSXText') return;
+
+          const start = context.sourceCode.getIndexFromLoc(previousChild.loc.start);
+          const lastIndex = previousChild.value.lastIndexOf('$');
+          const dollarStart = start + lastIndex;
+          const dollarStartLoc = context.sourceCode.getLocFromIndex(dollarStart);
+
+          if (previousChild.value.endsWith('$')) {
+            // Handle unambiguous cases where the dollar sign is adjacent to the curly braces.
+            context.report({
+              node,
+              loc: {
+                start: dollarStartLoc,
+                end: child.loc.end,
+              },
+              messageId: 'dollarInterpolationNotAllowed',
+            });
+          } else if (previousChild.value.trimEnd().endsWith('$')) {
+            // Handle ambiguous cases where the dollar sign is not adjacent to the curly braces,
+            // which may be the case if Prettier has formatted the code.
+            context.report({
+              node,
+              loc: {
+                start: dollarStartLoc,
+                end: child.loc.end,
+              },
+              messageId: 'dollarInterpolationSuspect',
+            });
+          }
path: packages/eslint-plugin-prairielearn/src/rules/jsx-no-dollar-interpolation.ts
user: login: nwalters512
body: I collapsed this into a single case in c048eef.
created_at: 2025-03-05T17:49:22Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-05T00:51:36Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: rate limited by coderabbit.ai -->

> [!WARNING]
> ## Rate limit exceeded
> 
> @nwalters512 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait **6 minutes and 6 seconds** before requesting another review.
> 
> <details>
> <summary>⌛ How to resolve this issue?</summary>
> 
> After the wait time has elapsed, a review can be triggered using the `@coderabbitai review` command as a PR comment. Alternatively, push new commits to this PR.
> 
> We recommend that you space out your commits to avoid hitting the rate limit.
> 
> </details>
> 
> 
> <details>
> <summary>🚦 How do rate limits work?</summary>
> 
> CodeRabbit enforces hourly rate limits for each developer per organization.
> 
> Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.
> 
> Please see our [FAQ](https://docs.coderabbit.ai/faq) for further information.
> 
> </details>
> 
> <details>
> <summary>📥 Commits</summary>
> 
> Reviewing files that changed from the base of the PR and between ac0d336491cfc6ea22caeab9a29904542c77daab and 0bcd3cf5ba653d35ba85efc717904cc2e365ef09.
> 
> </details>
> 
> <details>
> <summary>⛔ Files ignored due to path filters (1)</summary>
> 
> * `yarn.lock` is excluded by `!**/yarn.lock`, `!**/*.lock`
> 
> </details>
> 
> <details>
> <summary>📒 Files selected for processing (5)</summary>
> 
> * `.changeset/early-pianos-draw.md` (1 hunks)
> * `packages/eslint-plugin-prairielearn/src/rules/jsx-no-dollar-interpolation.ts` (1 hunks)
> * `packages/eslint-plugin-prairielearn/src/tests/aws-client-mandatory-config.test.ts` (1 hunks)
> * `packages/eslint-plugin-prairielearn/src/tests/aws-client-shared-config.test.ts` (1 hunks)
> * `packages/eslint-plugin-prairielearn/src/tests/jsx-no-dollar-interpolation.test.ts` (1 hunks)
> 
> </details>

<!-- end of auto-generated comment: rate limited by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough

This pull request introduces a new JSX interpolation rule in the ESLint configuration and extends the ESLint plugin with several new rules and tests for AWS client configurations and JSX syntax. A new Mocha configuration file is added and the package.json is updated with testing scripts, dependencies, and c8 coverage configuration. Additionally, the TypeScript configuration is updated from CommonJS to NodeNext, and rule definitions are restructured using ESLintUtils.RuleCreator.withoutDocs.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `.eslintrc.cjs`, `packages/.../src/index.ts` | Added new ESLint rule `@prairielearn/jsx-no-dollar-interpolation` to the configuration and exported it in the plugin index. |
| `packages/.../src/rules/aws-client-mandatory-config.ts`, `packages/.../src/rules/aws-client-shared-config.ts`, `packages/.../src/rules/jsx-no-dollar-interpolation.ts` | Introduced/updated ESLint rules using `ESLintUtils.RuleCreator.withoutDocs` with enhanced metadata, message reporting, and standardized structure. |
| `packages/.../src/tests/aws-client-mandatory-config.test.ts`, `packages/.../src/tests/aws-client-shared-config.test.ts`, `packages/.../src/tests/jsx-no-dollar-interpolation.test.ts` | Added new test suites to validate the functionality of the newly implemented ESLint rules. |
| `packages/eslint-plugin-prairielearn/.mocharc.cjs`, `packages/eslint-plugin-prairielearn/package.json` | Added a new Mocha configuration file and updated package.json with a test script, new devDependencies, and c8 configuration for coverage reporting. |
| `packages/tsconfig/tsconfig.package-cjs.json` | Updated `module` from "CommonJS" to "NodeNext" and `moduleResolution` from "Node" to "NodeNext". |

## Suggested labels

`chore`

## Suggested reviewers

- reteps

</details>

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpADK2VmgqyjcEj2wAbb5AokAI7Y0riQ4rjedJAARACSGLgUitgMnkKIAB5gGIS0+L4uYPCJlNwF1PD4WACi9gAyJWEUPiQxkADuaMiI2ALM6jT0cpDYiJSQGF3eNBSIAKwAjABM6Bj0qPBKifAAZvDR3ZNOAhOLiwAsAJxXGjCwntzYFOXj/LvhCMi2KMgEKMxuFE2Il0JMSB1IHVGqCWlF0PA2PRqDwAlIdhgiJB8oU+Ih4EQsE0yhVxNVOuoEFgAFL2AAakEQskSaEydzgmww6nglSkvzBvHwaDEkk8bG6z08JTW4gY8G4lXJ+A+JQYAW60SYGCkcyqWH+tLpABpIGRehQSljcLAUUopN58NxKMh8pN8GESmhaDqaIlvPImslaKlPJRknN/CRvNRov8cdG8QSiaUXqS9Rp3A8cNb8HxcjQ/jawtbUHDPF1kCVENx4AFhvIGDbfGRSMgyExsCnoh0EPCOrmANaW/hYNCokgSKpjLy+SPBUKQAAUZkWAAYrhcLgBKdn4U0YG0YNKfUO7XaicTo6TIZUnqENJr+VqIE3WzxoPCwXOQG3IbDcWgY3oN9TUyJBxExJ8oj+PdTkgXYfH9cJZCdMBEDQc8TTweBvHgAAvYcFQYAc0FbVF8EnJR6zvAADAABXAUOkNV5VwMBpFwxIaP4CgiEwfDFQwO4AEFaFoblqjQXxZFfLMflVbxsCUQspUSINUjJLBbwLXBkF2b8QJIMDEAgrEyxg/dzU8N9a0jXCVBw9R5EwegmAoAIxHIRBEAzAwADFvwQig3z4JRcG0bwXzvJ5ZwCecTMYTBIDg4U0m8w5iyzfTfHwDphwAVRseouAAbVgXBcG4RAOAAehqohKT6DQmGYGqrCoWsDnqEgXAwNqOotEhut6mqYu8GrzmuK4AF1F3KyrqrqhrrSalr+u0QbhooPr2o2rqeu20bEImy4bh3Ix9GMcAoDIehbw/AhiDIZQhgUVh2C4Xh+GEC9RRkBtFGUVR1C0HRLpMKAOUrZAkoewhSHIKhXpakFOH8NBIV6ZxXGSgGlCoYHNG0XQwEMK7TAMDQOMDBhmoyDgDBiJmDAsSBhLiJ7EaAxknGYFx5FvRtMFbIxhPBSFoUfMsfyOU4yHQMTYz3ECtX2IhnkE+CcM8GiqcQTiklphgMl1+5PGl+jeD2qMDr6jJslyMB4yKYlU2jTTuN/ZKSHlwMUjSeg8utNZTXc78ontRkSAqy12S+BXxM07F8GkN0wmYRQ9mcjB5CM8Dh3MniFAwNWNc0mQ8FAhV1hUqOwlvA2C+ffd9IoAPcbvSXQVVgky/TTNPHISFpc5X0qPCPcyFb48x2rUQ9ngBgFHEyCTJclx6ACd2lcgQ1GWZcLMhNOe5X2BgpKQwUEDUUzk9xRkkxQFNyndvUKRLGl6Tjyh3wCN1sRtuXIuGdxJn0EhZPOJkm7QRNC5O8FFlCzhMi0MQkp3h3h7urJGb8Ah8yrKMI8h5SC0F8uYSwwkZgvT1BZECSgGAJnAegoy5RgrRG/E8AQuEl7sG5NIC6kAAByEIoLvkVrQLgNEADkdEradRtiNe2OQ8gFATMUZ+aZqiSK4JIsMuZJHcWlCBLuYRMF93JNKXW1M1JGxNozZmBgIBgCMEREirYapWLYkCbADUMBgFkYNKII0NAZyFm3Om1U7ExBZuQjmCMXrRCxnzHGgsiF8IMGLIexdS7YPJPseEusQk2jCcbRAntZY+2TEkf20RDFZhoi40i0h3H6yaH4xSPi/EDQOIE7a3FxIeQIK4OOqA8mhkyCw3SIdBAiDEJ8FEJ8F6pzHHFbAtYSCo3ggZOpulMjcRAa0E0PZF6wH5LhAcUYAylHWDvMgKh4TQCYvYFi3Awi9G4BMzZfAdLDiDicgAsvgIWpDols0oTkjANCsx0IYUA28zDcyvXYX0Lh+4IgHEQPwoRkJTHgq1vCNUPUhgSP2VEKm4yEWlKfpAepwpXFNI8W07xJROnWx6X1YJgKik2Mpb8sEKy1kbNbrRHZeyUiksiRdCmDS3EMq8R0/x3TbajVpY0jQGRqgMyZlE1m7NObxPoIk/m6ChaYjSR4RgqTKz6jqdKkgarEDVG4qMz5tEZFdPkYdWV7SSjcVtU/ehSlrJZi9InN+91xaMmeWEIV3zIJwPGDqKSEa7RRkdBspQTprlHnRd/aldpuKICjTLGQFTIAgMWcBPcClA1ghRmOFEhl1gmmyg6Do0QRggRDRJLSHwxw0R0gWot1oUQtAhdShgAAOMtnLZ5txqgAKnnQujQOkV2lLuEYBItE7QABESCZu2HKaQBbfrVGPhORByb923XbOimWfI5ajjERIhiTFEA1SFvAbivKxw6nxEqD4NEAB68wNCrg0IsGiJp6KMSdO+wpaBv2UjBH+sNgGgNrjAxB1cUHqWvrg1G9iLTEg1TLGAHSlAkPB1/c6ND1KgMTo0MsAAbGB3DNFJ1UZOTR3UAH6OYcWBoAAzOxz9XGUO0b48B0Dyw2PQYQ+Jnj/6e38cE4JnDsD1jUpFe/bjkBUNSaAxcCDtwRMiTEt2i+MkwSZJohmm92bj1RxFOSL2j6n5VODAHQ5CBGz+sUspPDsHmIWheURg2NVsKRUU/pyTKngOMZY2xjd/lnghQzgEWBEacWayFRxidZSS3yxDXQY+TpT6yGHCBAIEznQT29qMcY9BFw0XKswbw7GaCZDYoa1wuG4EccEJQXAGsaJblgQ6SCvLHRAP+NWpQ6BZyjJhlphZux5AgQdc8Y8y36twXmzU61ngmCJtIJGCZwKdVgsYf8WhogYXUKYeS1hd0+AcJRTw8QaSoBYsjaFsIJXxHUpiDpGIXAYiTunULRkc7F3LtXbpGIBisA0ULf9yl4wXPxdte+r1TLfEKo9X1W19rHUOMEcI+zWaGDOWfUFt9H6bRft07F3jKOQNYcg1SuzE490HtvU5zHSdbw0uIo03HxHPHeoJ+6tlyqxekFJxgGi5PftU8PbTqiL7gvwZneJgz7PMPgbXMjvNvPr3U/RSerH6DRd0olwbRl8rZdKpJ+q5XqvKcW41wnOg2u32EY8aR1o5HQiUZZwb+jiXWM4e57u73AuMenuxyqmVkunfMsJ3Lt3DqPc/a9/zo8mu/fjoKxHuL/HjfCdNzziQfOHNHqTzbkXOPmmO7lZnl3I0c9k/z5CdX7Zi9A440z/XFfpNMbY3H83hfG/W+F4B1vePnestd6nu17uVd94AbPofxK9fl7Z6piDU+LHx9n1b5zC/qVL/Tx3mXq/u/r6V1vin/eE9F998PnTvLI/AeM4sKZjXufg3pfkLnRnbuLm3q0vfiynItns/pvp7u/rvl/v7gRv9uFk0JFuINFofsplHkxjHsAR/nPlfhAbfu3tLnAQEmvgrhvrnq/r9jlknIDi6vljXpQTAdQVnnQXSi/hKg4lKuvg7twfjjQYqiNIgHOiUEoGyLpJqszDqrEs9EjAkrzEaiksLGalmCamRH7F5u+BGmWNBooo7M7BQGorMC/IJLhrdnUuZNxPCsFFSiBDRLIUZGuk6trOgkYg+KCPfsMsgCPJWICAitELsMkMwLRBoMHtBDVGYcoriFYSSK/NUPat4fCHAmwfYWMvnJBDRI4d9DMgDpNkQPiAtiBLwBOFOPrPIEoPsOQJvM3HlhjIgGAPQgcIkGAHzOsNQLmLIB0dUGrNxANm0R0bhOwGhEUnQEMSXASKbAIpPMZLfIXG2n/OWvsGwnwLgggiQkYGQqCrMDdsrFCvdi4IwnCs9oim9siovKirwhisgSImgdSpIokU7Coi7OomkRgFopAPbEsTul8RQAkNYRosrtzs4SYtUAlEURYlwd0bAbwVITIdcvIaUoIY4s4iIdAUiTwV3odNIQwHEU0uMZ0VMb0YBIMoMZgmuoodqjEnqmoQahockh8HoTocdpah5upDPBGsghpGgjGlmMYi8SMFFvhMODRMYvlLgT5DYK0AAMLqiDIaC/L4B4DAkMCUpVg0BejoJjhAjaBaQ/RiBBHTqgKLyaxkCHhpR3hliSLIBsDhTUljgNElDdomjzZVbBoWbX59oumIbkROjBQbZFiMjlaLK1zSzBboDICSKCicLrKSJrD0AeleRghsDeSNLhDrLGk0AuoDDeTDgsHphmzUrZnoStjcTTIXhuiQgHYwyVlID4iYhKnDELGoquA+bHL8hjCHZ3i6I7H7oIrDhsAmpIDMApbmocaqkkB7LRxfibzrImnIAJikBISdickbBEiTIOgNQMAmgCCVwgRdDyBVni7oB/w1bhH0Bualr/jUl0B3AJAmQ9R3QfD9IXhIRaiCkiiQQgSXlnbII/KUi1I6xahdaaC3nBTcTny+CySDzCKIhAjrLsCawDm1w0TAUkBxC0C+rJChmMT1YBDngBBHhBo6zFltlEAdnzFECLk5lnZwKCiUSpwqyTGggYBoBsCfDJDeInJuFum1lmmaD3CoCcmMjhR9EUDiR4QcVZi4XnZjmQSChpQrYbCAjJBSDOkmnhSehqC4SMS+SiShqSTSRIVm67AfgzAADyLy1ChFjow28gD5xWYi3pXI4gUk+EhwK2pogIJFLgVA1mRyfmfM8gNlcoxlMY8EeAaCZZSojlsJLq1WrQvkdliaiFGCPJtpmAx4m2SQQpf8cC9CLgTkvhop/hzQrQnQvYYo+lJpw46gyAbkngCER4mkvlJlBxIKFCxxs2pxng0KFxsKHw0J2xXgnC9xn26K/CfyS5igD8hI1AaCT5QEEi0JACNlPgYQAA3owPOYuFBUZGjJgLIFuJAPtZwbicvp3o/kSXOuZDVOSVxWxFSf0a4HMWrF4ZAIAEmE1K21DRtlYQsp8pGgipUQKphKuY6plImpuA2piAi4h1QZXAh1GgWNkAAAviaCDXtQ5eXKVNNCaASjGCddUNBVdTddzoiVLuISiU9SSS9W9V0R9evDST9QSF4VicIfQaIfiYzYSX1MSaSe+mzVMYgDMbQNzUQPSRKsocydzL1gLByZagPBatoVap5iGDZsIv+SNn/CKZ4GKdLBKbgVKQUeDThAqcqaqXDRqVqYCrqRCvqZ+QKNGNKHWeaRJc6ZnGAknE2XeF2gGZWdHMGYKMReGbaCQI0YpebK0E6chE6CaLhVFIWg8HzJpumXHaDfwClRCrmunS6rBbfKhURRMElWOnAp2AOLkB0FgNXcgDaA+qWhmdEGMOOdeLmXEDulau+QabeCUJxJ4KBZiFFBfLlMOEKqcBVBMA6AhQJKwVpr0Y0hsreOnTOXUuTTQE6tuUnLgmuZABuRcgQjuVSq1SffgIeceaeVmOeWWj3WRC4ObKOS9sWt7PLBta9P8AOeHcxXhQRWmXmtQJHZXcFOiiaPldmoBboQmJVXAngiyIZY5CRdpFmMOSpZA5iLmo2KIAOHpFssdp2VgprC4OrBsnGXAiBJ1TbjxXxa3Z4O5tUeMBQFILQHfZlG/RMt3ZOYgNEe5VgGvLJfJTvJKQpXeJkspYbZKClllYgt4FZZyW2AeAVVRSIsnbI6VVplg2XWBTmJXFJLML6SQ3/AefcbeF+JCMJAAOr2CMDvVZK9zgowx/wSC+XPn7EHFXaDWPa5EALlWuNPa1avbTUfY7BfZPFQA1DXHRCfbyA/0l40TA1517XXVHWEokCU2lDdZcAXU023UC14kM0r7wFKpi2s0dDtEUndHS2v2y10m6TcSA0pPXE7X5022RSQ322w0UDw2GNI0u2o1P3hQY0p0kDaJJnAiSJp3P3SDjMV0uUUD0VqzaKrMEgur7X7W1O4ACK8UkA4041lpjBhBwSCAGVNEdwIWcRYjbPKRrK0AbNEB+QH16j7NsBHOLg7izMEL125QYDPPaL5QYD/ON3ONYhsVbA7x2MOO7MaC/N42RpZ1oAk0mhIsE32WF3VSQAlSk2ZMU2nXdaFN013V34EmPWi3PXPivXVMTHs3TENNy281aqSpOIGD00Z4P7lOoks00sfEWEpFuyCQK2stK1xIsk8zYxq1a2mpPEZLCJm11VCO8nVK7l3jOo0QCsglCs2GaReG9l+baDMAWTQs7DrYgNl0AhR0TA0CAjbzSUWiQSux6uPa/LSh7w2yow+QVnSxrbWnSThAUPRxtUajIA9g/x3wJgrU11/wDm7loDvJEUWgxj+jHypDcZWoasqLT2QRGS8VoU4s0TADiQSB6AAAk+1uFONwANUpbegoxWmxb9b5b111btb9bps5qIRO1JQndtF1KXTdt0NDt/TTtQzOp/WWm6g/mgazpEdbpkZ8862pjEz6C0z6yID3QDqco3MJdQq+juDmtc5WT+9XVb87rR2LxHdzZk4+IgyPA1Asw1qKIVY5WkyNEhoNQwI7A3EuQykdwAUfAPUfmnrP7iQ3pHog1ilAlRAJyV9jYOEtAFFx8Q47yq7+wcwJivYBqqoUoJimA+YDW1RAc7azk1Kho0AZ1f7gMr52HBDlYHwKszwFF2HSH/IfaX74yAQJZ1Q9Flz4ecCAQSQBwul0UHkdApZOHr5THWYJHknkEiH3gGwzZn79IVH3WuGM7CFDAPgMYtcrw3aVVng0YCUFhMbl9kyHjikdq7g/Yxc5lsboY1n2Am1kABggmkAcQsnjwEnK8WIandIGnuAThNcLO7pIJMbkH2DkySUWDsh1pt8w6HoPxmsP6UbLgFnqARHU9ba+xsmXnPn44Ac0plH1H+4tA4byGEX98+IhImyOUeXHcRyBYREJA0XZdzZvQJ8BHbwEbf8tDuYfMMckEkV96zDpaxjP8nDPMRArYSXEZ2nmpynDWSUSzoZSErcw3r049WILrEJvk3bdV+QqcRHHdaw8gmdq5RcmL9c2LzagKXdq8BQZ95Id2NANuQn79t8GD0gY9UZZ8iU4wPkfVvjVCqVATo1wTVxoTRc72s1kT81zx0sgOW17Tt394MIuAcpttPTI7fTAzX4ztOpqNWNGgONW4RT9uJTXLEhRONUlT/LWQSinxyR+3vxLL9i2JHLZLVBwtlLDPc6OkEtdLuzPRnNAxzLCOESYrTJErKtbJMrKjosEaOkeKngKrBhIY9AJtmPUsdVNEkt3Rn1XNmCXbWUPhqFFKtEUNJAVH75FATqUR9OGBrEWBJGZGFGjvID6V8IrVUYHwkRLAd4/r0QCo1odHdcf43AYINEtv9vswyOIjM8WmN7YIavvQ6gHVxDIiuaGfqyhZwduA9n1nWwy8YyBb0EcyMJ7kP5lyIjPlhZbh9gImjj7NLO1WQQqydYELZiGAOdI4qkpf9A+b9rQaKIT7+Zkyc2btmAjfGjNELf8FTj474IGlRqCoVALpzohrQlFoc3dWcXYcfAvKQF8zXnO6LZJZ7ZpD5vY9xVqCf8v3eZJkRDjXtcIjgEclnyw3LqSr8IsaIgNAwhTPBfSDaLMObRIBMMYYbYcZBeEHICB3QJyYfiAxKAoDC0tyC0PgBB4+NyE12IaneCh6XEJqcTMJvD24SI9vsb+F/q8gL6iItc1KMsAn0oAaBR0i4SREbw5p9FTepDX5iYWupk8KeVPKAvdW5a0FeWNUYXrSxqbvVxe3AyXk01CCc8okQhdlpy2RIi1BeJJKQZwMZZ1gpeSghQorTl6qEFe0rY1BrXSSq8FwmrXQfU30GKCTIf1TXmpDVa79RgltBSthXj5h5veQfaIm4XwwhY3eQeT3r4N9Tr56sw/OKiBFOCt0qgfAZ/ob1F6yD7BsxM3rnwrL58s+s7QLIgODgoC4EaAzxjQKB6pxdeZYXNCgPPjA8LUBDF1G5AGSNZcyt4WFg4B3QABpNvuwCq7UYk2joFNoWWrrxVz2sJMrMuwDabdvwi/VvgNhqBKllgd/J+NULDbXkRq0gJMDvBE4H9gOx/cNg8GTCND6yzdNYeRHYozdegRrF0GsjECXxt2w4JKD7TCDpcAgDUJUHwCe5YgxwHQZIJBDoZJwhU1dO4DUGFDwdtQJQmof9wmFLIsAzCeASP2P5P1AGKAbYOIC2LUQ3C63SgM8yYrVlPAfdPPguEz6F8egD/I2jchAEDcIyvvCbtAPjJVx4RBCfGPpgqqakegaQHilgJwGHEBq4PMdJD3OLQ8SBsPJFDNQoFooqBv2NXs6l3rJM7BMtAwU4OaaktimogunnLjFpSCVcrLVQTiVVHkt+ePLZmpINCDvptWbPVLvq2l4Ml+qKhLmDt0V4WDtaKvTJNKJ8Jatme5hHVuzxFbS9CsX9SpHyR3jRCm+WUN5hZUqpJDzRqiH0R7BeL+DO4NVHgNLlzTOorewUbwa0GYF+DneMGAPJgVCEh4vejbYCBALqr+9vAuwSPuMEmT/gbeWY8ISzn9ZLxN+bDAugQNuTJld49IfeCyDZBZCiRdAp8GOk7DMiQIDDTwO8U9FJEYxlovUKmSKFHgAsHFezuAVhISJh+JY6lMUNwgEVt6OsTceQXJBQVj61oAINZAXCQjCwKIfBsRBdR7wmAC2RAFyHeQhtq+ycNOI1hGqRdYxeodNlcOpQltJAegB4DlA7bAT2MQEstmuFXDltwJZbKdvQGbbATy2ME+CQ21zTuFwRu4+fG/CL72c1eV498UoAzhu01C1rCBs0LOy3hzOv4jVIP3C55l7WcVYypemQlltK27bOthBPqp9lthpACMEf3DAd8lK5/PupAEkQWEwSqRQSEsVwAUJW0dAX5tkXzAPAT+1XSMsKFUiEDIudXFHFBIrZtt5mNbbiQhPfH8TD+sIxEaf1EnIjxJkkkEtJOFaaRHAPXSRPI2ypKNPgqAN0fCFCB3IkADwZspEQOb9gKAA4F1GaFAFwNwBOsaMd8XBK/FuIPbf4XqFLK5gBkSEEYCGOHD5DuMDANIC8juTvgtMnYVKPunChdi944fZ9tyP6r4D/Gw1QJg9lSow87ycPO4uKMeKYphEvk47POWHzxTLCdEoSH6JVHU81RTNKltoNNE1RtRXPfmtT10iYJJBhaUhhoFtQdEMgSuG0eK1MEOjzBWhOVprRUauFtka0hihtPXxbSfIm+dXssIKB8hNiiXRqbRBag1gogFAImk5TrimQfW9gAHpMOsxuESUC5dsW/BVYX0ExNEGIB2VYDVBaQSOerDDKWJKAhE3WJHAPxBlioSANgaQAUGwiOpwZrmI4FJAdQBjRgAEbmNDJiCoy2g3Ef4CjMBjozcASOb+G8Cm7BMEu58QsrPAQC7AUud4UGfvHfLREvuDqRSEnGQQxgiAiTJrB3BAgPInQTyf7G9A+ksDQeeAvxhDyalEDxqVcdqaKIiYSjomlZHGbWULqUyvGEiGIKDLByxA4ZpExGcILcTLTSGq0ukptJKQv4AawOO2eDjpksykZCJXEm7IYoez1pXs7aUgSgA4UcZeMiWYTMhIzY34STYfLbPjn4zJZeoe2bTMBjByUcreMOWrAjmXSo5t0xgr7JhmgyE5BMzSLnMDlnUC5N+UORdJLnFyea5cgQjqMugGAIYFXQ0ngHhj7TNQLAVGFwCoCYxHRIwR8UDBvigwSYhgfuSjHUAAB9LYIgFXlogDgeXVeWvBcK9z+5woVcLQCExCZmM1wRYAwF2AMBmMPUZYMsHPg9QBAVwNAMsBuCrgLg8wC4I/IADsv8wCCoEXnkwoAK83AOvMq5byaiEIOgKvNuiLygAA -->

<!-- internal state end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=PrairieLearn/PrairieLearn&utm_content=11499):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-05T01:03:30Z
body: ## All images

| Image | Platform | Old Size | New Size | Change |
| --- | --- | --- | --- | --- |
| [prairielearn/executor:0bcd3cf5ba653d35ba85efc717904cc2e365ef09](https://hub.docker.com/layers/prairielearn/executor/0bcd3cf5ba653d35ba85efc717904cc2e365ef09/images/sha256-62122e9103e1767078296ccd11165d8c6868b8c7f12257f17d24e44cd36b65d1?context=explore) | null | 1197.30 MB | 1197.54 MB | 0.02% |
| [prairielearn/prairielearn:0bcd3cf5ba653d35ba85efc717904cc2e365ef09](https://hub.docker.com/layers/prairielearn/prairielearn/0bcd3cf5ba653d35ba85efc717904cc2e365ef09/images/sha256-054bb9a66775e1d2776a8a373c9c443eb68db9313be44b3c6018bee7bb1771df?context=explore) | linux/amd64 | 1197.31 MB | 1197.54 MB | 0.02% |
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-03-05T01:15:06Z
body: ## [Codecov](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11499?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) Report
Attention: Patch coverage is `90.75630%` with `11 lines` in your changes missing coverage. Please review.
> Project coverage is 71.60%. Comparing base [(`6cb61ae`)](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/commit/6cb61ae75ad276b299d5e4f28a530e496673d70e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) to head [(`0bcd3cf`)](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/commit/0bcd3cf5ba653d35ba85efc717904cc2e365ef09?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn).
> Report is 384 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11499?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) | Patch % | Lines |
|---|---|---|
| [...prairielearn/src/rules/aws-client-shared-config.ts](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11499?src=pr&el=tree&filepath=packages%2Feslint-plugin-prairielearn%2Fsrc%2Frules%2Faws-client-shared-config.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi1wcmFpcmllbGVhcm4vc3JjL3J1bGVzL2F3cy1jbGllbnQtc2hhcmVkLWNvbmZpZy50cw==) | 83.78% | [6 Missing :warning: ](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11499?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) |
| [...irielearn/src/rules/jsx-no-dollar-interpolation.ts](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11499?src=pr&el=tree&filepath=packages%2Feslint-plugin-prairielearn%2Fsrc%2Frules%2Fjsx-no-dollar-interpolation.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi1wcmFpcmllbGVhcm4vc3JjL3J1bGVzL2pzeC1uby1kb2xsYXItaW50ZXJwb2xhdGlvbi50cw==) | 95.08% | [1 Missing and 2 partials :warning: ](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11499?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) |
| [packages/eslint-plugin-prairielearn/src/index.ts](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11499?src=pr&el=tree&filepath=packages%2Feslint-plugin-prairielearn%2Fsrc%2Findex.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi1wcmFpcmllbGVhcm4vc3JjL2luZGV4LnRz) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11499?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master   #11499      +/-   ##
==========================================
+ Coverage   71.57%   71.60%   +0.03%     
==========================================
  Files         633      638       +5     
  Lines      100447   100729     +282     
  Branches     7786     7823      +37     
==========================================
+ Hits        71893    72129     +236     
- Misses      28068    28108      +40     
- Partials      486      492       +6     
```

| [Flag](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11499/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) | Coverage Δ | |
|---|---|---|
| [javascript](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11499/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) | `71.60% <90.75%> (+0.03%)` | :arrow_up: |
| [python](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11499/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn) | `81.45% <ø> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PrairieLearn/PrairieLearn/pull/11499?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PrairieLearn).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE