@prairielearn/eslint-plugin 2.2.7 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @prairielearn/eslint-plugin
2
2
 
3
+ ## 2.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 70a8029: Upgrade all JavaScript dependencies
8
+
9
+ ## 2.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 2aa898c: Add new `safe-db-types` rule
14
+
3
15
  ## 2.2.7
4
16
 
5
17
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -3,5 +3,8 @@ export declare const rules: {
3
3
  'aws-client-shared-config': import("@typescript-eslint/utils/ts-eslint").RuleModule<"improperConfig" | "unknownConfig", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
4
4
  'jsx-no-dollar-interpolation': import("@typescript-eslint/utils/ts-eslint").RuleModule<"dollarInterpolationNotAllowed", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
5
5
  'no-unused-sql-blocks': import("@typescript-eslint/utils/ts-eslint").RuleModule<"unusedSqlBlock", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
6
+ 'safe-db-types': import("@typescript-eslint/utils/ts-eslint").RuleModule<"unsafeTypes" | "spreadAttributes", [({
7
+ allowDbTypes?: (string | RegExp)[];
8
+ } | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
6
9
  };
7
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,KAAK;;;;;CAKjB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,KAAK;;;;;;;;CAMjB,CAAC"}
package/dist/index.js CHANGED
@@ -5,10 +5,12 @@ const aws_client_mandatory_config_js_1 = require("./rules/aws-client-mandatory-c
5
5
  const aws_client_shared_config_js_1 = require("./rules/aws-client-shared-config.js");
6
6
  const jsx_no_dollar_interpolation_js_1 = require("./rules/jsx-no-dollar-interpolation.js");
7
7
  const no_unused_sql_blocks_js_1 = require("./rules/no-unused-sql-blocks.js");
8
+ const safe_db_types_js_1 = require("./rules/safe-db-types.js");
8
9
  exports.rules = {
9
10
  'aws-client-mandatory-config': aws_client_mandatory_config_js_1.default,
10
11
  'aws-client-shared-config': aws_client_shared_config_js_1.default,
11
12
  'jsx-no-dollar-interpolation': jsx_no_dollar_interpolation_js_1.default,
12
13
  'no-unused-sql-blocks': no_unused_sql_blocks_js_1.default,
14
+ 'safe-db-types': safe_db_types_js_1.default,
13
15
  };
14
16
  //# 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":";;;AAAA,2FAA8E;AAC9E,qFAAwE;AACxE,2FAA8E;AAC9E,6EAAgE;AAEnD,QAAA,KAAK,GAAG;IACnB,6BAA6B,EAAE,wCAAwB;IACvD,0BAA0B,EAAE,qCAAqB;IACjD,6BAA6B,EAAE,wCAAwB;IACvD,sBAAsB,EAAE,iCAAiB;CAC1C,CAAC","sourcesContent":["import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';\nimport awsClientSharedConfig from './rules/aws-client-shared-config.js';\nimport jsxNoDollarInterpolation from './rules/jsx-no-dollar-interpolation.js';\nimport noUnusedSqlBlocks from './rules/no-unused-sql-blocks.js';\n\nexport const rules = {\n 'aws-client-mandatory-config': awsClientMandatoryConfig,\n 'aws-client-shared-config': awsClientSharedConfig,\n 'jsx-no-dollar-interpolation': jsxNoDollarInterpolation,\n 'no-unused-sql-blocks': noUnusedSqlBlocks,\n};\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,2FAA8E;AAC9E,qFAAwE;AACxE,2FAA8E;AAC9E,6EAAgE;AAChE,+DAAmD;AAEtC,QAAA,KAAK,GAAG;IACnB,6BAA6B,EAAE,wCAAwB;IACvD,0BAA0B,EAAE,qCAAqB;IACjD,6BAA6B,EAAE,wCAAwB;IACvD,sBAAsB,EAAE,iCAAiB;IACzC,eAAe,EAAE,0BAAW;CAC7B,CAAC","sourcesContent":["import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';\nimport awsClientSharedConfig from './rules/aws-client-shared-config.js';\nimport jsxNoDollarInterpolation from './rules/jsx-no-dollar-interpolation.js';\nimport noUnusedSqlBlocks from './rules/no-unused-sql-blocks.js';\nimport safeDbTypes from './rules/safe-db-types.js';\n\nexport const rules = {\n 'aws-client-mandatory-config': awsClientMandatoryConfig,\n 'aws-client-shared-config': awsClientSharedConfig,\n 'jsx-no-dollar-interpolation': jsxNoDollarInterpolation,\n 'no-unused-sql-blocks': noUnusedSqlBlocks,\n 'safe-db-types': safeDbTypes,\n};\n"]}
@@ -0,0 +1,6 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ declare const _default: ESLintUtils.RuleModule<"unsafeTypes" | "spreadAttributes", [({
3
+ allowDbTypes?: (string | RegExp)[];
4
+ } | undefined)?], unknown, ESLintUtils.RuleListener>;
5
+ export default _default;
6
+ //# sourceMappingURL=safe-db-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-db-types.d.ts","sourceRoot":"","sources":["../../src/rules/safe-db-types.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;;mBA0enC,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE;;AADvC,wBAsGG"}
@@ -0,0 +1,491 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("@typescript-eslint/utils");
4
+ const ts = require("typescript");
5
+ const HYDRATE_FUNCTION_NAME = 'hydrateHtml';
6
+ const HYDRATE_COMPONENT_NAME = 'Hydrate';
7
+ /**
8
+ * Check if a variable declaration is a Zod schema that uses schemas from db-types.ts
9
+ * For example: const RubricDataSchema = RubricSchema.extend({...})
10
+ */
11
+ function checkZodSchemaForDbTypes(declaration, typeChecker) {
12
+ const violations = [];
13
+ if (!declaration.initializer)
14
+ return violations;
15
+ // Walk the expression tree to find all identifiers
16
+ const findIdentifiers = (node) => {
17
+ // Check property access expressions like RubricSchema.extend() or InstanceQuestionSchema.shape
18
+ if (ts.isPropertyAccessExpression(node)) {
19
+ const objectSymbol = typeChecker.getSymbolAtLocation(node.expression);
20
+ if (objectSymbol) {
21
+ const aliasedSymbol = objectSymbol.flags & ts.SymbolFlags.Alias
22
+ ? typeChecker.getAliasedSymbol(objectSymbol)
23
+ : objectSymbol;
24
+ const decls = aliasedSymbol.getDeclarations();
25
+ if (decls && decls.length > 0) {
26
+ const sourceFile = decls[0].getSourceFile();
27
+ if (sourceFile.fileName.endsWith('/db-types.ts')) {
28
+ violations.push(aliasedSymbol.getName());
29
+ }
30
+ }
31
+ }
32
+ }
33
+ // Check spread elements in object literals (e.g., ...SomeSchema.shape)
34
+ if (ts.isSpreadAssignment(node)) {
35
+ // The expression being spread (e.g., SomeSchema.shape)
36
+ const spreadExpr = node.expression;
37
+ // Check if it's a property access (e.g., accessing .shape)
38
+ if (ts.isPropertyAccessExpression(spreadExpr)) {
39
+ const objectSymbol = typeChecker.getSymbolAtLocation(spreadExpr.expression);
40
+ if (objectSymbol) {
41
+ const aliasedSymbol = objectSymbol.flags & ts.SymbolFlags.Alias
42
+ ? typeChecker.getAliasedSymbol(objectSymbol)
43
+ : objectSymbol;
44
+ const decls = aliasedSymbol.getDeclarations();
45
+ if (decls && decls.length > 0) {
46
+ const sourceFile = decls[0].getSourceFile();
47
+ if (sourceFile.fileName.endsWith('/db-types.ts')) {
48
+ violations.push(aliasedSymbol.getName());
49
+ }
50
+ else {
51
+ // The schema is defined locally, check if IT uses db-types
52
+ for (const decl of decls) {
53
+ if (ts.isVariableDeclaration(decl)) {
54
+ // Recursively check the local schema
55
+ const nestedViolations = checkZodSchemaForDbTypes(decl, typeChecker);
56
+ violations.push(...nestedViolations);
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ // Also check if the spread is a direct identifier (e.g., ...someObject)
64
+ else if (ts.isIdentifier(spreadExpr)) {
65
+ const spreadSymbol = typeChecker.getSymbolAtLocation(spreadExpr);
66
+ if (spreadSymbol) {
67
+ const aliasedSymbol = spreadSymbol.flags & ts.SymbolFlags.Alias
68
+ ? typeChecker.getAliasedSymbol(spreadSymbol)
69
+ : spreadSymbol;
70
+ const decls = aliasedSymbol.getDeclarations();
71
+ if (decls && decls.length > 0) {
72
+ const sourceFile = decls[0].getSourceFile();
73
+ if (sourceFile.fileName.endsWith('/db-types.ts')) {
74
+ violations.push(aliasedSymbol.getName());
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
80
+ // Check call expressions for arguments
81
+ if (ts.isCallExpression(node)) {
82
+ for (const arg of node.arguments) {
83
+ // Check if argument is an identifier (e.g., RubricItemSchema)
84
+ if (ts.isIdentifier(arg)) {
85
+ const argSymbol = typeChecker.getSymbolAtLocation(arg);
86
+ if (argSymbol) {
87
+ const aliasedSymbol = argSymbol.flags & ts.SymbolFlags.Alias
88
+ ? typeChecker.getAliasedSymbol(argSymbol)
89
+ : argSymbol;
90
+ const decls = aliasedSymbol.getDeclarations();
91
+ if (decls && decls.length > 0) {
92
+ const sourceFile = decls[0].getSourceFile();
93
+ if (sourceFile.fileName.endsWith('/db-types.ts')) {
94
+ violations.push(aliasedSymbol.getName());
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+ // Check object literal property assignments (e.g., { rubric: RubricSchema })
102
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.initializer)) {
103
+ const identifier = node.initializer;
104
+ const symbol = typeChecker.getSymbolAtLocation(identifier);
105
+ if (symbol) {
106
+ const aliasedSymbol = symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;
107
+ const decls = aliasedSymbol.getDeclarations();
108
+ if (decls && decls.length > 0) {
109
+ const sourceFile = decls[0].getSourceFile();
110
+ if (sourceFile.fileName.endsWith('/db-types.ts')) {
111
+ violations.push(aliasedSymbol.getName());
112
+ }
113
+ }
114
+ }
115
+ }
116
+ ts.forEachChild(node, findIdentifiers);
117
+ };
118
+ findIdentifiers(declaration.initializer);
119
+ return violations;
120
+ }
121
+ /**
122
+ * Check if a type node is z.infer<typeof SchemaName> and if SchemaName uses db-types
123
+ * Returns the names of db-types that the schema depends on
124
+ */
125
+ function checkForZodInferPattern(typeNode, typeChecker) {
126
+ // Check if this is a type reference with type arguments
127
+ if (!ts.isTypeReferenceNode(typeNode))
128
+ return [];
129
+ // Check if the type reference is named (e.g., "infer" from z.infer)
130
+ const typeName = typeNode.typeName;
131
+ if (!ts.isQualifiedName(typeName))
132
+ return [];
133
+ // Check if it's z.infer or similar pattern
134
+ // The pattern is: z.infer<typeof SchemaName>
135
+ const typeArgs = typeNode.typeArguments;
136
+ if (!typeArgs || typeArgs.length !== 1)
137
+ return [];
138
+ const typeArg = typeArgs[0];
139
+ // Check if the type argument is a typeof expression
140
+ if (!ts.isTypeQueryNode(typeArg))
141
+ return [];
142
+ // Get the schema name from typeof X
143
+ const exprName = typeArg.exprName;
144
+ if (!ts.isIdentifier(exprName))
145
+ return [];
146
+ // Now find the schema variable declaration
147
+ const schemaSymbol = typeChecker.getSymbolAtLocation(exprName);
148
+ if (!schemaSymbol)
149
+ return [];
150
+ // Check if this is an imported symbol (alias) - follow it to the original
151
+ const symbolToCheck = schemaSymbol.flags & ts.SymbolFlags.Alias
152
+ ? typeChecker.getAliasedSymbol(schemaSymbol)
153
+ : schemaSymbol;
154
+ const schemaDecls = symbolToCheck.getDeclarations();
155
+ if (!schemaDecls || schemaDecls.length === 0)
156
+ return [];
157
+ // Check if it's a variable declaration
158
+ for (const decl of schemaDecls) {
159
+ if (ts.isVariableDeclaration(decl)) {
160
+ // If the schema is defined in safe-db-types.ts, it's safe by definition
161
+ const sourceFile = decl.getSourceFile();
162
+ if (sourceFile.fileName.endsWith('/safe-db-types.ts')) {
163
+ return [];
164
+ }
165
+ // Use our existing helper to check if the schema uses db-types
166
+ return checkZodSchemaForDbTypes(decl, typeChecker);
167
+ }
168
+ }
169
+ return [];
170
+ }
171
+ function extractChild(children) {
172
+ const nonWhitespaceChildren = children.filter((child) => {
173
+ if (child.type === 'JSXText') {
174
+ return child.value.trim().length > 0;
175
+ }
176
+ return true;
177
+ });
178
+ if (nonWhitespaceChildren.length !== 1 || nonWhitespaceChildren[0].type !== 'JSXElement') {
179
+ return null;
180
+ }
181
+ return nonWhitespaceChildren[0];
182
+ }
183
+ /**
184
+ * Check if a TypeScript type node references a type from db-types.ts
185
+ * This checks the actual source code type annotation, not the resolved type.
186
+ * Follows type aliases (imports) to their original declaration.
187
+ * Returns all unsafe type names found.
188
+ */
189
+ function checkTypeNodeForDbTypes(typeNode, typeChecker, visited = new Set()) {
190
+ if (visited.has(typeNode))
191
+ return [];
192
+ visited.add(typeNode);
193
+ const violations = [];
194
+ // Check type references (e.g., User, Course, AuthnProvider)
195
+ if (ts.isTypeReferenceNode(typeNode)) {
196
+ const typeName = typeNode.typeName;
197
+ const symbol = typeChecker.getSymbolAtLocation(typeName);
198
+ if (symbol) {
199
+ // Check if this is an imported symbol (alias) - follow it to the original
200
+ const symbolToCheck = symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;
201
+ const declarations = symbolToCheck.getDeclarations();
202
+ if (declarations && declarations.length > 0) {
203
+ for (const decl of declarations) {
204
+ const sourceFile = decl.getSourceFile();
205
+ if (sourceFile.fileName.endsWith('/db-types.ts')) {
206
+ // Found a type from db-types.ts!
207
+ violations.push(symbolToCheck.getName());
208
+ }
209
+ else {
210
+ // If it's a type alias or interface defined locally, check its properties
211
+ if (ts.isTypeAliasDeclaration(decl) && decl.type) {
212
+ // Special case: Check if this is z.infer<typeof XxxSchema>
213
+ const zodSchemaViolations = checkForZodInferPattern(decl.type, typeChecker);
214
+ violations.push(...zodSchemaViolations);
215
+ // Also check the type itself
216
+ const nestedViolations = checkTypeNodeForDbTypes(decl.type, typeChecker, visited);
217
+ violations.push(...nestedViolations);
218
+ }
219
+ else if (ts.isInterfaceDeclaration(decl)) {
220
+ // Check interface members
221
+ for (const member of decl.members) {
222
+ if (ts.isPropertySignature(member) && member.type) {
223
+ const nestedViolations = checkTypeNodeForDbTypes(member.type, typeChecker, visited);
224
+ violations.push(...nestedViolations);
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ }
231
+ }
232
+ // Check type arguments (e.g., Array<User>, Promise<Course>)
233
+ if (typeNode.typeArguments) {
234
+ for (const typeArg of typeNode.typeArguments) {
235
+ const nestedViolations = checkTypeNodeForDbTypes(typeArg, typeChecker, visited);
236
+ violations.push(...nestedViolations);
237
+ }
238
+ }
239
+ }
240
+ // Check array types (e.g., User[])
241
+ if (ts.isArrayTypeNode(typeNode)) {
242
+ const nestedViolations = checkTypeNodeForDbTypes(typeNode.elementType, typeChecker, visited);
243
+ violations.push(...nestedViolations);
244
+ }
245
+ // Check union types (e.g., User | null)
246
+ if (ts.isUnionTypeNode(typeNode)) {
247
+ for (const type of typeNode.types) {
248
+ const nestedViolations = checkTypeNodeForDbTypes(type, typeChecker, visited);
249
+ violations.push(...nestedViolations);
250
+ }
251
+ }
252
+ // Check intersection types (e.g., User & { extra: string })
253
+ if (ts.isIntersectionTypeNode(typeNode)) {
254
+ for (const type of typeNode.types) {
255
+ const nestedViolations = checkTypeNodeForDbTypes(type, typeChecker, visited);
256
+ violations.push(...nestedViolations);
257
+ }
258
+ }
259
+ // Check object type literals and their properties
260
+ if (ts.isTypeLiteralNode(typeNode)) {
261
+ for (const member of typeNode.members) {
262
+ if (ts.isPropertySignature(member) && member.type) {
263
+ const nestedViolations = checkTypeNodeForDbTypes(member.type, typeChecker, visited);
264
+ violations.push(...nestedViolations);
265
+ }
266
+ }
267
+ }
268
+ // Check indexed access types (e.g., User['name'])
269
+ // These are safe! We're only extracting a specific property, not passing the whole object.
270
+ // So we DON'T recurse into the object type for indexed access.
271
+ if (ts.isIndexedAccessTypeNode(typeNode)) {
272
+ // Do not check - indexed access is safe
273
+ }
274
+ return violations;
275
+ }
276
+ /**
277
+ * Check if a type name is in the allowlist of safe types
278
+ */
279
+ function isTypeInAllowlist(typeName, allowlist) {
280
+ return allowlist.some((pattern) => {
281
+ if (typeof pattern === 'string') {
282
+ return typeName === pattern;
283
+ }
284
+ return pattern.test(typeName);
285
+ });
286
+ }
287
+ /**
288
+ * Check the props of a component for unsafe types from db-types.ts
289
+ */
290
+ function checkComponentProps(context, typeChecker, componentSymbol, tsComponentNode, jsxElement, reportNode, allowlist) {
291
+ const childOpeningElement = jsxElement.openingElement;
292
+ // Get the component's type (function or class component)
293
+ const componentType = typeChecker.getTypeOfSymbolAtLocation(componentSymbol, tsComponentNode);
294
+ const signatures = componentType.getCallSignatures();
295
+ if (signatures.length === 0)
296
+ return;
297
+ // Get the first parameter (props) of the component function
298
+ const propsParam = signatures[0].getParameters()[0];
299
+ if (!propsParam)
300
+ return;
301
+ const propsDeclaration = propsParam.valueDeclaration;
302
+ if (!propsDeclaration || !ts.isParameter(propsDeclaration))
303
+ return;
304
+ // Get the type annotation node from the props parameter
305
+ const propsTypeNode = propsDeclaration.type;
306
+ if (!propsTypeNode)
307
+ return;
308
+ // Check each property in the props type
309
+ if (ts.isTypeLiteralNode(propsTypeNode)) {
310
+ // Inline props object: { foo: string; bar: number }
311
+ for (const member of propsTypeNode.members) {
312
+ if (ts.isPropertySignature(member) && member.type && member.name) {
313
+ if (!ts.isIdentifier(member.name))
314
+ continue;
315
+ const propName = member.name.text;
316
+ const violations = checkTypeNodeForDbTypes(member.type, typeChecker);
317
+ if (violations.length > 0) {
318
+ // Find the JSX attribute for this prop
319
+ const attribute = childOpeningElement.attributes.find((attr) => attr.type === 'JSXAttribute' &&
320
+ attr.name.type === 'JSXIdentifier' &&
321
+ attr.name.name === propName);
322
+ for (const typeName of violations) {
323
+ if (isTypeInAllowlist(typeName, allowlist))
324
+ continue;
325
+ context.report({
326
+ node: attribute || reportNode,
327
+ messageId: 'unsafeTypes',
328
+ data: { propName, typeName },
329
+ });
330
+ }
331
+ }
332
+ }
333
+ }
334
+ }
335
+ else if (ts.isTypeReferenceNode(propsTypeNode)) {
336
+ // Props is a type reference (e.g., interface or type alias)
337
+ const symbol = typeChecker.getSymbolAtLocation(propsTypeNode.typeName);
338
+ if (symbol) {
339
+ const resolvedSymbol = symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;
340
+ const declarations = resolvedSymbol.getDeclarations();
341
+ if (declarations && declarations.length > 0) {
342
+ for (const decl of declarations) {
343
+ if (ts.isInterfaceDeclaration(decl) || ts.isTypeLiteralNode(decl)) {
344
+ const members = ts.isInterfaceDeclaration(decl) ? decl.members : decl.members;
345
+ for (const member of members) {
346
+ if (ts.isPropertySignature(member) && member.type && member.name) {
347
+ if (!ts.isIdentifier(member.name))
348
+ continue;
349
+ const propName = member.name.text;
350
+ const violations = checkTypeNodeForDbTypes(member.type, typeChecker);
351
+ if (violations.length > 0) {
352
+ // Find the JSX attribute for this prop
353
+ const attribute = childOpeningElement.attributes.find((attr) => attr.type === 'JSXAttribute' &&
354
+ attr.name.type === 'JSXIdentifier' &&
355
+ attr.name.name === propName);
356
+ for (const typeName of violations) {
357
+ if (isTypeInAllowlist(typeName, allowlist))
358
+ continue;
359
+ context.report({
360
+ node: attribute || reportNode,
361
+ messageId: 'unsafeTypes',
362
+ data: { propName, typeName },
363
+ });
364
+ }
365
+ }
366
+ }
367
+ }
368
+ }
369
+ else if (ts.isTypeAliasDeclaration(decl) && decl.type) {
370
+ // Type alias might be an inline object type
371
+ if (ts.isTypeLiteralNode(decl.type)) {
372
+ for (const member of decl.type.members) {
373
+ if (ts.isPropertySignature(member) && member.type && member.name) {
374
+ if (!ts.isIdentifier(member.name))
375
+ continue;
376
+ const propName = member.name.text;
377
+ const violations = checkTypeNodeForDbTypes(member.type, typeChecker);
378
+ if (violations.length > 0) {
379
+ // Find the JSX attribute for this prop
380
+ const attribute = childOpeningElement.attributes.find((attr) => attr.type === 'JSXAttribute' &&
381
+ attr.name.type === 'JSXIdentifier' &&
382
+ attr.name.name === propName);
383
+ for (const typeName of violations) {
384
+ if (isTypeInAllowlist(typeName, allowlist))
385
+ continue;
386
+ context.report({
387
+ node: attribute || reportNode,
388
+ messageId: 'unsafeTypes',
389
+ data: { propName, typeName },
390
+ });
391
+ }
392
+ }
393
+ }
394
+ }
395
+ }
396
+ }
397
+ }
398
+ }
399
+ }
400
+ }
401
+ // Check for spread attributes
402
+ const attributes = childOpeningElement.attributes;
403
+ for (const attr of attributes) {
404
+ if (attr.type === 'JSXSpreadAttribute') {
405
+ context.report({
406
+ node: reportNode,
407
+ messageId: 'spreadAttributes',
408
+ });
409
+ continue;
410
+ }
411
+ }
412
+ }
413
+ exports.default = utils_1.ESLintUtils.RuleCreator.withoutDocs({
414
+ meta: {
415
+ type: 'problem',
416
+ messages: {
417
+ spreadAttributes: 'Spread attributes are not allowed in Hydrate children.',
418
+ unsafeTypes: 'Prop "{{propName}}" uses type "{{typeName}}" which is derived from db-types.ts. Use safe-db-types.ts instead.',
419
+ },
420
+ schema: [
421
+ {
422
+ type: 'object',
423
+ properties: {
424
+ allowDbTypes: {
425
+ type: 'array',
426
+ items: {
427
+ type: 'string',
428
+ },
429
+ },
430
+ },
431
+ additionalProperties: false,
432
+ },
433
+ ],
434
+ },
435
+ defaultOptions: [{}],
436
+ create(context) {
437
+ const options = context.options[0] || {};
438
+ const allowlist = options.allowDbTypes || [];
439
+ return {
440
+ JSXElement(node) {
441
+ const openingElementNameExpression = node.openingElement.name;
442
+ if (openingElementNameExpression.type !== 'JSXIdentifier')
443
+ return;
444
+ const elementName = openingElementNameExpression.name;
445
+ if (elementName !== HYDRATE_COMPONENT_NAME)
446
+ return;
447
+ const child = extractChild(node.children);
448
+ if (!child)
449
+ return;
450
+ // Get the component being rendered
451
+ const childOpeningElement = child.openingElement;
452
+ const childElementName = childOpeningElement.name;
453
+ if (childElementName.type !== 'JSXIdentifier')
454
+ return;
455
+ // Get the component's type to inspect its props
456
+ const services = utils_1.ESLintUtils.getParserServices(context);
457
+ const typeChecker = services.program.getTypeChecker();
458
+ const tsChildNode = services.esTreeNodeToTSNodeMap.get(childElementName);
459
+ const componentSymbol = typeChecker.getSymbolAtLocation(tsChildNode);
460
+ if (!componentSymbol)
461
+ return;
462
+ checkComponentProps(context, typeChecker, componentSymbol, tsChildNode, child, child, allowlist);
463
+ },
464
+ CallExpression(node) {
465
+ // Check for hydrateHtml(<Component ... />, props?) calls
466
+ if (node.callee.type !== 'Identifier' || node.callee.name !== HYDRATE_FUNCTION_NAME)
467
+ return;
468
+ // Should have at least one argument, the first is JSX element.
469
+ if (node.arguments.length === 0)
470
+ return;
471
+ const arg = node.arguments[0];
472
+ if (arg.type !== 'JSXElement')
473
+ return;
474
+ const jsxElement = arg;
475
+ const openingElement = jsxElement.openingElement;
476
+ const elementName = openingElement.name;
477
+ if (elementName.type !== 'JSXIdentifier')
478
+ return;
479
+ // Get the component's type to inspect its props
480
+ const services = utils_1.ESLintUtils.getParserServices(context);
481
+ const typeChecker = services.program.getTypeChecker();
482
+ const tsElementNode = services.esTreeNodeToTSNodeMap.get(elementName);
483
+ const componentSymbol = typeChecker.getSymbolAtLocation(tsElementNode);
484
+ if (!componentSymbol)
485
+ return;
486
+ checkComponentProps(context, typeChecker, componentSymbol, tsElementNode, jsxElement, node, allowlist);
487
+ },
488
+ };
489
+ },
490
+ });
491
+ //# sourceMappingURL=safe-db-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-db-types.js","sourceRoot":"","sources":["../../src/rules/safe-db-types.ts"],"names":[],"mappings":"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","sourcesContent":["import { type TSESTree } from '@typescript-eslint/types';\nimport { ESLintUtils } from '@typescript-eslint/utils';\nimport * as ts from 'typescript';\n\nconst HYDRATE_FUNCTION_NAME = 'hydrateHtml';\nconst HYDRATE_COMPONENT_NAME = 'Hydrate';\n\n/**\n * Check if a variable declaration is a Zod schema that uses schemas from db-types.ts\n * For example: const RubricDataSchema = RubricSchema.extend({...})\n */\nfunction checkZodSchemaForDbTypes(\n declaration: ts.VariableDeclaration,\n typeChecker: ts.TypeChecker,\n): string[] {\n const violations: string[] = [];\n\n if (!declaration.initializer) return violations;\n\n // Walk the expression tree to find all identifiers\n const findIdentifiers = (node: ts.Node): void => {\n // Check property access expressions like RubricSchema.extend() or InstanceQuestionSchema.shape\n if (ts.isPropertyAccessExpression(node)) {\n const objectSymbol = typeChecker.getSymbolAtLocation(node.expression);\n if (objectSymbol) {\n const aliasedSymbol =\n objectSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(objectSymbol)\n : objectSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n\n // Check spread elements in object literals (e.g., ...SomeSchema.shape)\n if (ts.isSpreadAssignment(node)) {\n // The expression being spread (e.g., SomeSchema.shape)\n const spreadExpr = node.expression;\n\n // Check if it's a property access (e.g., accessing .shape)\n if (ts.isPropertyAccessExpression(spreadExpr)) {\n const objectSymbol = typeChecker.getSymbolAtLocation(spreadExpr.expression);\n if (objectSymbol) {\n const aliasedSymbol =\n objectSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(objectSymbol)\n : objectSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n } else {\n // The schema is defined locally, check if IT uses db-types\n for (const decl of decls) {\n if (ts.isVariableDeclaration(decl)) {\n // Recursively check the local schema\n const nestedViolations = checkZodSchemaForDbTypes(decl, typeChecker);\n violations.push(...nestedViolations);\n }\n }\n }\n }\n }\n }\n // Also check if the spread is a direct identifier (e.g., ...someObject)\n else if (ts.isIdentifier(spreadExpr)) {\n const spreadSymbol = typeChecker.getSymbolAtLocation(spreadExpr);\n if (spreadSymbol) {\n const aliasedSymbol =\n spreadSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(spreadSymbol)\n : spreadSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n }\n\n // Check call expressions for arguments\n if (ts.isCallExpression(node)) {\n for (const arg of node.arguments) {\n // Check if argument is an identifier (e.g., RubricItemSchema)\n if (ts.isIdentifier(arg)) {\n const argSymbol = typeChecker.getSymbolAtLocation(arg);\n if (argSymbol) {\n const aliasedSymbol =\n argSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(argSymbol)\n : argSymbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n }\n }\n\n // Check object literal property assignments (e.g., { rubric: RubricSchema })\n if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.initializer)) {\n const identifier = node.initializer;\n const symbol = typeChecker.getSymbolAtLocation(identifier);\n if (symbol) {\n const aliasedSymbol =\n symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;\n const decls = aliasedSymbol.getDeclarations();\n if (decls && decls.length > 0) {\n const sourceFile = decls[0].getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n violations.push(aliasedSymbol.getName());\n }\n }\n }\n }\n\n ts.forEachChild(node, findIdentifiers);\n };\n\n findIdentifiers(declaration.initializer);\n return violations;\n}\n\n/**\n * Check if a type node is z.infer<typeof SchemaName> and if SchemaName uses db-types\n * Returns the names of db-types that the schema depends on\n */\nfunction checkForZodInferPattern(typeNode: ts.TypeNode, typeChecker: ts.TypeChecker): string[] {\n // Check if this is a type reference with type arguments\n if (!ts.isTypeReferenceNode(typeNode)) return [];\n\n // Check if the type reference is named (e.g., \"infer\" from z.infer)\n const typeName = typeNode.typeName;\n if (!ts.isQualifiedName(typeName)) return [];\n\n // Check if it's z.infer or similar pattern\n // The pattern is: z.infer<typeof SchemaName>\n const typeArgs = typeNode.typeArguments;\n if (!typeArgs || typeArgs.length !== 1) return [];\n\n const typeArg = typeArgs[0];\n\n // Check if the type argument is a typeof expression\n if (!ts.isTypeQueryNode(typeArg)) return [];\n\n // Get the schema name from typeof X\n const exprName = typeArg.exprName;\n if (!ts.isIdentifier(exprName)) return [];\n\n // Now find the schema variable declaration\n const schemaSymbol = typeChecker.getSymbolAtLocation(exprName);\n if (!schemaSymbol) return [];\n\n // Check if this is an imported symbol (alias) - follow it to the original\n const symbolToCheck =\n schemaSymbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(schemaSymbol)\n : schemaSymbol;\n\n const schemaDecls = symbolToCheck.getDeclarations();\n if (!schemaDecls || schemaDecls.length === 0) return [];\n\n // Check if it's a variable declaration\n for (const decl of schemaDecls) {\n if (ts.isVariableDeclaration(decl)) {\n // If the schema is defined in safe-db-types.ts, it's safe by definition\n const sourceFile = decl.getSourceFile();\n if (sourceFile.fileName.endsWith('/safe-db-types.ts')) {\n return [];\n }\n\n // Use our existing helper to check if the schema uses db-types\n return checkZodSchemaForDbTypes(decl, typeChecker);\n }\n }\n\n return [];\n}\n\nfunction extractChild(children: TSESTree.JSXChild[]): TSESTree.JSXElement | null {\n const nonWhitespaceChildren = children.filter((child) => {\n if (child.type === 'JSXText') {\n return child.value.trim().length > 0;\n }\n return true;\n });\n\n if (nonWhitespaceChildren.length !== 1 || nonWhitespaceChildren[0].type !== 'JSXElement') {\n return null;\n }\n\n return nonWhitespaceChildren[0];\n}\n\n/**\n * Check if a TypeScript type node references a type from db-types.ts\n * This checks the actual source code type annotation, not the resolved type.\n * Follows type aliases (imports) to their original declaration.\n * Returns all unsafe type names found.\n */\nfunction checkTypeNodeForDbTypes(\n typeNode: ts.TypeNode,\n typeChecker: ts.TypeChecker,\n visited = new Set<ts.TypeNode>(),\n): string[] {\n if (visited.has(typeNode)) return [];\n visited.add(typeNode);\n\n const violations: string[] = [];\n\n // Check type references (e.g., User, Course, AuthnProvider)\n if (ts.isTypeReferenceNode(typeNode)) {\n const typeName = typeNode.typeName;\n const symbol = typeChecker.getSymbolAtLocation(typeName);\n\n if (symbol) {\n // Check if this is an imported symbol (alias) - follow it to the original\n const symbolToCheck =\n symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;\n\n const declarations = symbolToCheck.getDeclarations();\n if (declarations && declarations.length > 0) {\n for (const decl of declarations) {\n const sourceFile = decl.getSourceFile();\n if (sourceFile.fileName.endsWith('/db-types.ts')) {\n // Found a type from db-types.ts!\n violations.push(symbolToCheck.getName());\n } else {\n // If it's a type alias or interface defined locally, check its properties\n if (ts.isTypeAliasDeclaration(decl) && decl.type) {\n // Special case: Check if this is z.infer<typeof XxxSchema>\n const zodSchemaViolations = checkForZodInferPattern(decl.type, typeChecker);\n violations.push(...zodSchemaViolations);\n\n // Also check the type itself\n const nestedViolations = checkTypeNodeForDbTypes(decl.type, typeChecker, visited);\n violations.push(...nestedViolations);\n } else if (ts.isInterfaceDeclaration(decl)) {\n // Check interface members\n for (const member of decl.members) {\n if (ts.isPropertySignature(member) && member.type) {\n const nestedViolations = checkTypeNodeForDbTypes(\n member.type,\n typeChecker,\n visited,\n );\n violations.push(...nestedViolations);\n }\n }\n }\n }\n }\n }\n }\n\n // Check type arguments (e.g., Array<User>, Promise<Course>)\n if (typeNode.typeArguments) {\n for (const typeArg of typeNode.typeArguments) {\n const nestedViolations = checkTypeNodeForDbTypes(typeArg, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n }\n\n // Check array types (e.g., User[])\n if (ts.isArrayTypeNode(typeNode)) {\n const nestedViolations = checkTypeNodeForDbTypes(typeNode.elementType, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n\n // Check union types (e.g., User | null)\n if (ts.isUnionTypeNode(typeNode)) {\n for (const type of typeNode.types) {\n const nestedViolations = checkTypeNodeForDbTypes(type, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n\n // Check intersection types (e.g., User & { extra: string })\n if (ts.isIntersectionTypeNode(typeNode)) {\n for (const type of typeNode.types) {\n const nestedViolations = checkTypeNodeForDbTypes(type, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n\n // Check object type literals and their properties\n if (ts.isTypeLiteralNode(typeNode)) {\n for (const member of typeNode.members) {\n if (ts.isPropertySignature(member) && member.type) {\n const nestedViolations = checkTypeNodeForDbTypes(member.type, typeChecker, visited);\n violations.push(...nestedViolations);\n }\n }\n }\n\n // Check indexed access types (e.g., User['name'])\n // These are safe! We're only extracting a specific property, not passing the whole object.\n // So we DON'T recurse into the object type for indexed access.\n if (ts.isIndexedAccessTypeNode(typeNode)) {\n // Do not check - indexed access is safe\n }\n\n return violations;\n}\n\n/**\n * Check if a type name is in the allowlist of safe types\n */\nfunction isTypeInAllowlist(typeName: string, allowlist: (string | RegExp)[]): boolean {\n return allowlist.some((pattern) => {\n if (typeof pattern === 'string') {\n return typeName === pattern;\n }\n return pattern.test(typeName);\n });\n}\n\n/**\n * Check the props of a component for unsafe types from db-types.ts\n */\nfunction checkComponentProps(\n context: ReturnType<typeof ESLintUtils.RuleCreator.withoutDocs>['create'] extends (\n context: infer C,\n ) => any\n ? C\n : never,\n typeChecker: ts.TypeChecker,\n componentSymbol: ts.Symbol,\n tsComponentNode: ts.Node,\n jsxElement: TSESTree.JSXElement,\n reportNode: TSESTree.Node,\n allowlist: (string | RegExp)[],\n): void {\n const childOpeningElement = jsxElement.openingElement;\n\n // Get the component's type (function or class component)\n const componentType = typeChecker.getTypeOfSymbolAtLocation(componentSymbol, tsComponentNode);\n const signatures = componentType.getCallSignatures();\n\n if (signatures.length === 0) return;\n\n // Get the first parameter (props) of the component function\n const propsParam = signatures[0].getParameters()[0];\n if (!propsParam) return;\n\n const propsDeclaration = propsParam.valueDeclaration;\n\n if (!propsDeclaration || !ts.isParameter(propsDeclaration)) return;\n\n // Get the type annotation node from the props parameter\n const propsTypeNode = propsDeclaration.type;\n if (!propsTypeNode) return;\n\n // Check each property in the props type\n if (ts.isTypeLiteralNode(propsTypeNode)) {\n // Inline props object: { foo: string; bar: number }\n for (const member of propsTypeNode.members) {\n if (ts.isPropertySignature(member) && member.type && member.name) {\n if (!ts.isIdentifier(member.name)) continue;\n\n const propName = member.name.text;\n const violations = checkTypeNodeForDbTypes(member.type, typeChecker);\n\n if (violations.length > 0) {\n // Find the JSX attribute for this prop\n const attribute = childOpeningElement.attributes.find(\n (attr) =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === propName,\n );\n\n for (const typeName of violations) {\n if (isTypeInAllowlist(typeName, allowlist)) continue;\n\n context.report({\n node: attribute || reportNode,\n messageId: 'unsafeTypes',\n data: { propName, typeName },\n });\n }\n }\n }\n }\n } else if (ts.isTypeReferenceNode(propsTypeNode)) {\n // Props is a type reference (e.g., interface or type alias)\n const symbol = typeChecker.getSymbolAtLocation(propsTypeNode.typeName);\n if (symbol) {\n const resolvedSymbol =\n symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;\n const declarations = resolvedSymbol.getDeclarations();\n\n if (declarations && declarations.length > 0) {\n for (const decl of declarations) {\n if (ts.isInterfaceDeclaration(decl) || ts.isTypeLiteralNode(decl)) {\n const members = ts.isInterfaceDeclaration(decl) ? decl.members : decl.members;\n\n for (const member of members) {\n if (ts.isPropertySignature(member) && member.type && member.name) {\n if (!ts.isIdentifier(member.name)) continue;\n\n const propName = member.name.text;\n const violations = checkTypeNodeForDbTypes(member.type, typeChecker);\n\n if (violations.length > 0) {\n // Find the JSX attribute for this prop\n const attribute = childOpeningElement.attributes.find(\n (attr) =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === propName,\n );\n\n for (const typeName of violations) {\n if (isTypeInAllowlist(typeName, allowlist)) continue;\n\n context.report({\n node: attribute || reportNode,\n messageId: 'unsafeTypes',\n data: { propName, typeName },\n });\n }\n }\n }\n }\n } else if (ts.isTypeAliasDeclaration(decl) && decl.type) {\n // Type alias might be an inline object type\n if (ts.isTypeLiteralNode(decl.type)) {\n for (const member of decl.type.members) {\n if (ts.isPropertySignature(member) && member.type && member.name) {\n if (!ts.isIdentifier(member.name)) continue;\n\n const propName = member.name.text;\n const violations = checkTypeNodeForDbTypes(member.type, typeChecker);\n\n if (violations.length > 0) {\n // Find the JSX attribute for this prop\n const attribute = childOpeningElement.attributes.find(\n (attr) =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === propName,\n );\n\n for (const typeName of violations) {\n if (isTypeInAllowlist(typeName, allowlist)) continue;\n\n context.report({\n node: attribute || reportNode,\n messageId: 'unsafeTypes',\n data: { propName, typeName },\n });\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n\n // Check for spread attributes\n const attributes = childOpeningElement.attributes;\n for (const attr of attributes) {\n if (attr.type === 'JSXSpreadAttribute') {\n context.report({\n node: reportNode,\n messageId: 'spreadAttributes',\n });\n continue;\n }\n }\n}\n\nexport default ESLintUtils.RuleCreator.withoutDocs<\n [{ allowDbTypes?: (string | RegExp)[] }?],\n 'spreadAttributes' | 'unsafeTypes'\n>({\n meta: {\n type: 'problem',\n messages: {\n spreadAttributes: 'Spread attributes are not allowed in Hydrate children.',\n unsafeTypes:\n 'Prop \"{{propName}}\" uses type \"{{typeName}}\" which is derived from db-types.ts. Use safe-db-types.ts instead.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n allowDbTypes: {\n type: 'array',\n items: {\n type: 'string',\n },\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{}],\n create(context) {\n const options = context.options[0] || {};\n const allowlist = options.allowDbTypes || [];\n return {\n JSXElement(node) {\n const openingElementNameExpression = node.openingElement.name;\n if (openingElementNameExpression.type !== 'JSXIdentifier') return;\n\n const elementName = openingElementNameExpression.name;\n if (elementName !== HYDRATE_COMPONENT_NAME) return;\n\n const child = extractChild(node.children);\n if (!child) return;\n\n // Get the component being rendered\n const childOpeningElement = child.openingElement;\n const childElementName = childOpeningElement.name;\n\n if (childElementName.type !== 'JSXIdentifier') return;\n\n // Get the component's type to inspect its props\n const services = ESLintUtils.getParserServices(context);\n const typeChecker = services.program.getTypeChecker();\n const tsChildNode = services.esTreeNodeToTSNodeMap.get(childElementName);\n const componentSymbol = typeChecker.getSymbolAtLocation(tsChildNode);\n\n if (!componentSymbol) return;\n\n checkComponentProps(\n context,\n typeChecker,\n componentSymbol,\n tsChildNode,\n child,\n child,\n allowlist,\n );\n },\n\n CallExpression(node) {\n // Check for hydrateHtml(<Component ... />, props?) calls\n if (node.callee.type !== 'Identifier' || node.callee.name !== HYDRATE_FUNCTION_NAME) return;\n\n // Should have at least one argument, the first is JSX element.\n if (node.arguments.length === 0) return;\n\n const arg = node.arguments[0];\n if (arg.type !== 'JSXElement') return;\n\n const jsxElement = arg;\n const openingElement = jsxElement.openingElement;\n const elementName = openingElement.name;\n\n if (elementName.type !== 'JSXIdentifier') return;\n\n // Get the component's type to inspect its props\n const services = ESLintUtils.getParserServices(context);\n const typeChecker = services.program.getTypeChecker();\n const tsElementNode = services.esTreeNodeToTSNodeMap.get(elementName);\n const componentSymbol = typeChecker.getSymbolAtLocation(tsElementNode);\n\n if (!componentSymbol) return;\n\n checkComponentProps(\n context,\n typeChecker,\n componentSymbol,\n tsElementNode,\n jsxElement,\n node,\n allowlist,\n );\n },\n };\n },\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prairielearn/eslint-plugin",
3
- "version": "2.2.7",
3
+ "version": "2.3.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/PrairieLearn/PrairieLearn.git",
@@ -13,15 +13,16 @@
13
13
  "test": "vitest run --coverage"
14
14
  },
15
15
  "dependencies": {
16
- "@typescript-eslint/utils": "^8.46.2"
16
+ "@typescript-eslint/utils": "^8.51.0",
17
+ "typescript": "^5.9.3"
17
18
  },
18
19
  "devDependencies": {
19
20
  "@prairielearn/tsconfig": "^0.0.0",
20
- "@types/node": "^22.18.13",
21
- "@typescript-eslint/rule-tester": "^8.46.2",
22
- "@vitest/coverage-v8": "^4.0.6",
23
- "tsx": "^4.20.6",
24
- "typescript": "^5.9.3",
25
- "vitest": "^4.0.6"
21
+ "@types/node": "^22.19.3",
22
+ "@typescript-eslint/rule-tester": "^8.51.0",
23
+ "@typescript-eslint/types": "^8.51.0",
24
+ "@vitest/coverage-v8": "^4.0.16",
25
+ "tsx": "^4.21.0",
26
+ "vitest": "^4.0.16"
26
27
  }
27
28
  }
package/src/index.ts CHANGED
@@ -2,10 +2,12 @@ import awsClientMandatoryConfig from './rules/aws-client-mandatory-config.js';
2
2
  import awsClientSharedConfig from './rules/aws-client-shared-config.js';
3
3
  import jsxNoDollarInterpolation from './rules/jsx-no-dollar-interpolation.js';
4
4
  import noUnusedSqlBlocks from './rules/no-unused-sql-blocks.js';
5
+ import safeDbTypes from './rules/safe-db-types.js';
5
6
 
6
7
  export const rules = {
7
8
  'aws-client-mandatory-config': awsClientMandatoryConfig,
8
9
  'aws-client-shared-config': awsClientSharedConfig,
9
10
  'jsx-no-dollar-interpolation': jsxNoDollarInterpolation,
10
11
  'no-unused-sql-blocks': noUnusedSqlBlocks,
12
+ 'safe-db-types': safeDbTypes,
11
13
  };
@@ -0,0 +1,593 @@
1
+ import { type TSESTree } from '@typescript-eslint/types';
2
+ import { ESLintUtils } from '@typescript-eslint/utils';
3
+ import * as ts from 'typescript';
4
+
5
+ const HYDRATE_FUNCTION_NAME = 'hydrateHtml';
6
+ const HYDRATE_COMPONENT_NAME = 'Hydrate';
7
+
8
+ /**
9
+ * Check if a variable declaration is a Zod schema that uses schemas from db-types.ts
10
+ * For example: const RubricDataSchema = RubricSchema.extend({...})
11
+ */
12
+ function checkZodSchemaForDbTypes(
13
+ declaration: ts.VariableDeclaration,
14
+ typeChecker: ts.TypeChecker,
15
+ ): string[] {
16
+ const violations: string[] = [];
17
+
18
+ if (!declaration.initializer) return violations;
19
+
20
+ // Walk the expression tree to find all identifiers
21
+ const findIdentifiers = (node: ts.Node): void => {
22
+ // Check property access expressions like RubricSchema.extend() or InstanceQuestionSchema.shape
23
+ if (ts.isPropertyAccessExpression(node)) {
24
+ const objectSymbol = typeChecker.getSymbolAtLocation(node.expression);
25
+ if (objectSymbol) {
26
+ const aliasedSymbol =
27
+ objectSymbol.flags & ts.SymbolFlags.Alias
28
+ ? typeChecker.getAliasedSymbol(objectSymbol)
29
+ : objectSymbol;
30
+ const decls = aliasedSymbol.getDeclarations();
31
+ if (decls && decls.length > 0) {
32
+ const sourceFile = decls[0].getSourceFile();
33
+ if (sourceFile.fileName.endsWith('/db-types.ts')) {
34
+ violations.push(aliasedSymbol.getName());
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ // Check spread elements in object literals (e.g., ...SomeSchema.shape)
41
+ if (ts.isSpreadAssignment(node)) {
42
+ // The expression being spread (e.g., SomeSchema.shape)
43
+ const spreadExpr = node.expression;
44
+
45
+ // Check if it's a property access (e.g., accessing .shape)
46
+ if (ts.isPropertyAccessExpression(spreadExpr)) {
47
+ const objectSymbol = typeChecker.getSymbolAtLocation(spreadExpr.expression);
48
+ if (objectSymbol) {
49
+ const aliasedSymbol =
50
+ objectSymbol.flags & ts.SymbolFlags.Alias
51
+ ? typeChecker.getAliasedSymbol(objectSymbol)
52
+ : objectSymbol;
53
+ const decls = aliasedSymbol.getDeclarations();
54
+ if (decls && decls.length > 0) {
55
+ const sourceFile = decls[0].getSourceFile();
56
+ if (sourceFile.fileName.endsWith('/db-types.ts')) {
57
+ violations.push(aliasedSymbol.getName());
58
+ } else {
59
+ // The schema is defined locally, check if IT uses db-types
60
+ for (const decl of decls) {
61
+ if (ts.isVariableDeclaration(decl)) {
62
+ // Recursively check the local schema
63
+ const nestedViolations = checkZodSchemaForDbTypes(decl, typeChecker);
64
+ violations.push(...nestedViolations);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ // Also check if the spread is a direct identifier (e.g., ...someObject)
72
+ else if (ts.isIdentifier(spreadExpr)) {
73
+ const spreadSymbol = typeChecker.getSymbolAtLocation(spreadExpr);
74
+ if (spreadSymbol) {
75
+ const aliasedSymbol =
76
+ spreadSymbol.flags & ts.SymbolFlags.Alias
77
+ ? typeChecker.getAliasedSymbol(spreadSymbol)
78
+ : spreadSymbol;
79
+ const decls = aliasedSymbol.getDeclarations();
80
+ if (decls && decls.length > 0) {
81
+ const sourceFile = decls[0].getSourceFile();
82
+ if (sourceFile.fileName.endsWith('/db-types.ts')) {
83
+ violations.push(aliasedSymbol.getName());
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ // Check call expressions for arguments
91
+ if (ts.isCallExpression(node)) {
92
+ for (const arg of node.arguments) {
93
+ // Check if argument is an identifier (e.g., RubricItemSchema)
94
+ if (ts.isIdentifier(arg)) {
95
+ const argSymbol = typeChecker.getSymbolAtLocation(arg);
96
+ if (argSymbol) {
97
+ const aliasedSymbol =
98
+ argSymbol.flags & ts.SymbolFlags.Alias
99
+ ? typeChecker.getAliasedSymbol(argSymbol)
100
+ : argSymbol;
101
+ const decls = aliasedSymbol.getDeclarations();
102
+ if (decls && decls.length > 0) {
103
+ const sourceFile = decls[0].getSourceFile();
104
+ if (sourceFile.fileName.endsWith('/db-types.ts')) {
105
+ violations.push(aliasedSymbol.getName());
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ // Check object literal property assignments (e.g., { rubric: RubricSchema })
114
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.initializer)) {
115
+ const identifier = node.initializer;
116
+ const symbol = typeChecker.getSymbolAtLocation(identifier);
117
+ if (symbol) {
118
+ const aliasedSymbol =
119
+ symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;
120
+ const decls = aliasedSymbol.getDeclarations();
121
+ if (decls && decls.length > 0) {
122
+ const sourceFile = decls[0].getSourceFile();
123
+ if (sourceFile.fileName.endsWith('/db-types.ts')) {
124
+ violations.push(aliasedSymbol.getName());
125
+ }
126
+ }
127
+ }
128
+ }
129
+
130
+ ts.forEachChild(node, findIdentifiers);
131
+ };
132
+
133
+ findIdentifiers(declaration.initializer);
134
+ return violations;
135
+ }
136
+
137
+ /**
138
+ * Check if a type node is z.infer<typeof SchemaName> and if SchemaName uses db-types
139
+ * Returns the names of db-types that the schema depends on
140
+ */
141
+ function checkForZodInferPattern(typeNode: ts.TypeNode, typeChecker: ts.TypeChecker): string[] {
142
+ // Check if this is a type reference with type arguments
143
+ if (!ts.isTypeReferenceNode(typeNode)) return [];
144
+
145
+ // Check if the type reference is named (e.g., "infer" from z.infer)
146
+ const typeName = typeNode.typeName;
147
+ if (!ts.isQualifiedName(typeName)) return [];
148
+
149
+ // Check if it's z.infer or similar pattern
150
+ // The pattern is: z.infer<typeof SchemaName>
151
+ const typeArgs = typeNode.typeArguments;
152
+ if (!typeArgs || typeArgs.length !== 1) return [];
153
+
154
+ const typeArg = typeArgs[0];
155
+
156
+ // Check if the type argument is a typeof expression
157
+ if (!ts.isTypeQueryNode(typeArg)) return [];
158
+
159
+ // Get the schema name from typeof X
160
+ const exprName = typeArg.exprName;
161
+ if (!ts.isIdentifier(exprName)) return [];
162
+
163
+ // Now find the schema variable declaration
164
+ const schemaSymbol = typeChecker.getSymbolAtLocation(exprName);
165
+ if (!schemaSymbol) return [];
166
+
167
+ // Check if this is an imported symbol (alias) - follow it to the original
168
+ const symbolToCheck =
169
+ schemaSymbol.flags & ts.SymbolFlags.Alias
170
+ ? typeChecker.getAliasedSymbol(schemaSymbol)
171
+ : schemaSymbol;
172
+
173
+ const schemaDecls = symbolToCheck.getDeclarations();
174
+ if (!schemaDecls || schemaDecls.length === 0) return [];
175
+
176
+ // Check if it's a variable declaration
177
+ for (const decl of schemaDecls) {
178
+ if (ts.isVariableDeclaration(decl)) {
179
+ // If the schema is defined in safe-db-types.ts, it's safe by definition
180
+ const sourceFile = decl.getSourceFile();
181
+ if (sourceFile.fileName.endsWith('/safe-db-types.ts')) {
182
+ return [];
183
+ }
184
+
185
+ // Use our existing helper to check if the schema uses db-types
186
+ return checkZodSchemaForDbTypes(decl, typeChecker);
187
+ }
188
+ }
189
+
190
+ return [];
191
+ }
192
+
193
+ function extractChild(children: TSESTree.JSXChild[]): TSESTree.JSXElement | null {
194
+ const nonWhitespaceChildren = children.filter((child) => {
195
+ if (child.type === 'JSXText') {
196
+ return child.value.trim().length > 0;
197
+ }
198
+ return true;
199
+ });
200
+
201
+ if (nonWhitespaceChildren.length !== 1 || nonWhitespaceChildren[0].type !== 'JSXElement') {
202
+ return null;
203
+ }
204
+
205
+ return nonWhitespaceChildren[0];
206
+ }
207
+
208
+ /**
209
+ * Check if a TypeScript type node references a type from db-types.ts
210
+ * This checks the actual source code type annotation, not the resolved type.
211
+ * Follows type aliases (imports) to their original declaration.
212
+ * Returns all unsafe type names found.
213
+ */
214
+ function checkTypeNodeForDbTypes(
215
+ typeNode: ts.TypeNode,
216
+ typeChecker: ts.TypeChecker,
217
+ visited = new Set<ts.TypeNode>(),
218
+ ): string[] {
219
+ if (visited.has(typeNode)) return [];
220
+ visited.add(typeNode);
221
+
222
+ const violations: string[] = [];
223
+
224
+ // Check type references (e.g., User, Course, AuthnProvider)
225
+ if (ts.isTypeReferenceNode(typeNode)) {
226
+ const typeName = typeNode.typeName;
227
+ const symbol = typeChecker.getSymbolAtLocation(typeName);
228
+
229
+ if (symbol) {
230
+ // Check if this is an imported symbol (alias) - follow it to the original
231
+ const symbolToCheck =
232
+ symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;
233
+
234
+ const declarations = symbolToCheck.getDeclarations();
235
+ if (declarations && declarations.length > 0) {
236
+ for (const decl of declarations) {
237
+ const sourceFile = decl.getSourceFile();
238
+ if (sourceFile.fileName.endsWith('/db-types.ts')) {
239
+ // Found a type from db-types.ts!
240
+ violations.push(symbolToCheck.getName());
241
+ } else {
242
+ // If it's a type alias or interface defined locally, check its properties
243
+ if (ts.isTypeAliasDeclaration(decl) && decl.type) {
244
+ // Special case: Check if this is z.infer<typeof XxxSchema>
245
+ const zodSchemaViolations = checkForZodInferPattern(decl.type, typeChecker);
246
+ violations.push(...zodSchemaViolations);
247
+
248
+ // Also check the type itself
249
+ const nestedViolations = checkTypeNodeForDbTypes(decl.type, typeChecker, visited);
250
+ violations.push(...nestedViolations);
251
+ } else if (ts.isInterfaceDeclaration(decl)) {
252
+ // Check interface members
253
+ for (const member of decl.members) {
254
+ if (ts.isPropertySignature(member) && member.type) {
255
+ const nestedViolations = checkTypeNodeForDbTypes(
256
+ member.type,
257
+ typeChecker,
258
+ visited,
259
+ );
260
+ violations.push(...nestedViolations);
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+ }
268
+
269
+ // Check type arguments (e.g., Array<User>, Promise<Course>)
270
+ if (typeNode.typeArguments) {
271
+ for (const typeArg of typeNode.typeArguments) {
272
+ const nestedViolations = checkTypeNodeForDbTypes(typeArg, typeChecker, visited);
273
+ violations.push(...nestedViolations);
274
+ }
275
+ }
276
+ }
277
+
278
+ // Check array types (e.g., User[])
279
+ if (ts.isArrayTypeNode(typeNode)) {
280
+ const nestedViolations = checkTypeNodeForDbTypes(typeNode.elementType, typeChecker, visited);
281
+ violations.push(...nestedViolations);
282
+ }
283
+
284
+ // Check union types (e.g., User | null)
285
+ if (ts.isUnionTypeNode(typeNode)) {
286
+ for (const type of typeNode.types) {
287
+ const nestedViolations = checkTypeNodeForDbTypes(type, typeChecker, visited);
288
+ violations.push(...nestedViolations);
289
+ }
290
+ }
291
+
292
+ // Check intersection types (e.g., User & { extra: string })
293
+ if (ts.isIntersectionTypeNode(typeNode)) {
294
+ for (const type of typeNode.types) {
295
+ const nestedViolations = checkTypeNodeForDbTypes(type, typeChecker, visited);
296
+ violations.push(...nestedViolations);
297
+ }
298
+ }
299
+
300
+ // Check object type literals and their properties
301
+ if (ts.isTypeLiteralNode(typeNode)) {
302
+ for (const member of typeNode.members) {
303
+ if (ts.isPropertySignature(member) && member.type) {
304
+ const nestedViolations = checkTypeNodeForDbTypes(member.type, typeChecker, visited);
305
+ violations.push(...nestedViolations);
306
+ }
307
+ }
308
+ }
309
+
310
+ // Check indexed access types (e.g., User['name'])
311
+ // These are safe! We're only extracting a specific property, not passing the whole object.
312
+ // So we DON'T recurse into the object type for indexed access.
313
+ if (ts.isIndexedAccessTypeNode(typeNode)) {
314
+ // Do not check - indexed access is safe
315
+ }
316
+
317
+ return violations;
318
+ }
319
+
320
+ /**
321
+ * Check if a type name is in the allowlist of safe types
322
+ */
323
+ function isTypeInAllowlist(typeName: string, allowlist: (string | RegExp)[]): boolean {
324
+ return allowlist.some((pattern) => {
325
+ if (typeof pattern === 'string') {
326
+ return typeName === pattern;
327
+ }
328
+ return pattern.test(typeName);
329
+ });
330
+ }
331
+
332
+ /**
333
+ * Check the props of a component for unsafe types from db-types.ts
334
+ */
335
+ function checkComponentProps(
336
+ context: ReturnType<typeof ESLintUtils.RuleCreator.withoutDocs>['create'] extends (
337
+ context: infer C,
338
+ ) => any
339
+ ? C
340
+ : never,
341
+ typeChecker: ts.TypeChecker,
342
+ componentSymbol: ts.Symbol,
343
+ tsComponentNode: ts.Node,
344
+ jsxElement: TSESTree.JSXElement,
345
+ reportNode: TSESTree.Node,
346
+ allowlist: (string | RegExp)[],
347
+ ): void {
348
+ const childOpeningElement = jsxElement.openingElement;
349
+
350
+ // Get the component's type (function or class component)
351
+ const componentType = typeChecker.getTypeOfSymbolAtLocation(componentSymbol, tsComponentNode);
352
+ const signatures = componentType.getCallSignatures();
353
+
354
+ if (signatures.length === 0) return;
355
+
356
+ // Get the first parameter (props) of the component function
357
+ const propsParam = signatures[0].getParameters()[0];
358
+ if (!propsParam) return;
359
+
360
+ const propsDeclaration = propsParam.valueDeclaration;
361
+
362
+ if (!propsDeclaration || !ts.isParameter(propsDeclaration)) return;
363
+
364
+ // Get the type annotation node from the props parameter
365
+ const propsTypeNode = propsDeclaration.type;
366
+ if (!propsTypeNode) return;
367
+
368
+ // Check each property in the props type
369
+ if (ts.isTypeLiteralNode(propsTypeNode)) {
370
+ // Inline props object: { foo: string; bar: number }
371
+ for (const member of propsTypeNode.members) {
372
+ if (ts.isPropertySignature(member) && member.type && member.name) {
373
+ if (!ts.isIdentifier(member.name)) continue;
374
+
375
+ const propName = member.name.text;
376
+ const violations = checkTypeNodeForDbTypes(member.type, typeChecker);
377
+
378
+ if (violations.length > 0) {
379
+ // Find the JSX attribute for this prop
380
+ const attribute = childOpeningElement.attributes.find(
381
+ (attr) =>
382
+ attr.type === 'JSXAttribute' &&
383
+ attr.name.type === 'JSXIdentifier' &&
384
+ attr.name.name === propName,
385
+ );
386
+
387
+ for (const typeName of violations) {
388
+ if (isTypeInAllowlist(typeName, allowlist)) continue;
389
+
390
+ context.report({
391
+ node: attribute || reportNode,
392
+ messageId: 'unsafeTypes',
393
+ data: { propName, typeName },
394
+ });
395
+ }
396
+ }
397
+ }
398
+ }
399
+ } else if (ts.isTypeReferenceNode(propsTypeNode)) {
400
+ // Props is a type reference (e.g., interface or type alias)
401
+ const symbol = typeChecker.getSymbolAtLocation(propsTypeNode.typeName);
402
+ if (symbol) {
403
+ const resolvedSymbol =
404
+ symbol.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;
405
+ const declarations = resolvedSymbol.getDeclarations();
406
+
407
+ if (declarations && declarations.length > 0) {
408
+ for (const decl of declarations) {
409
+ if (ts.isInterfaceDeclaration(decl) || ts.isTypeLiteralNode(decl)) {
410
+ const members = ts.isInterfaceDeclaration(decl) ? decl.members : decl.members;
411
+
412
+ for (const member of members) {
413
+ if (ts.isPropertySignature(member) && member.type && member.name) {
414
+ if (!ts.isIdentifier(member.name)) continue;
415
+
416
+ const propName = member.name.text;
417
+ const violations = checkTypeNodeForDbTypes(member.type, typeChecker);
418
+
419
+ if (violations.length > 0) {
420
+ // Find the JSX attribute for this prop
421
+ const attribute = childOpeningElement.attributes.find(
422
+ (attr) =>
423
+ attr.type === 'JSXAttribute' &&
424
+ attr.name.type === 'JSXIdentifier' &&
425
+ attr.name.name === propName,
426
+ );
427
+
428
+ for (const typeName of violations) {
429
+ if (isTypeInAllowlist(typeName, allowlist)) continue;
430
+
431
+ context.report({
432
+ node: attribute || reportNode,
433
+ messageId: 'unsafeTypes',
434
+ data: { propName, typeName },
435
+ });
436
+ }
437
+ }
438
+ }
439
+ }
440
+ } else if (ts.isTypeAliasDeclaration(decl) && decl.type) {
441
+ // Type alias might be an inline object type
442
+ if (ts.isTypeLiteralNode(decl.type)) {
443
+ for (const member of decl.type.members) {
444
+ if (ts.isPropertySignature(member) && member.type && member.name) {
445
+ if (!ts.isIdentifier(member.name)) continue;
446
+
447
+ const propName = member.name.text;
448
+ const violations = checkTypeNodeForDbTypes(member.type, typeChecker);
449
+
450
+ if (violations.length > 0) {
451
+ // Find the JSX attribute for this prop
452
+ const attribute = childOpeningElement.attributes.find(
453
+ (attr) =>
454
+ attr.type === 'JSXAttribute' &&
455
+ attr.name.type === 'JSXIdentifier' &&
456
+ attr.name.name === propName,
457
+ );
458
+
459
+ for (const typeName of violations) {
460
+ if (isTypeInAllowlist(typeName, allowlist)) continue;
461
+
462
+ context.report({
463
+ node: attribute || reportNode,
464
+ messageId: 'unsafeTypes',
465
+ data: { propName, typeName },
466
+ });
467
+ }
468
+ }
469
+ }
470
+ }
471
+ }
472
+ }
473
+ }
474
+ }
475
+ }
476
+ }
477
+
478
+ // Check for spread attributes
479
+ const attributes = childOpeningElement.attributes;
480
+ for (const attr of attributes) {
481
+ if (attr.type === 'JSXSpreadAttribute') {
482
+ context.report({
483
+ node: reportNode,
484
+ messageId: 'spreadAttributes',
485
+ });
486
+ continue;
487
+ }
488
+ }
489
+ }
490
+
491
+ export default ESLintUtils.RuleCreator.withoutDocs<
492
+ [{ allowDbTypes?: (string | RegExp)[] }?],
493
+ 'spreadAttributes' | 'unsafeTypes'
494
+ >({
495
+ meta: {
496
+ type: 'problem',
497
+ messages: {
498
+ spreadAttributes: 'Spread attributes are not allowed in Hydrate children.',
499
+ unsafeTypes:
500
+ 'Prop "{{propName}}" uses type "{{typeName}}" which is derived from db-types.ts. Use safe-db-types.ts instead.',
501
+ },
502
+ schema: [
503
+ {
504
+ type: 'object',
505
+ properties: {
506
+ allowDbTypes: {
507
+ type: 'array',
508
+ items: {
509
+ type: 'string',
510
+ },
511
+ },
512
+ },
513
+ additionalProperties: false,
514
+ },
515
+ ],
516
+ },
517
+ defaultOptions: [{}],
518
+ create(context) {
519
+ const options = context.options[0] || {};
520
+ const allowlist = options.allowDbTypes || [];
521
+ return {
522
+ JSXElement(node) {
523
+ const openingElementNameExpression = node.openingElement.name;
524
+ if (openingElementNameExpression.type !== 'JSXIdentifier') return;
525
+
526
+ const elementName = openingElementNameExpression.name;
527
+ if (elementName !== HYDRATE_COMPONENT_NAME) return;
528
+
529
+ const child = extractChild(node.children);
530
+ if (!child) return;
531
+
532
+ // Get the component being rendered
533
+ const childOpeningElement = child.openingElement;
534
+ const childElementName = childOpeningElement.name;
535
+
536
+ if (childElementName.type !== 'JSXIdentifier') return;
537
+
538
+ // Get the component's type to inspect its props
539
+ const services = ESLintUtils.getParserServices(context);
540
+ const typeChecker = services.program.getTypeChecker();
541
+ const tsChildNode = services.esTreeNodeToTSNodeMap.get(childElementName);
542
+ const componentSymbol = typeChecker.getSymbolAtLocation(tsChildNode);
543
+
544
+ if (!componentSymbol) return;
545
+
546
+ checkComponentProps(
547
+ context,
548
+ typeChecker,
549
+ componentSymbol,
550
+ tsChildNode,
551
+ child,
552
+ child,
553
+ allowlist,
554
+ );
555
+ },
556
+
557
+ CallExpression(node) {
558
+ // Check for hydrateHtml(<Component ... />, props?) calls
559
+ if (node.callee.type !== 'Identifier' || node.callee.name !== HYDRATE_FUNCTION_NAME) return;
560
+
561
+ // Should have at least one argument, the first is JSX element.
562
+ if (node.arguments.length === 0) return;
563
+
564
+ const arg = node.arguments[0];
565
+ if (arg.type !== 'JSXElement') return;
566
+
567
+ const jsxElement = arg;
568
+ const openingElement = jsxElement.openingElement;
569
+ const elementName = openingElement.name;
570
+
571
+ if (elementName.type !== 'JSXIdentifier') return;
572
+
573
+ // Get the component's type to inspect its props
574
+ const services = ESLintUtils.getParserServices(context);
575
+ const typeChecker = services.program.getTypeChecker();
576
+ const tsElementNode = services.esTreeNodeToTSNodeMap.get(elementName);
577
+ const componentSymbol = typeChecker.getSymbolAtLocation(tsElementNode);
578
+
579
+ if (!componentSymbol) return;
580
+
581
+ checkComponentProps(
582
+ context,
583
+ typeChecker,
584
+ componentSymbol,
585
+ tsElementNode,
586
+ jsxElement,
587
+ node,
588
+ allowlist,
589
+ );
590
+ },
591
+ };
592
+ },
593
+ });