@trackunit/eslint-plugin-trackunit 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +117 -0
  3. package/package.json +31 -0
  4. package/src/index.d.ts +8 -0
  5. package/src/index.js +20 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/config/fragments/ignores.d.ts +2 -0
  8. package/src/lib/config/fragments/ignores.js +18 -0
  9. package/src/lib/config/fragments/ignores.js.map +1 -0
  10. package/src/lib/config/fragments/import-rules.d.ts +3 -0
  11. package/src/lib/config/fragments/import-rules.js +58 -0
  12. package/src/lib/config/fragments/import-rules.js.map +1 -0
  13. package/src/lib/config/fragments/jest-overrides.d.ts +2 -0
  14. package/src/lib/config/fragments/jest-overrides.js +30 -0
  15. package/src/lib/config/fragments/jest-overrides.js.map +1 -0
  16. package/src/lib/config/fragments/jsdoc-rules.d.ts +3 -0
  17. package/src/lib/config/fragments/jsdoc-rules.js +71 -0
  18. package/src/lib/config/fragments/jsdoc-rules.js.map +1 -0
  19. package/src/lib/config/fragments/module-boundaries.d.ts +2 -0
  20. package/src/lib/config/fragments/module-boundaries.js +92 -0
  21. package/src/lib/config/fragments/module-boundaries.js.map +1 -0
  22. package/src/lib/config/fragments/react-rules.d.ts +5 -0
  23. package/src/lib/config/fragments/react-rules.js +137 -0
  24. package/src/lib/config/fragments/react-rules.js.map +1 -0
  25. package/src/lib/config/fragments/restricted-imports.d.ts +2 -0
  26. package/src/lib/config/fragments/restricted-imports.js +58 -0
  27. package/src/lib/config/fragments/restricted-imports.js.map +1 -0
  28. package/src/lib/config/fragments/testing-library.d.ts +2 -0
  29. package/src/lib/config/fragments/testing-library.js +7 -0
  30. package/src/lib/config/fragments/testing-library.js.map +1 -0
  31. package/src/lib/config/fragments/typescript-rules.d.ts +2 -0
  32. package/src/lib/config/fragments/typescript-rules.js +97 -0
  33. package/src/lib/config/fragments/typescript-rules.js.map +1 -0
  34. package/src/lib/config/index.d.ts +863 -0
  35. package/src/lib/config/index.js +10 -0
  36. package/src/lib/config/index.js.map +1 -0
  37. package/src/lib/config/plugins.d.ts +90 -0
  38. package/src/lib/config/plugins.js +44 -0
  39. package/src/lib/config/plugins.js.map +1 -0
  40. package/src/lib/config/presets/base.d.ts +265 -0
  41. package/src/lib/config/presets/base.js +145 -0
  42. package/src/lib/config/presets/base.js.map +1 -0
  43. package/src/lib/config/presets/e2e.d.ts +10 -0
  44. package/src/lib/config/presets/e2e.js +19 -0
  45. package/src/lib/config/presets/e2e.js.map +1 -0
  46. package/src/lib/config/presets/public-api.d.ts +147 -0
  47. package/src/lib/config/presets/public-api.js +62 -0
  48. package/src/lib/config/presets/public-api.js.map +1 -0
  49. package/src/lib/config/presets/react.d.ts +598 -0
  50. package/src/lib/config/presets/react.js +97 -0
  51. package/src/lib/config/presets/react.js.map +1 -0
  52. package/src/lib/config/presets/server.d.ts +36 -0
  53. package/src/lib/config/presets/server.js +37 -0
  54. package/src/lib/config/presets/server.js.map +1 -0
  55. package/src/lib/config/utils.d.ts +6 -0
  56. package/src/lib/config/utils.js +28 -0
  57. package/src/lib/config/utils.js.map +1 -0
  58. package/src/lib/config-helpers/create-skip-when.d.ts +35 -0
  59. package/src/lib/config-helpers/create-skip-when.js +54 -0
  60. package/src/lib/config-helpers/create-skip-when.js.map +1 -0
  61. package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.d.ts +16 -0
  62. package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.js +83 -0
  63. package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.js.map +1 -0
  64. package/src/lib/rules/design-guideline-button-icon-size-match/design-guideline-button-icon-size-match.d.ts +4 -0
  65. package/src/lib/rules/design-guideline-button-icon-size-match/design-guideline-button-icon-size-match.js +297 -0
  66. package/src/lib/rules/design-guideline-button-icon-size-match/design-guideline-button-icon-size-match.js.map +1 -0
  67. package/src/lib/rules/no-internal-barrel-files/examples.d.ts +80 -0
  68. package/src/lib/rules/no-internal-barrel-files/examples.js +84 -0
  69. package/src/lib/rules/no-internal-barrel-files/examples.js.map +1 -0
  70. package/src/lib/rules/no-internal-barrel-files/no-internal-barrel-files.d.ts +29 -0
  71. package/src/lib/rules/no-internal-barrel-files/no-internal-barrel-files.js +178 -0
  72. package/src/lib/rules/no-internal-barrel-files/no-internal-barrel-files.js.map +1 -0
  73. package/src/lib/rules/no-internal-graphql-when-tagged-with-gql-public/no-internal-graphql-when-tagged-with-gql-public.d.ts +5 -0
  74. package/src/lib/rules/no-internal-graphql-when-tagged-with-gql-public/no-internal-graphql-when-tagged-with-gql-public.js +67 -0
  75. package/src/lib/rules/no-internal-graphql-when-tagged-with-gql-public/no-internal-graphql-when-tagged-with-gql-public.js.map +1 -0
  76. package/src/lib/rules/no-jest-mock-trackunit-react-core-hooks/no-jest-mock-trackunit-react-core-hooks.d.ts +2 -0
  77. package/src/lib/rules/no-jest-mock-trackunit-react-core-hooks/no-jest-mock-trackunit-react-core-hooks.js +34 -0
  78. package/src/lib/rules/no-jest-mock-trackunit-react-core-hooks/no-jest-mock-trackunit-react-core-hooks.js.map +1 -0
  79. package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.d.ts +16 -0
  80. package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.js +55 -0
  81. package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.js.map +1 -0
  82. package/src/lib/rules/no-typescript-assertion/examples.d.ts +1 -0
  83. package/src/lib/rules/no-typescript-assertion/examples.js +45 -0
  84. package/src/lib/rules/no-typescript-assertion/examples.js.map +1 -0
  85. package/src/lib/rules/no-typescript-assertion/no-typescript-assertion.d.ts +20 -0
  86. package/src/lib/rules/no-typescript-assertion/no-typescript-assertion.js +83 -0
  87. package/src/lib/rules/no-typescript-assertion/no-typescript-assertion.js.map +1 -0
  88. package/src/lib/rules/prefer-destructured-imports/prefer-destructured-imports.d.ts +73 -0
  89. package/src/lib/rules/prefer-destructured-imports/prefer-destructured-imports.js +333 -0
  90. package/src/lib/rules/prefer-destructured-imports/prefer-destructured-imports.js.map +1 -0
  91. package/src/lib/rules/prefer-event-specific-callback-naming/name-suggestion-strategies.d.ts +56 -0
  92. package/src/lib/rules/prefer-event-specific-callback-naming/name-suggestion-strategies.js +225 -0
  93. package/src/lib/rules/prefer-event-specific-callback-naming/name-suggestion-strategies.js.map +1 -0
  94. package/src/lib/rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming.d.ts +49 -0
  95. package/src/lib/rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming.js +75 -0
  96. package/src/lib/rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming.js.map +1 -0
  97. package/src/lib/rules/prefer-event-specific-callback-naming/strategies/string-based.d.ts +32 -0
  98. package/src/lib/rules/prefer-event-specific-callback-naming/strategies/string-based.js +143 -0
  99. package/src/lib/rules/prefer-event-specific-callback-naming/strategies/string-based.js.map +1 -0
  100. package/src/lib/rules/prefer-event-specific-callback-naming/strategies/type-based.d.ts +27 -0
  101. package/src/lib/rules/prefer-event-specific-callback-naming/strategies/type-based.js +196 -0
  102. package/src/lib/rules/prefer-event-specific-callback-naming/strategies/type-based.js.map +1 -0
  103. package/src/lib/rules/prefer-event-specific-callback-naming/utils.d.ts +76 -0
  104. package/src/lib/rules/prefer-event-specific-callback-naming/utils.js +245 -0
  105. package/src/lib/rules/prefer-event-specific-callback-naming/utils.js.map +1 -0
  106. package/src/lib/rules/prefer-field-components/prefer-field-components.d.ts +4 -0
  107. package/src/lib/rules/prefer-field-components/prefer-field-components.js +289 -0
  108. package/src/lib/rules/prefer-field-components/prefer-field-components.js.map +1 -0
  109. package/src/lib/rules/prefer-mouse-event-handler-in-react-props/prefer-mouse-event-handler-in-react-props.d.ts +26 -0
  110. package/src/lib/rules/prefer-mouse-event-handler-in-react-props/prefer-mouse-event-handler-in-react-props.js +402 -0
  111. package/src/lib/rules/prefer-mouse-event-handler-in-react-props/prefer-mouse-event-handler-in-react-props.js.map +1 -0
  112. package/src/lib/rules/require-classname-alternatives/require-classname-alternatives.d.ts +13 -0
  113. package/src/lib/rules/require-classname-alternatives/require-classname-alternatives.js +271 -0
  114. package/src/lib/rules/require-classname-alternatives/require-classname-alternatives.js.map +1 -0
  115. package/src/lib/rules/require-list-item-virtualization-props/require-list-item-virtualization-props.d.ts +15 -0
  116. package/src/lib/rules/require-list-item-virtualization-props/require-list-item-virtualization-props.js +245 -0
  117. package/src/lib/rules/require-list-item-virtualization-props/require-list-item-virtualization-props.js.map +1 -0
  118. package/src/lib/rules/require-optional-prop-initialization/require-optional-prop-initialization.d.ts +17 -0
  119. package/src/lib/rules/require-optional-prop-initialization/require-optional-prop-initialization.js +133 -0
  120. package/src/lib/rules/require-optional-prop-initialization/require-optional-prop-initialization.js.map +1 -0
  121. package/src/lib/rules/require-optional-prop-initialization/suggestion-utils.d.ts +12 -0
  122. package/src/lib/rules/require-optional-prop-initialization/suggestion-utils.js +128 -0
  123. package/src/lib/rules/require-optional-prop-initialization/suggestion-utils.js.map +1 -0
  124. package/src/lib/rules-map.d.ts +66 -0
  125. package/src/lib/rules-map.js +34 -0
  126. package/src/lib/rules-map.js.map +1 -0
  127. package/src/lib/utils/ast-utils.d.ts +85 -0
  128. package/src/lib/utils/ast-utils.js +530 -0
  129. package/src/lib/utils/ast-utils.js.map +1 -0
  130. package/src/lib/utils/classname-utils.d.ts +150 -0
  131. package/src/lib/utils/classname-utils.js +492 -0
  132. package/src/lib/utils/classname-utils.js.map +1 -0
  133. package/src/lib/utils/file-utils.d.ts +14 -0
  134. package/src/lib/utils/file-utils.js +106 -0
  135. package/src/lib/utils/file-utils.js.map +1 -0
  136. package/src/lib/utils/import-utils.d.ts +85 -0
  137. package/src/lib/utils/import-utils.js +193 -0
  138. package/src/lib/utils/import-utils.js.map +1 -0
  139. package/src/lib/utils/nx-utils.d.ts +59 -0
  140. package/src/lib/utils/nx-utils.js +103 -0
  141. package/src/lib/utils/nx-utils.js.map +1 -0
  142. package/src/lib/utils/package-utils.d.ts +38 -0
  143. package/src/lib/utils/package-utils.js +74 -0
  144. package/src/lib/utils/package-utils.js.map +1 -0
  145. package/src/lib/utils/typescript-utils.d.ts +29 -0
  146. package/src/lib/utils/typescript-utils.js +213 -0
  147. package/src/lib/utils/typescript-utils.js.map +1 -0
@@ -0,0 +1,402 @@
1
+ "use strict";
2
+ /**
3
+ * ESLint rule: prefer-mouse-event-handler-in-react-props
4
+ *
5
+ * Enforces using MouseEventHandler<T> instead of () => void or (e: MouseEvent) => void
6
+ * for onClick* props in React components. This ensures proper React typing and consistency.
7
+ *
8
+ * Only flags properties in actual React component props, not in hook options or other interfaces.
9
+ *
10
+ * Uses ESLint suggestions (not autofix) to let users choose the appropriate element type.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.preferMouseEventHandlerInReactProps = void 0;
14
+ const tslib_1 = require("tslib");
15
+ const utils_1 = require("@typescript-eslint/utils");
16
+ const ts = tslib_1.__importStar(require("typescript"));
17
+ const ast_utils_1 = require("../../utils/ast-utils");
18
+ const createRule = utils_1.ESLintUtils.RuleCreator(name => `https://github.com/trackunit/manager/blob/main/libs/eslint/plugin-trackunit/src/lib/rules/${name}/${name}.ts`);
19
+ /**
20
+ * Checks if a property name starts with "onClick" (case-sensitive).
21
+ */
22
+ const isOnClickProp = (name) => {
23
+ return name.startsWith("onClick");
24
+ };
25
+ /**
26
+ * Extracts the element type from a MouseEvent generic type annotation.
27
+ * Returns null if not a MouseEvent type or no generic provided.
28
+ *
29
+ * Examples:
30
+ * - MouseEvent<HTMLButtonElement> -> "HTMLButtonElement"
31
+ * - React.MouseEvent<HTMLDivElement> -> "HTMLDivElement"
32
+ * - MouseEvent -> null (no generic)
33
+ */
34
+ const extractElementTypeFromMouseEvent = (typeAnnotation) => {
35
+ if (typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
36
+ return null;
37
+ }
38
+ const { typeName, typeArguments } = typeAnnotation;
39
+ let isMouseEvent = false;
40
+ if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
41
+ isMouseEvent = typeName.name === "MouseEvent";
42
+ }
43
+ else if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
44
+ isMouseEvent =
45
+ typeName.left.type === utils_1.AST_NODE_TYPES.Identifier &&
46
+ typeName.left.name === "React" &&
47
+ typeName.right.name === "MouseEvent";
48
+ }
49
+ if (!isMouseEvent) {
50
+ return null;
51
+ }
52
+ if (!typeArguments || typeArguments.params.length === 0) {
53
+ return null;
54
+ }
55
+ const firstTypeArg = typeArguments.params[0];
56
+ if (firstTypeArg?.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
57
+ if (firstTypeArg.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
58
+ return firstTypeArg.typeName.name;
59
+ }
60
+ }
61
+ return null;
62
+ };
63
+ /**
64
+ * Checks if a type annotation is a MouseEvent type (with or without generics).
65
+ */
66
+ const isMouseEventType = (typeAnnotation) => {
67
+ if (typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
68
+ return false;
69
+ }
70
+ const { typeName } = typeAnnotation;
71
+ if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
72
+ return typeName.name === "MouseEvent";
73
+ }
74
+ if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
75
+ return (typeName.left.type === utils_1.AST_NODE_TYPES.Identifier &&
76
+ typeName.left.name === "React" &&
77
+ typeName.right.name === "MouseEvent");
78
+ }
79
+ return false;
80
+ };
81
+ /**
82
+ * Checks if a function type returns void.
83
+ */
84
+ const hasVoidReturn = (funcType) => {
85
+ const { returnType } = funcType;
86
+ if (!returnType) {
87
+ return true;
88
+ }
89
+ const typeAnnotation = returnType.typeAnnotation;
90
+ if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSVoidKeyword) {
91
+ return true;
92
+ }
93
+ return false;
94
+ };
95
+ /**
96
+ * Analyzes a TSFunctionType to determine if it should be flagged.
97
+ */
98
+ const analyzeFunctionType = (funcType) => {
99
+ if (!hasVoidReturn(funcType)) {
100
+ return { shouldFlag: false };
101
+ }
102
+ const { params } = funcType;
103
+ // Case A: No parameters - () => void
104
+ if (params.length === 0) {
105
+ return {
106
+ shouldFlag: true,
107
+ inferredElementType: null,
108
+ typeAnnotationNode: funcType,
109
+ };
110
+ }
111
+ // Case B: Exactly one parameter that is MouseEvent
112
+ if (params.length === 1) {
113
+ const param = params[0];
114
+ let paramTypeAnnotation;
115
+ if (param?.type === utils_1.AST_NODE_TYPES.Identifier && param.typeAnnotation) {
116
+ paramTypeAnnotation = param.typeAnnotation.typeAnnotation;
117
+ }
118
+ if (paramTypeAnnotation && isMouseEventType(paramTypeAnnotation)) {
119
+ const elementType = extractElementTypeFromMouseEvent(paramTypeAnnotation);
120
+ return {
121
+ shouldFlag: true,
122
+ inferredElementType: elementType,
123
+ typeAnnotationNode: funcType,
124
+ };
125
+ }
126
+ }
127
+ return { shouldFlag: false };
128
+ };
129
+ /**
130
+ * Creates suggestion fixes for a given element type.
131
+ */
132
+ const createSuggestion = (elementType, typeAnnotationNode) => {
133
+ return {
134
+ messageId: "suggestMouseEventHandler",
135
+ data: { elementType },
136
+ fix: fixer => {
137
+ return fixer.replaceText(typeAnnotationNode, `MouseEventHandler<${elementType}>`);
138
+ },
139
+ };
140
+ };
141
+ /**
142
+ * Known valid HTML element types for MouseEventHandler.
143
+ */
144
+ const VALID_HTML_ELEMENT_TYPES = new Set([
145
+ "Element",
146
+ "HTMLElement",
147
+ "HTMLAnchorElement",
148
+ "HTMLButtonElement",
149
+ "HTMLDivElement",
150
+ "HTMLFormElement",
151
+ "HTMLImageElement",
152
+ "HTMLInputElement",
153
+ "HTMLLabelElement",
154
+ "HTMLLIElement",
155
+ "HTMLParagraphElement",
156
+ "HTMLSelectElement",
157
+ "HTMLSpanElement",
158
+ "HTMLTableElement",
159
+ "HTMLTableRowElement",
160
+ "HTMLTableCellElement",
161
+ "HTMLTextAreaElement",
162
+ "HTMLUListElement",
163
+ "SVGElement",
164
+ "SVGSVGElement",
165
+ ]);
166
+ /**
167
+ * Extracts element type from a MouseEventHandler type string.
168
+ */
169
+ const extractElementTypeFromTypeString = (typeString) => {
170
+ const match = typeString.match(/MouseEventHandler<([^>]+)>/);
171
+ if (match?.[1]) {
172
+ const elementType = match[1].trim();
173
+ if (VALID_HTML_ELEMENT_TYPES.has(elementType)) {
174
+ return elementType;
175
+ }
176
+ }
177
+ return null;
178
+ };
179
+ /**
180
+ * Tries to infer the element type by finding where the prop is used in JSX.
181
+ */
182
+ const inferElementTypeFromUsage = (propName, context) => {
183
+ try {
184
+ const services = utils_1.ESLintUtils.getParserServices(context);
185
+ const checker = services.program.getTypeChecker();
186
+ const sourceFile = services.program.getSourceFile(context.filename);
187
+ if (!sourceFile) {
188
+ return null;
189
+ }
190
+ let inferredType = null;
191
+ const visit = (node) => {
192
+ if (ts.isJsxAttribute(node)) {
193
+ const attrName = node.name.getText(sourceFile);
194
+ if (attrName.startsWith("on")) {
195
+ const initializer = node.initializer;
196
+ if (initializer && ts.isJsxExpression(initializer) && initializer.expression) {
197
+ const expr = initializer.expression;
198
+ let referencedName = null;
199
+ if (ts.isIdentifier(expr) && expr.text === propName) {
200
+ referencedName = propName;
201
+ }
202
+ if (ts.isArrowFunction(expr) && ts.isCallExpression(expr.body)) {
203
+ const callee = expr.body.expression;
204
+ if (ts.isIdentifier(callee) && callee.text === propName) {
205
+ referencedName = propName;
206
+ }
207
+ }
208
+ if (referencedName) {
209
+ const contextualType = checker.getContextualType(expr);
210
+ if (contextualType) {
211
+ const typeString = checker.typeToString(contextualType);
212
+ const elementType = extractElementTypeFromTypeString(typeString);
213
+ if (elementType) {
214
+ inferredType = elementType;
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ ts.forEachChild(node, visit);
222
+ };
223
+ visit(sourceFile);
224
+ return inferredType;
225
+ }
226
+ catch {
227
+ return null;
228
+ }
229
+ };
230
+ /**
231
+ * Gets onClick* properties from a component's props type annotation.
232
+ */
233
+ const getOnClickPropsFromType = (context, typeAnnotation) => {
234
+ if (!typeAnnotation) {
235
+ return [];
236
+ }
237
+ const result = [];
238
+ // Handle inline type literals: ({ onClick }: { onClick: () => void }) => ...
239
+ if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
240
+ for (const member of typeAnnotation.members) {
241
+ if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
242
+ member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
243
+ isOnClickProp(member.key.name) &&
244
+ member.typeAnnotation?.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSFunctionType) {
245
+ result.push({
246
+ name: member.key.name,
247
+ typeNode: member.typeAnnotation.typeAnnotation,
248
+ propertyNode: member,
249
+ });
250
+ }
251
+ }
252
+ return result;
253
+ }
254
+ // Handle type references: ({ onClick }: ButtonProps) => ...
255
+ if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
256
+ try {
257
+ const services = utils_1.ESLintUtils.getParserServices(context);
258
+ const checker = services.program.getTypeChecker();
259
+ const tsNode = services.esTreeNodeToTSNodeMap.get(typeAnnotation);
260
+ const type = checker.getTypeAtLocation(tsNode);
261
+ for (const prop of type.getProperties()) {
262
+ if (isOnClickProp(prop.name)) {
263
+ const propType = checker.getTypeOfSymbolAtLocation(prop, tsNode);
264
+ const callSignatures = propType.getCallSignatures();
265
+ if (callSignatures.length > 0) {
266
+ // Find the corresponding ESTree node for this property
267
+ const declarations = prop.getDeclarations();
268
+ const decl = declarations?.[0];
269
+ if (decl) {
270
+ const esTreeNode = services.tsNodeToESTreeNodeMap.get(decl);
271
+ if (esTreeNode.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
272
+ esTreeNode.typeAnnotation?.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSFunctionType) {
273
+ result.push({
274
+ name: prop.name,
275
+ typeNode: esTreeNode.typeAnnotation.typeAnnotation,
276
+ propertyNode: esTreeNode,
277
+ });
278
+ }
279
+ }
280
+ }
281
+ }
282
+ }
283
+ }
284
+ catch {
285
+ // Type checking failed - silently skip
286
+ }
287
+ }
288
+ return result;
289
+ };
290
+ exports.preferMouseEventHandlerInReactProps = createRule({
291
+ name: "prefer-mouse-event-handler-in-react-props",
292
+ meta: {
293
+ type: "suggestion",
294
+ docs: {
295
+ description: "Enforce using MouseEventHandler<T> instead of () => void or (e: MouseEvent) => void for onClick* props in React components. " +
296
+ "This ensures proper React typing and consistency across the codebase.",
297
+ },
298
+ hasSuggestions: true,
299
+ schema: [
300
+ {
301
+ type: "object",
302
+ properties: {
303
+ allowedNames: {
304
+ type: "array",
305
+ items: { type: "string" },
306
+ description: "Prop names to exempt from this rule",
307
+ },
308
+ },
309
+ additionalProperties: false,
310
+ },
311
+ ],
312
+ messages: {
313
+ preferMouseEventHandler: "Prop '{{propName}}' should use MouseEventHandler<T> instead of {{currentType}}.",
314
+ suggestMouseEventHandler: "Use MouseEventHandler<{{elementType}}>",
315
+ },
316
+ },
317
+ defaultOptions: [
318
+ {
319
+ allowedNames: [],
320
+ },
321
+ ],
322
+ create(context, [options]) {
323
+ const allowedNames = options.allowedNames ?? [];
324
+ // Only run on .tsx files
325
+ const filename = context.filename;
326
+ if (!filename.endsWith(".tsx")) {
327
+ return {};
328
+ }
329
+ // Track reported property nodes to avoid duplicate reports when multiple components use the same interface
330
+ const reportedNodes = new Set();
331
+ const checkComponentProps = (_node, propsParam) => {
332
+ if (!propsParam) {
333
+ return;
334
+ }
335
+ let typeAnnotation;
336
+ // Handle both regular parameters (props: Props) and destructured parameters ({prop1, prop2}: Props)
337
+ if (propsParam.type === utils_1.AST_NODE_TYPES.Identifier || propsParam.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
338
+ typeAnnotation = propsParam.typeAnnotation?.typeAnnotation;
339
+ }
340
+ else {
341
+ return;
342
+ }
343
+ const onClickProps = getOnClickPropsFromType(context, typeAnnotation);
344
+ for (const { name: propName, typeNode, propertyNode } of onClickProps) {
345
+ // Skip if already reported (prevents duplicates when multiple components use the same interface)
346
+ if (reportedNodes.has(propertyNode)) {
347
+ continue;
348
+ }
349
+ // Check if it's in the allowed list
350
+ if (allowedNames.includes(propName)) {
351
+ continue;
352
+ }
353
+ // Analyze the function type
354
+ const result = analyzeFunctionType(typeNode);
355
+ if (!result.shouldFlag) {
356
+ continue;
357
+ }
358
+ // Get the current type as a string for the error message
359
+ const sourceCode = context.sourceCode;
360
+ const currentType = sourceCode.getText(typeNode);
361
+ // Build suggestions
362
+ const suggestions = [];
363
+ let inferredElementType = result.inferredElementType;
364
+ if (!inferredElementType) {
365
+ inferredElementType = inferElementTypeFromUsage(propName, context);
366
+ }
367
+ if (inferredElementType) {
368
+ suggestions.push(createSuggestion(inferredElementType, result.typeAnnotationNode));
369
+ }
370
+ else {
371
+ suggestions.push(createSuggestion("HTMLButtonElement", result.typeAnnotationNode));
372
+ suggestions.push(createSuggestion("HTMLElement", result.typeAnnotationNode));
373
+ suggestions.push(createSuggestion("HTMLDivElement", result.typeAnnotationNode));
374
+ }
375
+ // Mark as reported to prevent duplicates
376
+ reportedNodes.add(propertyNode);
377
+ context.report({
378
+ node: propertyNode,
379
+ messageId: "preferMouseEventHandler",
380
+ data: {
381
+ propName,
382
+ currentType,
383
+ },
384
+ suggest: suggestions,
385
+ });
386
+ }
387
+ };
388
+ return {
389
+ FunctionDeclaration(node) {
390
+ if ((0, ast_utils_1.isReactComponent)(node)) {
391
+ checkComponentProps(node, node.params[0]);
392
+ }
393
+ },
394
+ ArrowFunctionExpression(node) {
395
+ if ((0, ast_utils_1.isReactComponent)(node)) {
396
+ checkComponentProps(node, node.params[0]);
397
+ }
398
+ },
399
+ };
400
+ },
401
+ });
402
+ //# sourceMappingURL=prefer-mouse-event-handler-in-react-props.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-mouse-event-handler-in-react-props.js","sourceRoot":"","sources":["../../../../../../../../libs/eslint/plugin-trackunit/src/lib/rules/prefer-mouse-event-handler-in-react-props/prefer-mouse-event-handler-in-react-props.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESLint rule: prefer-mouse-event-handler-in-react-props\n *\n * Enforces using MouseEventHandler<T> instead of () => void or (e: MouseEvent) => void\n * for onClick* props in React components. This ensures proper React typing and consistency.\n *\n * Only flags properties in actual React component props, not in hook options or other interfaces.\n *\n * Uses ESLint suggestions (not autofix) to let users choose the appropriate element type.\n */\n\nimport { AST_NODE_TYPES, ESLintUtils, TSESLint, TSESTree } from \"@typescript-eslint/utils\";\nimport * as ts from \"typescript\";\nimport { isReactComponent } from \"../../utils/ast-utils\";\n\ntype SuggestionResult = {\n messageId: \"suggestMouseEventHandler\";\n data: { elementType: string };\n fix: (fixer: TSESLint.RuleFixer) => TSESLint.RuleFix;\n};\n\nconst createRule = ESLintUtils.RuleCreator(\n name => `https://github.com/trackunit/manager/blob/main/libs/eslint/plugin-trackunit/src/lib/rules/${name}/${name}.ts`\n);\n\ntype Options = [\n {\n /**\n * Prop names to exempt from this rule.\n *\n * @example [\"onClick\"]\n */\n allowedNames?: ReadonlyArray<string>;\n },\n];\n\ntype MessageIds = \"preferMouseEventHandler\" | \"suggestMouseEventHandler\";\n\n/**\n * Checks if a property name starts with \"onClick\" (case-sensitive).\n */\nconst isOnClickProp = (name: string): boolean => {\n return name.startsWith(\"onClick\");\n};\n\n/**\n * Extracts the element type from a MouseEvent generic type annotation.\n * Returns null if not a MouseEvent type or no generic provided.\n *\n * Examples:\n * - MouseEvent<HTMLButtonElement> -> \"HTMLButtonElement\"\n * - React.MouseEvent<HTMLDivElement> -> \"HTMLDivElement\"\n * - MouseEvent -> null (no generic)\n */\nconst extractElementTypeFromMouseEvent = (typeAnnotation: TSESTree.TypeNode): string | null => {\n if (typeAnnotation.type !== AST_NODE_TYPES.TSTypeReference) {\n return null;\n }\n\n const { typeName, typeArguments } = typeAnnotation;\n\n let isMouseEvent = false;\n\n if (typeName.type === AST_NODE_TYPES.Identifier) {\n isMouseEvent = typeName.name === \"MouseEvent\";\n } else if (typeName.type === AST_NODE_TYPES.TSQualifiedName) {\n isMouseEvent =\n typeName.left.type === AST_NODE_TYPES.Identifier &&\n typeName.left.name === \"React\" &&\n typeName.right.name === \"MouseEvent\";\n }\n\n if (!isMouseEvent) {\n return null;\n }\n\n if (!typeArguments || typeArguments.params.length === 0) {\n return null;\n }\n\n const firstTypeArg = typeArguments.params[0];\n if (firstTypeArg?.type === AST_NODE_TYPES.TSTypeReference) {\n if (firstTypeArg.typeName.type === AST_NODE_TYPES.Identifier) {\n return firstTypeArg.typeName.name;\n }\n }\n\n return null;\n};\n\n/**\n * Checks if a type annotation is a MouseEvent type (with or without generics).\n */\nconst isMouseEventType = (typeAnnotation: TSESTree.TypeNode): boolean => {\n if (typeAnnotation.type !== AST_NODE_TYPES.TSTypeReference) {\n return false;\n }\n\n const { typeName } = typeAnnotation;\n\n if (typeName.type === AST_NODE_TYPES.Identifier) {\n return typeName.name === \"MouseEvent\";\n }\n\n if (typeName.type === AST_NODE_TYPES.TSQualifiedName) {\n return (\n typeName.left.type === AST_NODE_TYPES.Identifier &&\n typeName.left.name === \"React\" &&\n typeName.right.name === \"MouseEvent\"\n );\n }\n\n return false;\n};\n\n/**\n * Checks if a function type returns void.\n */\nconst hasVoidReturn = (funcType: TSESTree.TSFunctionType): boolean => {\n const { returnType } = funcType;\n\n if (!returnType) {\n return true;\n }\n\n const typeAnnotation = returnType.typeAnnotation;\n\n if (typeAnnotation.type === AST_NODE_TYPES.TSVoidKeyword) {\n return true;\n }\n\n return false;\n};\n\ntype AnalysisResult =\n | { shouldFlag: false }\n | {\n shouldFlag: true;\n inferredElementType: string | null;\n typeAnnotationNode: TSESTree.TSFunctionType;\n };\n\n/**\n * Analyzes a TSFunctionType to determine if it should be flagged.\n */\nconst analyzeFunctionType = (funcType: TSESTree.TSFunctionType): AnalysisResult => {\n if (!hasVoidReturn(funcType)) {\n return { shouldFlag: false };\n }\n\n const { params } = funcType;\n\n // Case A: No parameters - () => void\n if (params.length === 0) {\n return {\n shouldFlag: true,\n inferredElementType: null,\n typeAnnotationNode: funcType,\n };\n }\n\n // Case B: Exactly one parameter that is MouseEvent\n if (params.length === 1) {\n const param = params[0];\n\n let paramTypeAnnotation: TSESTree.TypeNode | undefined;\n\n if (param?.type === AST_NODE_TYPES.Identifier && param.typeAnnotation) {\n paramTypeAnnotation = param.typeAnnotation.typeAnnotation;\n }\n\n if (paramTypeAnnotation && isMouseEventType(paramTypeAnnotation)) {\n const elementType = extractElementTypeFromMouseEvent(paramTypeAnnotation);\n return {\n shouldFlag: true,\n inferredElementType: elementType,\n typeAnnotationNode: funcType,\n };\n }\n }\n\n return { shouldFlag: false };\n};\n\n/**\n * Creates suggestion fixes for a given element type.\n */\nconst createSuggestion = (elementType: string, typeAnnotationNode: TSESTree.TSFunctionType): SuggestionResult => {\n return {\n messageId: \"suggestMouseEventHandler\",\n data: { elementType },\n fix: fixer => {\n return fixer.replaceText(typeAnnotationNode, `MouseEventHandler<${elementType}>`);\n },\n };\n};\n\n/**\n * Known valid HTML element types for MouseEventHandler.\n */\nconst VALID_HTML_ELEMENT_TYPES = new Set([\n \"Element\",\n \"HTMLElement\",\n \"HTMLAnchorElement\",\n \"HTMLButtonElement\",\n \"HTMLDivElement\",\n \"HTMLFormElement\",\n \"HTMLImageElement\",\n \"HTMLInputElement\",\n \"HTMLLabelElement\",\n \"HTMLLIElement\",\n \"HTMLParagraphElement\",\n \"HTMLSelectElement\",\n \"HTMLSpanElement\",\n \"HTMLTableElement\",\n \"HTMLTableRowElement\",\n \"HTMLTableCellElement\",\n \"HTMLTextAreaElement\",\n \"HTMLUListElement\",\n \"SVGElement\",\n \"SVGSVGElement\",\n]);\n\n/**\n * Extracts element type from a MouseEventHandler type string.\n */\nconst extractElementTypeFromTypeString = (typeString: string): string | null => {\n const match = typeString.match(/MouseEventHandler<([^>]+)>/);\n if (match?.[1]) {\n const elementType = match[1].trim();\n if (VALID_HTML_ELEMENT_TYPES.has(elementType)) {\n return elementType;\n }\n }\n return null;\n};\n\n/**\n * Tries to infer the element type by finding where the prop is used in JSX.\n */\nconst inferElementTypeFromUsage = (\n propName: string,\n context: TSESLint.RuleContext<MessageIds, ReadonlyArray<unknown>>\n): string | null => {\n try {\n const services = ESLintUtils.getParserServices(context);\n const checker = services.program.getTypeChecker();\n const sourceFile = services.program.getSourceFile(context.filename);\n\n if (!sourceFile) {\n return null;\n }\n\n let inferredType: string | null = null;\n\n const visit = (node: ts.Node): void => {\n if (ts.isJsxAttribute(node)) {\n const attrName = node.name.getText(sourceFile);\n\n if (attrName.startsWith(\"on\")) {\n const initializer = node.initializer;\n\n if (initializer && ts.isJsxExpression(initializer) && initializer.expression) {\n const expr = initializer.expression;\n let referencedName: string | null = null;\n\n if (ts.isIdentifier(expr) && expr.text === propName) {\n referencedName = propName;\n }\n\n if (ts.isArrowFunction(expr) && ts.isCallExpression(expr.body)) {\n const callee = expr.body.expression;\n if (ts.isIdentifier(callee) && callee.text === propName) {\n referencedName = propName;\n }\n }\n\n if (referencedName) {\n const contextualType = checker.getContextualType(expr);\n if (contextualType) {\n const typeString = checker.typeToString(contextualType);\n const elementType = extractElementTypeFromTypeString(typeString);\n if (elementType) {\n inferredType = elementType;\n }\n }\n }\n }\n }\n }\n\n ts.forEachChild(node, visit);\n };\n\n visit(sourceFile);\n return inferredType;\n } catch {\n return null;\n }\n};\n\n/**\n * Gets onClick* properties from a component's props type annotation.\n */\nconst getOnClickPropsFromType = (\n context: TSESLint.RuleContext<MessageIds, ReadonlyArray<unknown>>,\n typeAnnotation: TSESTree.TypeNode | undefined\n): Array<{ name: string; typeNode: TSESTree.TSFunctionType; propertyNode: TSESTree.Node }> => {\n if (!typeAnnotation) {\n return [];\n }\n\n const result: Array<{ name: string; typeNode: TSESTree.TSFunctionType; propertyNode: TSESTree.Node }> = [];\n\n // Handle inline type literals: ({ onClick }: { onClick: () => void }) => ...\n if (typeAnnotation.type === AST_NODE_TYPES.TSTypeLiteral) {\n for (const member of typeAnnotation.members) {\n if (\n member.type === AST_NODE_TYPES.TSPropertySignature &&\n member.key.type === AST_NODE_TYPES.Identifier &&\n isOnClickProp(member.key.name) &&\n member.typeAnnotation?.typeAnnotation.type === AST_NODE_TYPES.TSFunctionType\n ) {\n result.push({\n name: member.key.name,\n typeNode: member.typeAnnotation.typeAnnotation,\n propertyNode: member,\n });\n }\n }\n return result;\n }\n\n // Handle type references: ({ onClick }: ButtonProps) => ...\n if (typeAnnotation.type === AST_NODE_TYPES.TSTypeReference) {\n try {\n const services = ESLintUtils.getParserServices(context);\n const checker = services.program.getTypeChecker();\n const tsNode = services.esTreeNodeToTSNodeMap.get(typeAnnotation);\n const type = checker.getTypeAtLocation(tsNode);\n\n for (const prop of type.getProperties()) {\n if (isOnClickProp(prop.name)) {\n const propType = checker.getTypeOfSymbolAtLocation(prop, tsNode);\n const callSignatures = propType.getCallSignatures();\n\n if (callSignatures.length > 0) {\n // Find the corresponding ESTree node for this property\n const declarations = prop.getDeclarations();\n const decl = declarations?.[0];\n if (decl) {\n const esTreeNode = services.tsNodeToESTreeNodeMap.get(decl);\n if (\n esTreeNode.type === AST_NODE_TYPES.TSPropertySignature &&\n esTreeNode.typeAnnotation?.typeAnnotation.type === AST_NODE_TYPES.TSFunctionType\n ) {\n result.push({\n name: prop.name,\n typeNode: esTreeNode.typeAnnotation.typeAnnotation,\n propertyNode: esTreeNode,\n });\n }\n }\n }\n }\n }\n } catch {\n // Type checking failed - silently skip\n }\n }\n\n return result;\n};\n\nexport const preferMouseEventHandlerInReactProps = createRule<Options, MessageIds>({\n name: \"prefer-mouse-event-handler-in-react-props\",\n meta: {\n type: \"suggestion\",\n docs: {\n description:\n \"Enforce using MouseEventHandler<T> instead of () => void or (e: MouseEvent) => void for onClick* props in React components. \" +\n \"This ensures proper React typing and consistency across the codebase.\",\n },\n hasSuggestions: true,\n schema: [\n {\n type: \"object\",\n properties: {\n allowedNames: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Prop names to exempt from this rule\",\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferMouseEventHandler: \"Prop '{{propName}}' should use MouseEventHandler<T> instead of {{currentType}}.\",\n suggestMouseEventHandler: \"Use MouseEventHandler<{{elementType}}>\",\n },\n },\n defaultOptions: [\n {\n allowedNames: [],\n },\n ],\n create(context, [options]) {\n const allowedNames = options.allowedNames ?? [];\n\n // Only run on .tsx files\n const filename = context.filename;\n if (!filename.endsWith(\".tsx\")) {\n return {};\n }\n\n // Track reported property nodes to avoid duplicate reports when multiple components use the same interface\n const reportedNodes = new Set<TSESTree.Node>();\n\n const checkComponentProps = (\n _node: TSESTree.FunctionDeclaration | TSESTree.ArrowFunctionExpression,\n propsParam: TSESTree.Parameter | undefined\n ) => {\n if (!propsParam) {\n return;\n }\n\n let typeAnnotation: TSESTree.TypeNode | undefined;\n\n // Handle both regular parameters (props: Props) and destructured parameters ({prop1, prop2}: Props)\n if (propsParam.type === AST_NODE_TYPES.Identifier || propsParam.type === AST_NODE_TYPES.ObjectPattern) {\n typeAnnotation = propsParam.typeAnnotation?.typeAnnotation;\n } else {\n return;\n }\n\n const onClickProps = getOnClickPropsFromType(context, typeAnnotation);\n\n for (const { name: propName, typeNode, propertyNode } of onClickProps) {\n // Skip if already reported (prevents duplicates when multiple components use the same interface)\n if (reportedNodes.has(propertyNode)) {\n continue;\n }\n\n // Check if it's in the allowed list\n if (allowedNames.includes(propName)) {\n continue;\n }\n\n // Analyze the function type\n const result = analyzeFunctionType(typeNode);\n\n if (!result.shouldFlag) {\n continue;\n }\n\n // Get the current type as a string for the error message\n const sourceCode = context.sourceCode;\n const currentType = sourceCode.getText(typeNode);\n\n // Build suggestions\n const suggestions: Array<SuggestionResult> = [];\n\n let inferredElementType = result.inferredElementType;\n\n if (!inferredElementType) {\n inferredElementType = inferElementTypeFromUsage(propName, context);\n }\n\n if (inferredElementType) {\n suggestions.push(createSuggestion(inferredElementType, result.typeAnnotationNode));\n } else {\n suggestions.push(createSuggestion(\"HTMLButtonElement\", result.typeAnnotationNode));\n suggestions.push(createSuggestion(\"HTMLElement\", result.typeAnnotationNode));\n suggestions.push(createSuggestion(\"HTMLDivElement\", result.typeAnnotationNode));\n }\n\n // Mark as reported to prevent duplicates\n reportedNodes.add(propertyNode);\n\n context.report({\n node: propertyNode,\n messageId: \"preferMouseEventHandler\",\n data: {\n propName,\n currentType,\n },\n suggest: suggestions,\n });\n }\n };\n\n return {\n FunctionDeclaration(node: TSESTree.FunctionDeclaration) {\n if (isReactComponent(node)) {\n checkComponentProps(node, node.params[0]);\n }\n },\n\n ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression) {\n if (isReactComponent(node)) {\n checkComponentProps(node, node.params[0]);\n }\n },\n };\n },\n});\n"]}
@@ -0,0 +1,13 @@
1
+ import { ESLintUtils } from "@typescript-eslint/utils";
2
+ type MessageIds = "bannedClassAutoFix" | "bannedClassSuggest" | "suggestReplacement";
3
+ type RuleConfig = {
4
+ /** Map of banned class name patterns to their allowed alternatives */
5
+ alternatives: Record<string, Array<string>>;
6
+ /** Optional prefixes to match (e.g., "bg", "text", "border"). If not provided, matches exact class names. */
7
+ prefixes?: Array<string>;
8
+ };
9
+ type Options = [RuleConfig?];
10
+ export declare const requireClassnameAlternatives: ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
11
+ name: string;
12
+ };
13
+ export {};