@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.
- package/CHANGELOG.md +9 -0
- package/README.md +117 -0
- package/package.json +31 -0
- package/src/index.d.ts +8 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/config/fragments/ignores.d.ts +2 -0
- package/src/lib/config/fragments/ignores.js +18 -0
- package/src/lib/config/fragments/ignores.js.map +1 -0
- package/src/lib/config/fragments/import-rules.d.ts +3 -0
- package/src/lib/config/fragments/import-rules.js +58 -0
- package/src/lib/config/fragments/import-rules.js.map +1 -0
- package/src/lib/config/fragments/jest-overrides.d.ts +2 -0
- package/src/lib/config/fragments/jest-overrides.js +30 -0
- package/src/lib/config/fragments/jest-overrides.js.map +1 -0
- package/src/lib/config/fragments/jsdoc-rules.d.ts +3 -0
- package/src/lib/config/fragments/jsdoc-rules.js +71 -0
- package/src/lib/config/fragments/jsdoc-rules.js.map +1 -0
- package/src/lib/config/fragments/module-boundaries.d.ts +2 -0
- package/src/lib/config/fragments/module-boundaries.js +92 -0
- package/src/lib/config/fragments/module-boundaries.js.map +1 -0
- package/src/lib/config/fragments/react-rules.d.ts +5 -0
- package/src/lib/config/fragments/react-rules.js +137 -0
- package/src/lib/config/fragments/react-rules.js.map +1 -0
- package/src/lib/config/fragments/restricted-imports.d.ts +2 -0
- package/src/lib/config/fragments/restricted-imports.js +58 -0
- package/src/lib/config/fragments/restricted-imports.js.map +1 -0
- package/src/lib/config/fragments/testing-library.d.ts +2 -0
- package/src/lib/config/fragments/testing-library.js +7 -0
- package/src/lib/config/fragments/testing-library.js.map +1 -0
- package/src/lib/config/fragments/typescript-rules.d.ts +2 -0
- package/src/lib/config/fragments/typescript-rules.js +97 -0
- package/src/lib/config/fragments/typescript-rules.js.map +1 -0
- package/src/lib/config/index.d.ts +863 -0
- package/src/lib/config/index.js +10 -0
- package/src/lib/config/index.js.map +1 -0
- package/src/lib/config/plugins.d.ts +90 -0
- package/src/lib/config/plugins.js +44 -0
- package/src/lib/config/plugins.js.map +1 -0
- package/src/lib/config/presets/base.d.ts +265 -0
- package/src/lib/config/presets/base.js +145 -0
- package/src/lib/config/presets/base.js.map +1 -0
- package/src/lib/config/presets/e2e.d.ts +10 -0
- package/src/lib/config/presets/e2e.js +19 -0
- package/src/lib/config/presets/e2e.js.map +1 -0
- package/src/lib/config/presets/public-api.d.ts +147 -0
- package/src/lib/config/presets/public-api.js +62 -0
- package/src/lib/config/presets/public-api.js.map +1 -0
- package/src/lib/config/presets/react.d.ts +598 -0
- package/src/lib/config/presets/react.js +97 -0
- package/src/lib/config/presets/react.js.map +1 -0
- package/src/lib/config/presets/server.d.ts +36 -0
- package/src/lib/config/presets/server.js +37 -0
- package/src/lib/config/presets/server.js.map +1 -0
- package/src/lib/config/utils.d.ts +6 -0
- package/src/lib/config/utils.js +28 -0
- package/src/lib/config/utils.js.map +1 -0
- package/src/lib/config-helpers/create-skip-when.d.ts +35 -0
- package/src/lib/config-helpers/create-skip-when.js +54 -0
- package/src/lib/config-helpers/create-skip-when.js.map +1 -0
- package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.d.ts +16 -0
- package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.js +83 -0
- package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.js.map +1 -0
- package/src/lib/rules/design-guideline-button-icon-size-match/design-guideline-button-icon-size-match.d.ts +4 -0
- package/src/lib/rules/design-guideline-button-icon-size-match/design-guideline-button-icon-size-match.js +297 -0
- package/src/lib/rules/design-guideline-button-icon-size-match/design-guideline-button-icon-size-match.js.map +1 -0
- package/src/lib/rules/no-internal-barrel-files/examples.d.ts +80 -0
- package/src/lib/rules/no-internal-barrel-files/examples.js +84 -0
- package/src/lib/rules/no-internal-barrel-files/examples.js.map +1 -0
- package/src/lib/rules/no-internal-barrel-files/no-internal-barrel-files.d.ts +29 -0
- package/src/lib/rules/no-internal-barrel-files/no-internal-barrel-files.js +178 -0
- package/src/lib/rules/no-internal-barrel-files/no-internal-barrel-files.js.map +1 -0
- package/src/lib/rules/no-internal-graphql-when-tagged-with-gql-public/no-internal-graphql-when-tagged-with-gql-public.d.ts +5 -0
- package/src/lib/rules/no-internal-graphql-when-tagged-with-gql-public/no-internal-graphql-when-tagged-with-gql-public.js +67 -0
- package/src/lib/rules/no-internal-graphql-when-tagged-with-gql-public/no-internal-graphql-when-tagged-with-gql-public.js.map +1 -0
- package/src/lib/rules/no-jest-mock-trackunit-react-core-hooks/no-jest-mock-trackunit-react-core-hooks.d.ts +2 -0
- package/src/lib/rules/no-jest-mock-trackunit-react-core-hooks/no-jest-mock-trackunit-react-core-hooks.js +34 -0
- package/src/lib/rules/no-jest-mock-trackunit-react-core-hooks/no-jest-mock-trackunit-react-core-hooks.js.map +1 -0
- package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.d.ts +16 -0
- package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.js +55 -0
- package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.js.map +1 -0
- package/src/lib/rules/no-typescript-assertion/examples.d.ts +1 -0
- package/src/lib/rules/no-typescript-assertion/examples.js +45 -0
- package/src/lib/rules/no-typescript-assertion/examples.js.map +1 -0
- package/src/lib/rules/no-typescript-assertion/no-typescript-assertion.d.ts +20 -0
- package/src/lib/rules/no-typescript-assertion/no-typescript-assertion.js +83 -0
- package/src/lib/rules/no-typescript-assertion/no-typescript-assertion.js.map +1 -0
- package/src/lib/rules/prefer-destructured-imports/prefer-destructured-imports.d.ts +73 -0
- package/src/lib/rules/prefer-destructured-imports/prefer-destructured-imports.js +333 -0
- package/src/lib/rules/prefer-destructured-imports/prefer-destructured-imports.js.map +1 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/name-suggestion-strategies.d.ts +56 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/name-suggestion-strategies.js +225 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/name-suggestion-strategies.js.map +1 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming.d.ts +49 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming.js +75 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming.js.map +1 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/string-based.d.ts +32 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/string-based.js +143 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/string-based.js.map +1 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/type-based.d.ts +27 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/type-based.js +196 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/type-based.js.map +1 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/utils.d.ts +76 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/utils.js +245 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/utils.js.map +1 -0
- package/src/lib/rules/prefer-field-components/prefer-field-components.d.ts +4 -0
- package/src/lib/rules/prefer-field-components/prefer-field-components.js +289 -0
- package/src/lib/rules/prefer-field-components/prefer-field-components.js.map +1 -0
- package/src/lib/rules/prefer-mouse-event-handler-in-react-props/prefer-mouse-event-handler-in-react-props.d.ts +26 -0
- package/src/lib/rules/prefer-mouse-event-handler-in-react-props/prefer-mouse-event-handler-in-react-props.js +402 -0
- package/src/lib/rules/prefer-mouse-event-handler-in-react-props/prefer-mouse-event-handler-in-react-props.js.map +1 -0
- package/src/lib/rules/require-classname-alternatives/require-classname-alternatives.d.ts +13 -0
- package/src/lib/rules/require-classname-alternatives/require-classname-alternatives.js +271 -0
- package/src/lib/rules/require-classname-alternatives/require-classname-alternatives.js.map +1 -0
- package/src/lib/rules/require-list-item-virtualization-props/require-list-item-virtualization-props.d.ts +15 -0
- package/src/lib/rules/require-list-item-virtualization-props/require-list-item-virtualization-props.js +245 -0
- package/src/lib/rules/require-list-item-virtualization-props/require-list-item-virtualization-props.js.map +1 -0
- package/src/lib/rules/require-optional-prop-initialization/require-optional-prop-initialization.d.ts +17 -0
- package/src/lib/rules/require-optional-prop-initialization/require-optional-prop-initialization.js +133 -0
- package/src/lib/rules/require-optional-prop-initialization/require-optional-prop-initialization.js.map +1 -0
- package/src/lib/rules/require-optional-prop-initialization/suggestion-utils.d.ts +12 -0
- package/src/lib/rules/require-optional-prop-initialization/suggestion-utils.js +128 -0
- package/src/lib/rules/require-optional-prop-initialization/suggestion-utils.js.map +1 -0
- package/src/lib/rules-map.d.ts +66 -0
- package/src/lib/rules-map.js +34 -0
- package/src/lib/rules-map.js.map +1 -0
- package/src/lib/utils/ast-utils.d.ts +85 -0
- package/src/lib/utils/ast-utils.js +530 -0
- package/src/lib/utils/ast-utils.js.map +1 -0
- package/src/lib/utils/classname-utils.d.ts +150 -0
- package/src/lib/utils/classname-utils.js +492 -0
- package/src/lib/utils/classname-utils.js.map +1 -0
- package/src/lib/utils/file-utils.d.ts +14 -0
- package/src/lib/utils/file-utils.js +106 -0
- package/src/lib/utils/file-utils.js.map +1 -0
- package/src/lib/utils/import-utils.d.ts +85 -0
- package/src/lib/utils/import-utils.js +193 -0
- package/src/lib/utils/import-utils.js.map +1 -0
- package/src/lib/utils/nx-utils.d.ts +59 -0
- package/src/lib/utils/nx-utils.js +103 -0
- package/src/lib/utils/nx-utils.js.map +1 -0
- package/src/lib/utils/package-utils.d.ts +38 -0
- package/src/lib/utils/package-utils.js +74 -0
- package/src/lib/utils/package-utils.js.map +1 -0
- package/src/lib/utils/typescript-utils.d.ts +29 -0
- package/src/lib/utils/typescript-utils.js +213 -0
- package/src/lib/utils/typescript-utils.js.map +1 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Type-based detection strategy for prefer-event-specific-callback-naming rule.
|
|
4
|
+
*
|
|
5
|
+
* Uses TypeScript's type checker to detect when a callback with problematic naming
|
|
6
|
+
* is passed to a prop typed as a mouse event handler (MouseEventHandler, MouseEvent, etc.).
|
|
7
|
+
*
|
|
8
|
+
* This catches cases where custom components use non-standard prop names for click handlers:
|
|
9
|
+
* - <MobileButton onTap={onClose} /> where onTap is typed as MouseEventHandler
|
|
10
|
+
* - <PressableArea onPress={onCancel} /> where onPress accepts MouseEvent
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.checkTypeBased = exports.typeBasedMessages = void 0;
|
|
14
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
15
|
+
const utils_2 = require("../utils");
|
|
16
|
+
exports.typeBasedMessages = {
|
|
17
|
+
preferOnClickNamingTyped: "Callback '{{current}}' passed to {{event}} should use '{{expectedPrefix}}*' naming (e.g., '{{suggested}}'). " +
|
|
18
|
+
"Reported because {{event}} is typed as a MouseEventHandler. Rename it at its definition.",
|
|
19
|
+
preferOnClickNamingTypedWrapped: "Callback '{{current}}' called in {{event}} should use '{{expectedPrefix}}*' naming (e.g., '{{suggested}}'). " +
|
|
20
|
+
"Reported because {{event}} is typed as a MouseEventHandler. Rename it at its definition.",
|
|
21
|
+
suggestRenameAllTyped: "Rename '{{current}}' to '{{suggested}}' (definition and all usages in this file)",
|
|
22
|
+
suggestRenameLocalTyped: "Rename to '{{suggested}}' (this reference only - definition is in another file)",
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Extracts the expected naming prefix from a suggested name.
|
|
26
|
+
* This is used to show the correct expected pattern in error messages.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* getExpectedPrefix("onClickPrimary") // "onClick"
|
|
30
|
+
* getExpectedPrefix("onMouseDownClose") // "onMouseDown"
|
|
31
|
+
*/
|
|
32
|
+
const getExpectedPrefix = (suggestedName) => {
|
|
33
|
+
// Find where the capitalized suffix starts (after the event prefix)
|
|
34
|
+
// e.g., "onClickPrimary" -> find "Primary" -> prefix is "onClick"
|
|
35
|
+
const match = suggestedName.match(/^(on[A-Z][a-z]*)/);
|
|
36
|
+
return match?.[1] ?? suggestedName;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Checks if a type represents a mouse event handler.
|
|
40
|
+
* Looks for:
|
|
41
|
+
* - MouseEventHandler<T>
|
|
42
|
+
* - React.MouseEventHandler<T>
|
|
43
|
+
* - Function types with MouseEvent as first parameter
|
|
44
|
+
*/
|
|
45
|
+
const isMouseEventHandlerType = (type, checker) => {
|
|
46
|
+
const typeString = checker.typeToString(type);
|
|
47
|
+
// Quick check for common patterns in the type string
|
|
48
|
+
if (typeString.includes("MouseEventHandler") ||
|
|
49
|
+
typeString.includes("MouseEvent<") ||
|
|
50
|
+
typeString.includes("MouseEvent)")) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
// Check call signatures for MouseEvent parameter
|
|
54
|
+
const signatures = type.getCallSignatures();
|
|
55
|
+
for (const signature of signatures) {
|
|
56
|
+
const params = signature.getParameters();
|
|
57
|
+
if (params.length > 0) {
|
|
58
|
+
const firstParam = params[0];
|
|
59
|
+
if (firstParam) {
|
|
60
|
+
const paramType = checker.getTypeOfSymbol(firstParam);
|
|
61
|
+
const paramTypeString = checker.typeToString(paramType);
|
|
62
|
+
if (paramTypeString.includes("MouseEvent")) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Checks a JSX attribute for type-based mouse event handler naming issues.
|
|
72
|
+
*
|
|
73
|
+
* Only runs when "onClick" is in the events list (since this check only detects
|
|
74
|
+
* MouseEventHandler types, which are click events).
|
|
75
|
+
*
|
|
76
|
+
* Skips props already in the events list (those are handled by the name-based strategy).
|
|
77
|
+
*
|
|
78
|
+
* @param node - The JSX attribute node to check
|
|
79
|
+
* @param context - The ESLint rule context
|
|
80
|
+
* @param allowedNames - Callback names to exempt from this rule
|
|
81
|
+
* @param events - Configured event names (used to skip already-covered props)
|
|
82
|
+
*/
|
|
83
|
+
const checkTypeBased = (node, context, allowedNames, events) => {
|
|
84
|
+
// Only run if "onClick" is in the events list, since this check only detects
|
|
85
|
+
// MouseEventHandler types (which are click events)
|
|
86
|
+
if (!events.includes("onClick")) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Extract prop name - must be a JSXIdentifier
|
|
90
|
+
if (node.name.type !== utils_1.AST_NODE_TYPES.JSXIdentifier) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const propName = node.name.name;
|
|
94
|
+
// Skip props already in the events list - those are handled by the name-based strategy
|
|
95
|
+
if (events.includes(propName)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Must have a value that's an expression container
|
|
99
|
+
if (!node.value || node.value.type !== utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const { expression } = node.value;
|
|
103
|
+
if (expression.type === utils_1.AST_NODE_TYPES.JSXEmptyExpression) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Extract callback identifier (without checking hardcoded onClick pattern)
|
|
107
|
+
const result = (0, utils_2.extractCallbackIdentifier)(expression);
|
|
108
|
+
if (!result) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Check if this name is in the allowed list
|
|
112
|
+
if (allowedNames.includes(result.callbackName)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Skip if callback name already matches or starts with the prop name
|
|
116
|
+
// e.g., onMouseEnter={onMouseEnter} or onMouseEnter={onMouseEnterSomething}
|
|
117
|
+
if (result.callbackName === propName || result.callbackName.startsWith(propName)) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// Skip if callback doesn't start with "on" - we only care about on* callbacks
|
|
121
|
+
// that might be confused with lifecycle/event callbacks
|
|
122
|
+
if (!result.callbackName.startsWith("on")) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// Now check if the prop is typed as a mouse event handler
|
|
126
|
+
try {
|
|
127
|
+
const services = utils_1.ESLintUtils.getParserServices(context);
|
|
128
|
+
const checker = services.program.getTypeChecker();
|
|
129
|
+
// Get the TypeScript node for the expression (already validated above)
|
|
130
|
+
const tsExpressionNode = services.esTreeNodeToTSNodeMap.get(expression);
|
|
131
|
+
// Get the contextual type (what type is expected at this position)
|
|
132
|
+
// The ESTree to TS node map returns a generic node, but getContextualType
|
|
133
|
+
// expects an Expression. This assertion is necessary for TypeScript API interop.
|
|
134
|
+
// eslint-disable-next-line @trackunit/no-typescript-assertion
|
|
135
|
+
const contextualType = checker.getContextualType(tsExpressionNode);
|
|
136
|
+
if (!contextualType) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!isMouseEventHandlerType(contextualType, checker)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Calculate suggested name using the actual prop name (e.g., onMouseDown -> onMouseDownClose)
|
|
143
|
+
const suggestedName = (0, utils_2.getSuggestedNameForEvent)(result.callbackName, propName);
|
|
144
|
+
// Select the appropriate messageId based on whether the callback is wrapped
|
|
145
|
+
const messageId = result.isWrapped
|
|
146
|
+
? "preferOnClickNamingTypedWrapped"
|
|
147
|
+
: "preferOnClickNamingTyped";
|
|
148
|
+
// Build suggestions based on whether the definition is in the same file
|
|
149
|
+
const suggestions = [];
|
|
150
|
+
const localInfo = (0, utils_2.findLocalDefinitionInfo)(result.identifierNode, context);
|
|
151
|
+
if (localInfo) {
|
|
152
|
+
// Definition is in this file - offer to rename all occurrences
|
|
153
|
+
suggestions.push({
|
|
154
|
+
messageId: "suggestRenameAllTyped",
|
|
155
|
+
data: {
|
|
156
|
+
current: result.callbackName,
|
|
157
|
+
suggested: suggestedName,
|
|
158
|
+
},
|
|
159
|
+
fix: fixer => {
|
|
160
|
+
return localInfo.allNodesToRename.map(nodeToRename => fixer.replaceText(nodeToRename, suggestedName));
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// Definition is external - only offer to rename this reference
|
|
166
|
+
suggestions.push({
|
|
167
|
+
messageId: "suggestRenameLocalTyped",
|
|
168
|
+
data: {
|
|
169
|
+
current: result.callbackName,
|
|
170
|
+
suggested: suggestedName,
|
|
171
|
+
},
|
|
172
|
+
fix: fixer => {
|
|
173
|
+
return fixer.replaceText(result.identifierNode, suggestedName);
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
context.report({
|
|
178
|
+
node: result.node,
|
|
179
|
+
messageId,
|
|
180
|
+
data: {
|
|
181
|
+
current: result.callbackName,
|
|
182
|
+
suggested: suggestedName,
|
|
183
|
+
event: propName,
|
|
184
|
+
expectedPrefix: getExpectedPrefix(suggestedName),
|
|
185
|
+
},
|
|
186
|
+
suggest: suggestions,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// Type checking failed - silently skip this check
|
|
191
|
+
// This can happen if TypeScript services are not available
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
exports.checkTypeBased = checkTypeBased;
|
|
196
|
+
//# sourceMappingURL=type-based.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type-based.js","sourceRoot":"","sources":["../../../../../../../../../libs/eslint/plugin-trackunit/src/lib/rules/prefer-event-specific-callback-naming/strategies/type-based.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Type-based detection strategy for prefer-event-specific-callback-naming rule.\n *\n * Uses TypeScript's type checker to detect when a callback with problematic naming\n * is passed to a prop typed as a mouse event handler (MouseEventHandler, MouseEvent, etc.).\n *\n * This catches cases where custom components use non-standard prop names for click handlers:\n * - <MobileButton onTap={onClose} /> where onTap is typed as MouseEventHandler\n * - <PressableArea onPress={onCancel} /> where onPress accepts MouseEvent\n */\n\nimport { AST_NODE_TYPES, ESLintUtils, TSESLint, TSESTree } from \"@typescript-eslint/utils\";\nimport * as ts from \"typescript\";\n\nimport { extractCallbackIdentifier, findLocalDefinitionInfo, getSuggestedNameForEvent } from \"../utils\";\n\nexport type TypeBasedMessageIds =\n | \"preferOnClickNamingTyped\"\n | \"preferOnClickNamingTypedWrapped\"\n | \"suggestRenameAllTyped\"\n | \"suggestRenameLocalTyped\";\n\nexport const typeBasedMessages: Record<TypeBasedMessageIds, string> = {\n preferOnClickNamingTyped:\n \"Callback '{{current}}' passed to {{event}} should use '{{expectedPrefix}}*' naming (e.g., '{{suggested}}'). \" +\n \"Reported because {{event}} is typed as a MouseEventHandler. Rename it at its definition.\",\n preferOnClickNamingTypedWrapped:\n \"Callback '{{current}}' called in {{event}} should use '{{expectedPrefix}}*' naming (e.g., '{{suggested}}'). \" +\n \"Reported because {{event}} is typed as a MouseEventHandler. Rename it at its definition.\",\n suggestRenameAllTyped: \"Rename '{{current}}' to '{{suggested}}' (definition and all usages in this file)\",\n suggestRenameLocalTyped: \"Rename to '{{suggested}}' (this reference only - definition is in another file)\",\n};\n\n/**\n * Extracts the expected naming prefix from a suggested name.\n * This is used to show the correct expected pattern in error messages.\n *\n * @example\n * getExpectedPrefix(\"onClickPrimary\") // \"onClick\"\n * getExpectedPrefix(\"onMouseDownClose\") // \"onMouseDown\"\n */\nconst getExpectedPrefix = (suggestedName: string): string => {\n // Find where the capitalized suffix starts (after the event prefix)\n // e.g., \"onClickPrimary\" -> find \"Primary\" -> prefix is \"onClick\"\n const match = suggestedName.match(/^(on[A-Z][a-z]*)/);\n return match?.[1] ?? suggestedName;\n};\n\ntype SuggestionDescriptor = {\n messageId: TypeBasedMessageIds;\n data: { current: string; suggested: string };\n fix: (fixer: TSESLint.RuleFixer) => TSESLint.RuleFix | ReadonlyArray<TSESLint.RuleFix>;\n};\n\n/**\n * Checks if a type represents a mouse event handler.\n * Looks for:\n * - MouseEventHandler<T>\n * - React.MouseEventHandler<T>\n * - Function types with MouseEvent as first parameter\n */\nconst isMouseEventHandlerType = (type: ts.Type, checker: ts.TypeChecker): boolean => {\n const typeString = checker.typeToString(type);\n\n // Quick check for common patterns in the type string\n if (\n typeString.includes(\"MouseEventHandler\") ||\n typeString.includes(\"MouseEvent<\") ||\n typeString.includes(\"MouseEvent)\")\n ) {\n return true;\n }\n\n // Check call signatures for MouseEvent parameter\n const signatures = type.getCallSignatures();\n for (const signature of signatures) {\n const params = signature.getParameters();\n if (params.length > 0) {\n const firstParam = params[0];\n if (firstParam) {\n const paramType = checker.getTypeOfSymbol(firstParam);\n const paramTypeString = checker.typeToString(paramType);\n\n if (paramTypeString.includes(\"MouseEvent\")) {\n return true;\n }\n }\n }\n }\n\n return false;\n};\n\n/**\n * Checks a JSX attribute for type-based mouse event handler naming issues.\n *\n * Only runs when \"onClick\" is in the events list (since this check only detects\n * MouseEventHandler types, which are click events).\n *\n * Skips props already in the events list (those are handled by the name-based strategy).\n *\n * @param node - The JSX attribute node to check\n * @param context - The ESLint rule context\n * @param allowedNames - Callback names to exempt from this rule\n * @param events - Configured event names (used to skip already-covered props)\n */\nexport const checkTypeBased = (\n node: TSESTree.JSXAttribute,\n context: TSESLint.RuleContext<TypeBasedMessageIds, ReadonlyArray<unknown>>,\n allowedNames: ReadonlyArray<string>,\n events: ReadonlyArray<string>\n): void => {\n // Only run if \"onClick\" is in the events list, since this check only detects\n // MouseEventHandler types (which are click events)\n if (!events.includes(\"onClick\")) {\n return;\n }\n\n // Extract prop name - must be a JSXIdentifier\n if (node.name.type !== AST_NODE_TYPES.JSXIdentifier) {\n return;\n }\n\n const propName = node.name.name;\n\n // Skip props already in the events list - those are handled by the name-based strategy\n if (events.includes(propName)) {\n return;\n }\n\n // Must have a value that's an expression container\n if (!node.value || node.value.type !== AST_NODE_TYPES.JSXExpressionContainer) {\n return;\n }\n\n const { expression } = node.value;\n if (expression.type === AST_NODE_TYPES.JSXEmptyExpression) {\n return;\n }\n\n // Extract callback identifier (without checking hardcoded onClick pattern)\n const result = extractCallbackIdentifier(expression);\n\n if (!result) {\n return;\n }\n\n // Check if this name is in the allowed list\n if (allowedNames.includes(result.callbackName)) {\n return;\n }\n\n // Skip if callback name already matches or starts with the prop name\n // e.g., onMouseEnter={onMouseEnter} or onMouseEnter={onMouseEnterSomething}\n if (result.callbackName === propName || result.callbackName.startsWith(propName)) {\n return;\n }\n\n // Skip if callback doesn't start with \"on\" - we only care about on* callbacks\n // that might be confused with lifecycle/event callbacks\n if (!result.callbackName.startsWith(\"on\")) {\n return;\n }\n\n // Now check if the prop is typed as a mouse event handler\n try {\n const services = ESLintUtils.getParserServices(context);\n const checker = services.program.getTypeChecker();\n\n // Get the TypeScript node for the expression (already validated above)\n const tsExpressionNode = services.esTreeNodeToTSNodeMap.get(expression);\n\n // Get the contextual type (what type is expected at this position)\n // The ESTree to TS node map returns a generic node, but getContextualType\n // expects an Expression. This assertion is necessary for TypeScript API interop.\n // eslint-disable-next-line @trackunit/no-typescript-assertion\n const contextualType = checker.getContextualType(tsExpressionNode as ts.Expression);\n\n if (!contextualType) {\n return;\n }\n\n if (!isMouseEventHandlerType(contextualType, checker)) {\n return;\n }\n\n // Calculate suggested name using the actual prop name (e.g., onMouseDown -> onMouseDownClose)\n const suggestedName = getSuggestedNameForEvent(result.callbackName, propName);\n\n // Select the appropriate messageId based on whether the callback is wrapped\n const messageId: TypeBasedMessageIds = result.isWrapped\n ? \"preferOnClickNamingTypedWrapped\"\n : \"preferOnClickNamingTyped\";\n\n // Build suggestions based on whether the definition is in the same file\n const suggestions: Array<SuggestionDescriptor> = [];\n const localInfo = findLocalDefinitionInfo(result.identifierNode, context);\n\n if (localInfo) {\n // Definition is in this file - offer to rename all occurrences\n suggestions.push({\n messageId: \"suggestRenameAllTyped\",\n data: {\n current: result.callbackName,\n suggested: suggestedName,\n },\n fix: fixer => {\n return localInfo.allNodesToRename.map(nodeToRename => fixer.replaceText(nodeToRename, suggestedName));\n },\n });\n } else {\n // Definition is external - only offer to rename this reference\n suggestions.push({\n messageId: \"suggestRenameLocalTyped\",\n data: {\n current: result.callbackName,\n suggested: suggestedName,\n },\n fix: fixer => {\n return fixer.replaceText(result.identifierNode, suggestedName);\n },\n });\n }\n\n context.report({\n node: result.node,\n messageId,\n data: {\n current: result.callbackName,\n suggested: suggestedName,\n event: propName,\n expectedPrefix: getExpectedPrefix(suggestedName),\n },\n suggest: suggestions,\n });\n } catch {\n // Type checking failed - silently skip this check\n // This can happen if TypeScript services are not available\n return;\n }\n};\n"]}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for the prefer-event-specific-callback-naming rule.
|
|
3
|
+
* These functions handle the naming convention logic and callback extraction
|
|
4
|
+
* used by both string-based and type-based detection strategies.
|
|
5
|
+
*/
|
|
6
|
+
import { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
|
7
|
+
/**
|
|
8
|
+
* Result of extracting callback information from an expression.
|
|
9
|
+
* Does not include messageId - each strategy adds its own.
|
|
10
|
+
*/
|
|
11
|
+
export type CallbackExtractionResult = {
|
|
12
|
+
callbackName: string;
|
|
13
|
+
suggestedName: string;
|
|
14
|
+
node: TSESTree.Node;
|
|
15
|
+
/** The identifier node for the callback - used for fix ranges */
|
|
16
|
+
identifierNode: TSESTree.Identifier;
|
|
17
|
+
/** Whether the callback is wrapped in an arrow function */
|
|
18
|
+
isWrapped: boolean;
|
|
19
|
+
} | null;
|
|
20
|
+
/**
|
|
21
|
+
* Information about a locally defined variable, including its definition
|
|
22
|
+
* and all references within the same file.
|
|
23
|
+
*/
|
|
24
|
+
export type LocalDefinitionInfo = {
|
|
25
|
+
/** The identifier node where the variable is defined */
|
|
26
|
+
definitionNode: TSESTree.Identifier;
|
|
27
|
+
/** All nodes that need to be renamed (variable references + type property definitions) */
|
|
28
|
+
allNodesToRename: ReadonlyArray<TSESTree.Identifier>;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Finds the local definition and all references of a variable in the current file.
|
|
32
|
+
* Also finds TypeScript interface/type property definitions with the same name
|
|
33
|
+
* (for React component props that need to be renamed in both the type and usage).
|
|
34
|
+
* Returns null if the variable is not defined in the current file (e.g., imported).
|
|
35
|
+
*
|
|
36
|
+
* @param identifier - The identifier node to look up
|
|
37
|
+
* @param context - The ESLint rule context
|
|
38
|
+
* @returns LocalDefinitionInfo if defined locally, null if defined externally
|
|
39
|
+
*/
|
|
40
|
+
export declare const findLocalDefinitionInfo: (identifier: TSESTree.Identifier, context: TSESLint.RuleContext<string, ReadonlyArray<unknown>>) => LocalDefinitionInfo | null;
|
|
41
|
+
/**
|
|
42
|
+
* Result of extracting a callback identifier from an expression.
|
|
43
|
+
* Unlike CallbackExtractionResult, this doesn't include suggested names
|
|
44
|
+
* since those depend on the specific event handler being used.
|
|
45
|
+
*/
|
|
46
|
+
export type CallbackIdentifierResult = {
|
|
47
|
+
callbackName: string;
|
|
48
|
+
node: TSESTree.Node;
|
|
49
|
+
/** The identifier node for the callback - used for fix ranges */
|
|
50
|
+
identifierNode: TSESTree.Identifier;
|
|
51
|
+
/** Whether the callback is wrapped in an arrow function */
|
|
52
|
+
isWrapped: boolean;
|
|
53
|
+
} | null;
|
|
54
|
+
/**
|
|
55
|
+
* Extracts the callback identifier from an expression without checking naming patterns.
|
|
56
|
+
* Used by strict prop events check which has its own naming criteria.
|
|
57
|
+
*
|
|
58
|
+
* @returns Callback identifier info, or null if not a simple identifier/single-call arrow
|
|
59
|
+
*/
|
|
60
|
+
export declare const extractCallbackIdentifier: (expression: TSESTree.Expression) => CallbackIdentifierResult;
|
|
61
|
+
/**
|
|
62
|
+
* Generates the suggested name for a callback based on the event handler it's passed to.
|
|
63
|
+
*
|
|
64
|
+
* Uses a strategy-based approach to handle different naming patterns appropriately.
|
|
65
|
+
* See `name-suggestion-strategies.ts` for the individual strategies.
|
|
66
|
+
*
|
|
67
|
+
* @param currentName - The current callback name (e.g., "secondaryAction", "close")
|
|
68
|
+
* @param eventName - The event handler name (e.g., "onClick", "onSubmit", "primaryAction")
|
|
69
|
+
* @returns The suggested name (e.g., "onClickSecondaryAction", "onSubmitClose", "onClickPrimary")
|
|
70
|
+
* @example
|
|
71
|
+
* getSuggestedNameForEvent("close", "onClick") // "onClickClose"
|
|
72
|
+
* getSuggestedNameForEvent("onCancel", "onClick") // "onClickCancel"
|
|
73
|
+
* getSuggestedNameForEvent("onPrimary", "primaryAction") // "onClickPrimary"
|
|
74
|
+
* getSuggestedNameForEvent("onClose", "onClickClose") // "onClickClose"
|
|
75
|
+
*/
|
|
76
|
+
export declare const getSuggestedNameForEvent: (currentName: string, eventName: string) => string;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared utilities for the prefer-event-specific-callback-naming rule.
|
|
4
|
+
* These functions handle the naming convention logic and callback extraction
|
|
5
|
+
* used by both string-based and type-based detection strategies.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.getSuggestedNameForEvent = exports.extractCallbackIdentifier = exports.findLocalDefinitionInfo = void 0;
|
|
9
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
10
|
+
const name_suggestion_strategies_1 = require("./name-suggestion-strategies");
|
|
11
|
+
/**
|
|
12
|
+
* Recursively finds all TSPropertySignature nodes in the AST with the given name.
|
|
13
|
+
* This finds interface/type property definitions that need to be renamed along with the variable.
|
|
14
|
+
*/
|
|
15
|
+
const findTypePropertyDefinitions = (ast, propertyName) => {
|
|
16
|
+
const results = [];
|
|
17
|
+
const visit = (node) => {
|
|
18
|
+
// Find interface/type property signatures with matching name
|
|
19
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
|
|
20
|
+
node.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
21
|
+
node.key.name === propertyName) {
|
|
22
|
+
results.push(node.key);
|
|
23
|
+
}
|
|
24
|
+
// Recursively visit all child nodes
|
|
25
|
+
// Use Object.entries to iterate without needing type assertions
|
|
26
|
+
for (const [key, value] of Object.entries(node)) {
|
|
27
|
+
if (key === "parent")
|
|
28
|
+
continue; // Skip parent to avoid cycles
|
|
29
|
+
if (value !== null && typeof value === "object") {
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
for (const item of value) {
|
|
32
|
+
if (item !== null && typeof item === "object" && "type" in item) {
|
|
33
|
+
visit(item);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if ("type" in value) {
|
|
38
|
+
visit(value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
visit(ast);
|
|
44
|
+
return results;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Finds the local definition and all references of a variable in the current file.
|
|
48
|
+
* Also finds TypeScript interface/type property definitions with the same name
|
|
49
|
+
* (for React component props that need to be renamed in both the type and usage).
|
|
50
|
+
* Returns null if the variable is not defined in the current file (e.g., imported).
|
|
51
|
+
*
|
|
52
|
+
* @param identifier - The identifier node to look up
|
|
53
|
+
* @param context - The ESLint rule context
|
|
54
|
+
* @returns LocalDefinitionInfo if defined locally, null if defined externally
|
|
55
|
+
*/
|
|
56
|
+
const findLocalDefinitionInfo = (identifier, context) => {
|
|
57
|
+
const scope = context.sourceCode.getScope(identifier);
|
|
58
|
+
const variable = scope.references.find(ref => ref.identifier === identifier)?.resolved;
|
|
59
|
+
if (!variable) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
// Check if the variable has definition nodes in this file
|
|
63
|
+
const definitions = variable.defs;
|
|
64
|
+
if (definitions.length === 0) {
|
|
65
|
+
// No definitions found - likely a global or imported variable
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
// Get the first definition (typically there's only one)
|
|
69
|
+
const definition = definitions[0];
|
|
70
|
+
if (!definition) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
// The definition's name node is where the variable is defined
|
|
74
|
+
// It could be an Identifier, ArrayPattern, or ObjectPattern
|
|
75
|
+
// We only handle simple Identifier definitions
|
|
76
|
+
const definitionName = definition.name;
|
|
77
|
+
if (definitionName.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
// Collect all variable references (identifiers that refer to this variable)
|
|
81
|
+
const variableReferences = variable.references
|
|
82
|
+
.map(ref => ref.identifier)
|
|
83
|
+
.filter((node) => node.type === utils_1.AST_NODE_TYPES.Identifier);
|
|
84
|
+
// Also find TypeScript type property definitions with the same name
|
|
85
|
+
// This handles the case where a callback is destructured from props:
|
|
86
|
+
// interface Props { onCancel: () => void } <-- need to rename this too
|
|
87
|
+
// const { onCancel } = props; <-- the variable reference
|
|
88
|
+
const ast = context.sourceCode.ast;
|
|
89
|
+
const typePropertyDefinitions = findTypePropertyDefinitions(ast, identifier.name);
|
|
90
|
+
// Combine definition, variable references, and type property definitions
|
|
91
|
+
// Use a Set based on node range to deduplicate (needed because shorthand properties
|
|
92
|
+
// may share node ranges between definition and reference)
|
|
93
|
+
const seenRanges = new Set();
|
|
94
|
+
const allNodesToRename = [];
|
|
95
|
+
for (const node of [definitionName, ...variableReferences, ...typePropertyDefinitions]) {
|
|
96
|
+
const rangeKey = `${node.range[0]}-${node.range[1]}`;
|
|
97
|
+
if (!seenRanges.has(rangeKey)) {
|
|
98
|
+
seenRanges.add(rangeKey);
|
|
99
|
+
allNodesToRename.push(node);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
definitionNode: definitionName,
|
|
104
|
+
allNodesToRename,
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
exports.findLocalDefinitionInfo = findLocalDefinitionInfo;
|
|
108
|
+
/**
|
|
109
|
+
* Checks if a callback name is problematic when used with click handlers.
|
|
110
|
+
* A name is problematic if it starts with "on" but not "onClick".
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* isProblematicName("onClose") // true - should be onClickClose
|
|
114
|
+
* isProblematicName("onClickClose") // false - already correct
|
|
115
|
+
* isProblematicName("handleClose") // false - different convention
|
|
116
|
+
* isProblematicName("close") // false - doesn't start with "on"
|
|
117
|
+
*/
|
|
118
|
+
const isProblematicName = (name) => {
|
|
119
|
+
return name.startsWith("on") && !name.startsWith("onClick");
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Converts a problematic callback name to the suggested onClick* format.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* getSuggestedName("onClose") // "onClickClose"
|
|
126
|
+
* getSuggestedName("onCancel") // "onClickCancel"
|
|
127
|
+
*/
|
|
128
|
+
const getSuggestedName = (name) => {
|
|
129
|
+
// Remove "on" prefix and add "onClick" prefix
|
|
130
|
+
return `onClick${name.slice(2)}`;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Extracts callback info from a direct identifier pattern: onClick={onClose}
|
|
134
|
+
*/
|
|
135
|
+
const extractFromDirectIdentifier = (expression) => {
|
|
136
|
+
if (expression.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const callbackName = expression.name;
|
|
140
|
+
if (!isProblematicName(callbackName)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
callbackName,
|
|
145
|
+
suggestedName: getSuggestedName(callbackName),
|
|
146
|
+
node: expression,
|
|
147
|
+
identifierNode: expression,
|
|
148
|
+
isWrapped: false,
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Extracts callback info from a single-call arrow function: onClick={() => onClose()}
|
|
153
|
+
*/
|
|
154
|
+
const extractFromSingleCallArrow = (expression) => {
|
|
155
|
+
if (expression.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const { body } = expression;
|
|
159
|
+
// Only check if body is a single call expression (not a block statement)
|
|
160
|
+
if (body.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
// Only check if the callee is a simple identifier
|
|
164
|
+
if (body.callee.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const callbackName = body.callee.name;
|
|
168
|
+
if (!isProblematicName(callbackName)) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
callbackName,
|
|
173
|
+
suggestedName: getSuggestedName(callbackName),
|
|
174
|
+
node: body.callee,
|
|
175
|
+
identifierNode: body.callee,
|
|
176
|
+
isWrapped: true,
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Extracts callback information from an expression.
|
|
181
|
+
* Handles both direct identifiers (onClick={onClose}) and
|
|
182
|
+
* single-call arrow functions (onClick={() => onClose()}).
|
|
183
|
+
*
|
|
184
|
+
* @returns Callback extraction result without messageId, or null if no problematic callback found
|
|
185
|
+
*/
|
|
186
|
+
const extractCallbackInfo = (expression) => {
|
|
187
|
+
return extractFromDirectIdentifier(expression) ?? extractFromSingleCallArrow(expression);
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Extracts the callback identifier from an expression without checking naming patterns.
|
|
191
|
+
* Used by strict prop events check which has its own naming criteria.
|
|
192
|
+
*
|
|
193
|
+
* @returns Callback identifier info, or null if not a simple identifier/single-call arrow
|
|
194
|
+
*/
|
|
195
|
+
const extractCallbackIdentifier = (expression) => {
|
|
196
|
+
// Direct identifier: onClick={onClose}
|
|
197
|
+
if (expression.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
198
|
+
return {
|
|
199
|
+
callbackName: expression.name,
|
|
200
|
+
node: expression,
|
|
201
|
+
identifierNode: expression,
|
|
202
|
+
isWrapped: false,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// Single-call arrow function: onClick={() => onClose()}
|
|
206
|
+
if (expression.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
207
|
+
const { body } = expression;
|
|
208
|
+
// Only check if body is a single call expression (not a block statement)
|
|
209
|
+
if (body.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
// Only check if the callee is a simple identifier
|
|
213
|
+
if (body.callee.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
callbackName: body.callee.name,
|
|
218
|
+
node: body.callee,
|
|
219
|
+
identifierNode: body.callee,
|
|
220
|
+
isWrapped: true,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
};
|
|
225
|
+
exports.extractCallbackIdentifier = extractCallbackIdentifier;
|
|
226
|
+
/**
|
|
227
|
+
* Generates the suggested name for a callback based on the event handler it's passed to.
|
|
228
|
+
*
|
|
229
|
+
* Uses a strategy-based approach to handle different naming patterns appropriately.
|
|
230
|
+
* See `name-suggestion-strategies.ts` for the individual strategies.
|
|
231
|
+
*
|
|
232
|
+
* @param currentName - The current callback name (e.g., "secondaryAction", "close")
|
|
233
|
+
* @param eventName - The event handler name (e.g., "onClick", "onSubmit", "primaryAction")
|
|
234
|
+
* @returns The suggested name (e.g., "onClickSecondaryAction", "onSubmitClose", "onClickPrimary")
|
|
235
|
+
* @example
|
|
236
|
+
* getSuggestedNameForEvent("close", "onClick") // "onClickClose"
|
|
237
|
+
* getSuggestedNameForEvent("onCancel", "onClick") // "onClickCancel"
|
|
238
|
+
* getSuggestedNameForEvent("onPrimary", "primaryAction") // "onClickPrimary"
|
|
239
|
+
* getSuggestedNameForEvent("onClose", "onClickClose") // "onClickClose"
|
|
240
|
+
*/
|
|
241
|
+
const getSuggestedNameForEvent = (currentName, eventName) => {
|
|
242
|
+
return (0, name_suggestion_strategies_1.getSuggestedName)({ callbackName: currentName, eventName });
|
|
243
|
+
};
|
|
244
|
+
exports.getSuggestedNameForEvent = getSuggestedNameForEvent;
|
|
245
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../../../../../../libs/eslint/plugin-trackunit/src/lib/rules/prefer-event-specific-callback-naming/utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared utilities for the prefer-event-specific-callback-naming rule.\n * These functions handle the naming convention logic and callback extraction\n * used by both string-based and type-based detection strategies.\n */\n\nimport { AST_NODE_TYPES, TSESLint, TSESTree } from \"@typescript-eslint/utils\";\n\nimport { getSuggestedName as getStrategySuggestedName } from \"./name-suggestion-strategies\";\n\n/**\n * Result of extracting callback information from an expression.\n * Does not include messageId - each strategy adds its own.\n */\nexport type CallbackExtractionResult = {\n callbackName: string;\n suggestedName: string;\n node: TSESTree.Node;\n /** The identifier node for the callback - used for fix ranges */\n identifierNode: TSESTree.Identifier;\n /** Whether the callback is wrapped in an arrow function */\n isWrapped: boolean;\n} | null;\n\n/**\n * Information about a locally defined variable, including its definition\n * and all references within the same file.\n */\nexport type LocalDefinitionInfo = {\n /** The identifier node where the variable is defined */\n definitionNode: TSESTree.Identifier;\n /** All nodes that need to be renamed (variable references + type property definitions) */\n allNodesToRename: ReadonlyArray<TSESTree.Identifier>;\n};\n\n/**\n * Recursively finds all TSPropertySignature nodes in the AST with the given name.\n * This finds interface/type property definitions that need to be renamed along with the variable.\n */\nconst findTypePropertyDefinitions = (ast: TSESTree.Program, propertyName: string): Array<TSESTree.Identifier> => {\n const results: Array<TSESTree.Identifier> = [];\n\n const visit = (node: TSESTree.Node): void => {\n // Find interface/type property signatures with matching name\n if (\n node.type === AST_NODE_TYPES.TSPropertySignature &&\n node.key.type === AST_NODE_TYPES.Identifier &&\n node.key.name === propertyName\n ) {\n results.push(node.key);\n }\n\n // Recursively visit all child nodes\n // Use Object.entries to iterate without needing type assertions\n for (const [key, value] of Object.entries(node)) {\n if (key === \"parent\") continue; // Skip parent to avoid cycles\n\n if (value !== null && typeof value === \"object\") {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== null && typeof item === \"object\" && \"type\" in item) {\n visit(item);\n }\n }\n } else if (\"type\" in value) {\n visit(value);\n }\n }\n }\n };\n\n visit(ast);\n return results;\n};\n\n/**\n * Finds the local definition and all references of a variable in the current file.\n * Also finds TypeScript interface/type property definitions with the same name\n * (for React component props that need to be renamed in both the type and usage).\n * Returns null if the variable is not defined in the current file (e.g., imported).\n *\n * @param identifier - The identifier node to look up\n * @param context - The ESLint rule context\n * @returns LocalDefinitionInfo if defined locally, null if defined externally\n */\nexport const findLocalDefinitionInfo = (\n identifier: TSESTree.Identifier,\n context: TSESLint.RuleContext<string, ReadonlyArray<unknown>>\n): LocalDefinitionInfo | null => {\n const scope = context.sourceCode.getScope(identifier);\n const variable = scope.references.find(ref => ref.identifier === identifier)?.resolved;\n\n if (!variable) {\n return null;\n }\n\n // Check if the variable has definition nodes in this file\n const definitions = variable.defs;\n if (definitions.length === 0) {\n // No definitions found - likely a global or imported variable\n return null;\n }\n\n // Get the first definition (typically there's only one)\n const definition = definitions[0];\n if (!definition) {\n return null;\n }\n\n // The definition's name node is where the variable is defined\n // It could be an Identifier, ArrayPattern, or ObjectPattern\n // We only handle simple Identifier definitions\n const definitionName = definition.name;\n if (definitionName.type !== AST_NODE_TYPES.Identifier) {\n return null;\n }\n\n // Collect all variable references (identifiers that refer to this variable)\n const variableReferences = variable.references\n .map(ref => ref.identifier)\n .filter((node): node is TSESTree.Identifier => node.type === AST_NODE_TYPES.Identifier);\n\n // Also find TypeScript type property definitions with the same name\n // This handles the case where a callback is destructured from props:\n // interface Props { onCancel: () => void } <-- need to rename this too\n // const { onCancel } = props; <-- the variable reference\n const ast = context.sourceCode.ast;\n const typePropertyDefinitions = findTypePropertyDefinitions(ast, identifier.name);\n\n // Combine definition, variable references, and type property definitions\n // Use a Set based on node range to deduplicate (needed because shorthand properties\n // may share node ranges between definition and reference)\n const seenRanges = new Set<string>();\n const allNodesToRename: Array<TSESTree.Identifier> = [];\n\n for (const node of [definitionName, ...variableReferences, ...typePropertyDefinitions]) {\n const rangeKey = `${node.range[0]}-${node.range[1]}`;\n if (!seenRanges.has(rangeKey)) {\n seenRanges.add(rangeKey);\n allNodesToRename.push(node);\n }\n }\n\n return {\n definitionNode: definitionName,\n allNodesToRename,\n };\n};\n\n/**\n * Checks if a callback name is problematic when used with click handlers.\n * A name is problematic if it starts with \"on\" but not \"onClick\".\n *\n * @example\n * isProblematicName(\"onClose\") // true - should be onClickClose\n * isProblematicName(\"onClickClose\") // false - already correct\n * isProblematicName(\"handleClose\") // false - different convention\n * isProblematicName(\"close\") // false - doesn't start with \"on\"\n */\nconst isProblematicName = (name: string): boolean => {\n return name.startsWith(\"on\") && !name.startsWith(\"onClick\");\n};\n\n/**\n * Converts a problematic callback name to the suggested onClick* format.\n *\n * @example\n * getSuggestedName(\"onClose\") // \"onClickClose\"\n * getSuggestedName(\"onCancel\") // \"onClickCancel\"\n */\nconst getSuggestedName = (name: string): string => {\n // Remove \"on\" prefix and add \"onClick\" prefix\n return `onClick${name.slice(2)}`;\n};\n\n/**\n * Extracts callback info from a direct identifier pattern: onClick={onClose}\n */\nconst extractFromDirectIdentifier = (expression: TSESTree.Expression): CallbackExtractionResult => {\n if (expression.type !== AST_NODE_TYPES.Identifier) {\n return null;\n }\n\n const callbackName = expression.name;\n if (!isProblematicName(callbackName)) {\n return null;\n }\n\n return {\n callbackName,\n suggestedName: getSuggestedName(callbackName),\n node: expression,\n identifierNode: expression,\n isWrapped: false,\n };\n};\n\n/**\n * Extracts callback info from a single-call arrow function: onClick={() => onClose()}\n */\nconst extractFromSingleCallArrow = (expression: TSESTree.Expression): CallbackExtractionResult => {\n if (expression.type !== AST_NODE_TYPES.ArrowFunctionExpression) {\n return null;\n }\n\n const { body } = expression;\n\n // Only check if body is a single call expression (not a block statement)\n if (body.type !== AST_NODE_TYPES.CallExpression) {\n return null;\n }\n\n // Only check if the callee is a simple identifier\n if (body.callee.type !== AST_NODE_TYPES.Identifier) {\n return null;\n }\n\n const callbackName = body.callee.name;\n if (!isProblematicName(callbackName)) {\n return null;\n }\n\n return {\n callbackName,\n suggestedName: getSuggestedName(callbackName),\n node: body.callee,\n identifierNode: body.callee,\n isWrapped: true,\n };\n};\n\n/**\n * Extracts callback information from an expression.\n * Handles both direct identifiers (onClick={onClose}) and\n * single-call arrow functions (onClick={() => onClose()}).\n *\n * @returns Callback extraction result without messageId, or null if no problematic callback found\n */\nconst extractCallbackInfo = (expression: TSESTree.Expression): CallbackExtractionResult => {\n return extractFromDirectIdentifier(expression) ?? extractFromSingleCallArrow(expression);\n};\n\n/**\n * Result of extracting a callback identifier from an expression.\n * Unlike CallbackExtractionResult, this doesn't include suggested names\n * since those depend on the specific event handler being used.\n */\nexport type CallbackIdentifierResult = {\n callbackName: string;\n node: TSESTree.Node;\n /** The identifier node for the callback - used for fix ranges */\n identifierNode: TSESTree.Identifier;\n /** Whether the callback is wrapped in an arrow function */\n isWrapped: boolean;\n} | null;\n\n/**\n * Extracts the callback identifier from an expression without checking naming patterns.\n * Used by strict prop events check which has its own naming criteria.\n *\n * @returns Callback identifier info, or null if not a simple identifier/single-call arrow\n */\nexport const extractCallbackIdentifier = (expression: TSESTree.Expression): CallbackIdentifierResult => {\n // Direct identifier: onClick={onClose}\n if (expression.type === AST_NODE_TYPES.Identifier) {\n return {\n callbackName: expression.name,\n node: expression,\n identifierNode: expression,\n isWrapped: false,\n };\n }\n\n // Single-call arrow function: onClick={() => onClose()}\n if (expression.type === AST_NODE_TYPES.ArrowFunctionExpression) {\n const { body } = expression;\n\n // Only check if body is a single call expression (not a block statement)\n if (body.type !== AST_NODE_TYPES.CallExpression) {\n return null;\n }\n\n // Only check if the callee is a simple identifier\n if (body.callee.type !== AST_NODE_TYPES.Identifier) {\n return null;\n }\n\n return {\n callbackName: body.callee.name,\n node: body.callee,\n identifierNode: body.callee,\n isWrapped: true,\n };\n }\n\n return null;\n};\n\n/**\n * Generates the suggested name for a callback based on the event handler it's passed to.\n *\n * Uses a strategy-based approach to handle different naming patterns appropriately.\n * See `name-suggestion-strategies.ts` for the individual strategies.\n *\n * @param currentName - The current callback name (e.g., \"secondaryAction\", \"close\")\n * @param eventName - The event handler name (e.g., \"onClick\", \"onSubmit\", \"primaryAction\")\n * @returns The suggested name (e.g., \"onClickSecondaryAction\", \"onSubmitClose\", \"onClickPrimary\")\n * @example\n * getSuggestedNameForEvent(\"close\", \"onClick\") // \"onClickClose\"\n * getSuggestedNameForEvent(\"onCancel\", \"onClick\") // \"onClickCancel\"\n * getSuggestedNameForEvent(\"onPrimary\", \"primaryAction\") // \"onClickPrimary\"\n * getSuggestedNameForEvent(\"onClose\", \"onClickClose\") // \"onClickClose\"\n */\nexport const getSuggestedNameForEvent = (currentName: string, eventName: string): string => {\n return getStrategySuggestedName({ callbackName: currentName, eventName });\n};\n"]}
|