@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,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Strategy-based name suggestion system for the prefer-event-specific-callback-naming rule.
|
|
4
|
+
*
|
|
5
|
+
* This module provides a structured, extensible approach to generating suggested names
|
|
6
|
+
* for callback functions based on different naming patterns. Each strategy handles a
|
|
7
|
+
* specific type of problematic name and knows how to transform it correctly.
|
|
8
|
+
*
|
|
9
|
+
* Strategies are evaluated in order - the first matching strategy wins.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.testableStrategies = exports.getSuggestedName = void 0;
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Helper Functions
|
|
15
|
+
// =============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* Capitalizes the first letter of a string.
|
|
18
|
+
*
|
|
19
|
+
* @example capitalize("primary") // "Primary"
|
|
20
|
+
*/
|
|
21
|
+
const capitalize = (str) => {
|
|
22
|
+
if (str.length === 0)
|
|
23
|
+
return str;
|
|
24
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
25
|
+
};
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// Strategy Implementations
|
|
28
|
+
// =============================================================================
|
|
29
|
+
/**
|
|
30
|
+
* Strategy: Handle `*Action` suffix event names.
|
|
31
|
+
*
|
|
32
|
+
* When the event prop ends with "Action" (e.g., "primaryAction", "secondaryAction"),
|
|
33
|
+
* the prop itself is poorly named for a click handler. We extract the prefix and
|
|
34
|
+
* construct a proper onClick* name.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* - eventName: "primaryAction", callbackName: "onPrimary" → "onClickPrimary"
|
|
38
|
+
* - eventName: "secondaryAction", callbackName: "action" → "onClickSecondary"
|
|
39
|
+
* - eventName: "primaryAction", callbackName: "foo" → "onClickPrimary"
|
|
40
|
+
*/
|
|
41
|
+
const actionSuffixEventStrategy = {
|
|
42
|
+
name: "actionSuffixEventStrategy",
|
|
43
|
+
matches: (ctx) => {
|
|
44
|
+
// Matches when the event name ends with "Action" (case-sensitive)
|
|
45
|
+
return ctx.eventName.endsWith("Action");
|
|
46
|
+
},
|
|
47
|
+
suggest: (ctx) => {
|
|
48
|
+
// Extract the prefix before "Action" (e.g., "primary" from "primaryAction")
|
|
49
|
+
const prefix = ctx.eventName.slice(0, -"Action".length);
|
|
50
|
+
// If the callback starts with "on", extract its semantic part
|
|
51
|
+
// e.g., "onPrimary" → "Primary", "onConfirm" → "Confirm"
|
|
52
|
+
if (ctx.callbackName.startsWith("on") && ctx.callbackName.length > 2) {
|
|
53
|
+
const callbackSuffix = ctx.callbackName.slice(2);
|
|
54
|
+
// If the callback suffix matches the event prefix (case-insensitive),
|
|
55
|
+
// use the callback's casing for consistency
|
|
56
|
+
// e.g., eventName: "primaryAction", callbackName: "onPrimary" → "onClickPrimary"
|
|
57
|
+
if (callbackSuffix.toLowerCase() === prefix.toLowerCase()) {
|
|
58
|
+
return `onClick${callbackSuffix}`;
|
|
59
|
+
}
|
|
60
|
+
// Otherwise, combine both for clarity
|
|
61
|
+
// e.g., eventName: "primaryAction", callbackName: "onConfirm" → "onClickPrimaryConfirm"
|
|
62
|
+
return `onClick${capitalize(prefix)}${callbackSuffix}`;
|
|
63
|
+
}
|
|
64
|
+
// For non-"on" prefixed callbacks, just use the event prefix
|
|
65
|
+
// e.g., eventName: "primaryAction", callbackName: "foo" → "onClickPrimary"
|
|
66
|
+
return `onClick${capitalize(prefix)}`;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Strategy: Handle `*Handler` suffix event names.
|
|
71
|
+
*
|
|
72
|
+
* When the event prop ends with "Handler" (e.g., "clickHandler", "submitHandler"),
|
|
73
|
+
* the prop itself is poorly named. We extract the prefix and construct a proper onClick* name.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* - eventName: "clickHandler", callbackName: "onClose" → "onClickClose"
|
|
77
|
+
* - eventName: "submitHandler", callbackName: "foo" → "onClickSubmit"
|
|
78
|
+
*/
|
|
79
|
+
const handlerSuffixEventStrategy = {
|
|
80
|
+
name: "handlerSuffixEventStrategy",
|
|
81
|
+
matches: (ctx) => {
|
|
82
|
+
// Matches when the event name ends with "Handler" (case-sensitive)
|
|
83
|
+
return ctx.eventName.endsWith("Handler");
|
|
84
|
+
},
|
|
85
|
+
suggest: (ctx) => {
|
|
86
|
+
// Extract the prefix before "Handler" (e.g., "click" from "clickHandler")
|
|
87
|
+
const prefix = ctx.eventName.slice(0, -"Handler".length);
|
|
88
|
+
// If the callback starts with "on", extract its semantic part
|
|
89
|
+
if (ctx.callbackName.startsWith("on") && ctx.callbackName.length > 2) {
|
|
90
|
+
const callbackSuffix = ctx.callbackName.slice(2);
|
|
91
|
+
return `onClick${capitalize(prefix)}${callbackSuffix}`;
|
|
92
|
+
}
|
|
93
|
+
// For non-"on" prefixed callbacks
|
|
94
|
+
return `onClick${capitalize(prefix)}`;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Strategy: Handle `on*` callback → `onClick` event.
|
|
99
|
+
*
|
|
100
|
+
* When a callback starts with "on" (but not "onClick") and is passed to an onClick handler,
|
|
101
|
+
* we replace the "on" prefix with "onClick".
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* - eventName: "onClick", callbackName: "onClose" → "onClickClose"
|
|
105
|
+
* - eventName: "onClick", callbackName: "onCancel" → "onClickCancel"
|
|
106
|
+
*/
|
|
107
|
+
const onPrefixCallbackToClickEventStrategy = {
|
|
108
|
+
name: "onPrefixCallbackToClickEventStrategy",
|
|
109
|
+
matches: (ctx) => {
|
|
110
|
+
return ctx.eventName === "onClick" && ctx.callbackName.startsWith("on") && !ctx.callbackName.startsWith("onClick");
|
|
111
|
+
},
|
|
112
|
+
suggest: (ctx) => {
|
|
113
|
+
// Replace "on" with "onClick"
|
|
114
|
+
// e.g., "onClose" → "onClickClose"
|
|
115
|
+
const suffix = ctx.callbackName.slice(2);
|
|
116
|
+
return `onClick${suffix}`;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Strategy: Handle `on*` callback → generic `on*` event (not onClick).
|
|
121
|
+
*
|
|
122
|
+
* When a callback starts with "on" and is passed to a different on* event handler
|
|
123
|
+
* (like onMouseDown, onSubmit, etc.), we replace the "on" prefix with the event name.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* - eventName: "onMouseDown", callbackName: "onClose" → "onMouseDownClose"
|
|
127
|
+
* - eventName: "onSubmit", callbackName: "onConfirm" → "onSubmitConfirm"
|
|
128
|
+
*/
|
|
129
|
+
const onPrefixCallbackToGenericEventStrategy = {
|
|
130
|
+
name: "onPrefixCallbackToGenericEventStrategy",
|
|
131
|
+
matches: (ctx) => {
|
|
132
|
+
return (ctx.eventName.startsWith("on") &&
|
|
133
|
+
ctx.eventName !== "onClick" &&
|
|
134
|
+
ctx.callbackName.startsWith("on") &&
|
|
135
|
+
!ctx.callbackName.startsWith(ctx.eventName));
|
|
136
|
+
},
|
|
137
|
+
suggest: (ctx) => {
|
|
138
|
+
const callbackSuffix = ctx.callbackName.slice(2);
|
|
139
|
+
// Avoid duplication: if event name already ends with the suffix, just use the event name
|
|
140
|
+
// e.g., eventName: "onClickClose", callbackName: "onClose" → "onClickClose"
|
|
141
|
+
if (ctx.eventName.toLowerCase().endsWith(callbackSuffix.toLowerCase())) {
|
|
142
|
+
return ctx.eventName;
|
|
143
|
+
}
|
|
144
|
+
return `${ctx.eventName}${callbackSuffix}`;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Strategy: Default fallback.
|
|
149
|
+
*
|
|
150
|
+
* For any callback that doesn't match other patterns, capitalize and prepend
|
|
151
|
+
* the event name.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* - eventName: "onClick", callbackName: "myCallback" → "onClickMyCallback"
|
|
155
|
+
* - eventName: "onClick", callbackName: "close" → "onClickClose"
|
|
156
|
+
*/
|
|
157
|
+
const defaultCapitalizeStrategy = {
|
|
158
|
+
name: "defaultCapitalizeStrategy",
|
|
159
|
+
matches: () => {
|
|
160
|
+
// Always matches as a fallback
|
|
161
|
+
return true;
|
|
162
|
+
},
|
|
163
|
+
suggest: (ctx) => {
|
|
164
|
+
const capitalizedName = capitalize(ctx.callbackName);
|
|
165
|
+
return `${ctx.eventName}${capitalizedName}`;
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// Strategy Registry
|
|
170
|
+
// =============================================================================
|
|
171
|
+
/**
|
|
172
|
+
* Ordered list of strategies. First matching strategy wins.
|
|
173
|
+
*
|
|
174
|
+
* Order matters:
|
|
175
|
+
* 1. Specific patterns (Action suffix, Handler suffix) first
|
|
176
|
+
* 2. General patterns (on* prefix) next
|
|
177
|
+
* 3. Default fallback last
|
|
178
|
+
*/
|
|
179
|
+
const strategies = [
|
|
180
|
+
actionSuffixEventStrategy,
|
|
181
|
+
handlerSuffixEventStrategy,
|
|
182
|
+
onPrefixCallbackToClickEventStrategy,
|
|
183
|
+
onPrefixCallbackToGenericEventStrategy,
|
|
184
|
+
defaultCapitalizeStrategy,
|
|
185
|
+
];
|
|
186
|
+
// =============================================================================
|
|
187
|
+
// Public API
|
|
188
|
+
// =============================================================================
|
|
189
|
+
/**
|
|
190
|
+
* Generates a suggested name for a callback based on the event it's passed to.
|
|
191
|
+
*
|
|
192
|
+
* Uses a strategy pattern to handle different naming conventions appropriately.
|
|
193
|
+
* The first matching strategy determines the suggested name.
|
|
194
|
+
*
|
|
195
|
+
* @param ctx - The naming context with callback and event names
|
|
196
|
+
* @returns The suggested name for the callback
|
|
197
|
+
* @example
|
|
198
|
+
* getSuggestedName({ callbackName: "onClose", eventName: "onClick" })
|
|
199
|
+
* // Returns: "onClickClose"
|
|
200
|
+
*
|
|
201
|
+
* getSuggestedName({ callbackName: "onPrimary", eventName: "primaryAction" })
|
|
202
|
+
* // Returns: "onClickPrimary"
|
|
203
|
+
*/
|
|
204
|
+
const getSuggestedName = (ctx) => {
|
|
205
|
+
for (const strategy of strategies) {
|
|
206
|
+
if (strategy.matches(ctx)) {
|
|
207
|
+
return strategy.suggest(ctx);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Should never reach here since defaultCapitalizeStrategy always matches,
|
|
211
|
+
// but TypeScript doesn't know that
|
|
212
|
+
return defaultCapitalizeStrategy.suggest(ctx);
|
|
213
|
+
};
|
|
214
|
+
exports.getSuggestedName = getSuggestedName;
|
|
215
|
+
/**
|
|
216
|
+
* Exported for testing purposes - allows testing individual strategies.
|
|
217
|
+
*/
|
|
218
|
+
exports.testableStrategies = {
|
|
219
|
+
actionSuffixEventStrategy,
|
|
220
|
+
handlerSuffixEventStrategy,
|
|
221
|
+
onPrefixCallbackToClickEventStrategy,
|
|
222
|
+
onPrefixCallbackToGenericEventStrategy,
|
|
223
|
+
defaultCapitalizeStrategy,
|
|
224
|
+
};
|
|
225
|
+
//# sourceMappingURL=name-suggestion-strategies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"name-suggestion-strategies.js","sourceRoot":"","sources":["../../../../../../../../libs/eslint/plugin-trackunit/src/lib/rules/prefer-event-specific-callback-naming/name-suggestion-strategies.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Strategy-based name suggestion system for the prefer-event-specific-callback-naming rule.\n *\n * This module provides a structured, extensible approach to generating suggested names\n * for callback functions based on different naming patterns. Each strategy handles a\n * specific type of problematic name and knows how to transform it correctly.\n *\n * Strategies are evaluated in order - the first matching strategy wins.\n */\n\n/**\n * Context passed to name suggestion strategies.\n */\nexport type NameSuggestionContext = {\n /** The current callback name (e.g., \"onPrimary\", \"onClose\", \"handleClick\") */\n callbackName: string;\n /** The event handler prop name (e.g., \"onClick\", \"primaryAction\", \"onSubmit\") */\n eventName: string;\n};\n\n/**\n * A strategy for suggesting renamed callback names.\n * Each strategy handles a specific pattern of problematic naming.\n */\nexport type NameSuggestionStrategy = {\n /** Human-readable name for debugging and logging */\n name: string;\n /** Returns true if this strategy should handle the given context */\n matches: (ctx: NameSuggestionContext) => boolean;\n /** Returns the suggested name for the callback */\n suggest: (ctx: NameSuggestionContext) => string;\n};\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Capitalizes the first letter of a string.\n *\n * @example capitalize(\"primary\") // \"Primary\"\n */\nconst capitalize = (str: string): string => {\n if (str.length === 0) return str;\n return str.charAt(0).toUpperCase() + str.slice(1);\n};\n\n// =============================================================================\n// Strategy Implementations\n// =============================================================================\n\n/**\n * Strategy: Handle `*Action` suffix event names.\n *\n * When the event prop ends with \"Action\" (e.g., \"primaryAction\", \"secondaryAction\"),\n * the prop itself is poorly named for a click handler. We extract the prefix and\n * construct a proper onClick* name.\n *\n * @example\n * - eventName: \"primaryAction\", callbackName: \"onPrimary\" → \"onClickPrimary\"\n * - eventName: \"secondaryAction\", callbackName: \"action\" → \"onClickSecondary\"\n * - eventName: \"primaryAction\", callbackName: \"foo\" → \"onClickPrimary\"\n */\nconst actionSuffixEventStrategy: NameSuggestionStrategy = {\n name: \"actionSuffixEventStrategy\",\n\n matches: (ctx: NameSuggestionContext): boolean => {\n // Matches when the event name ends with \"Action\" (case-sensitive)\n return ctx.eventName.endsWith(\"Action\");\n },\n\n suggest: (ctx: NameSuggestionContext): string => {\n // Extract the prefix before \"Action\" (e.g., \"primary\" from \"primaryAction\")\n const prefix = ctx.eventName.slice(0, -\"Action\".length);\n\n // If the callback starts with \"on\", extract its semantic part\n // e.g., \"onPrimary\" → \"Primary\", \"onConfirm\" → \"Confirm\"\n if (ctx.callbackName.startsWith(\"on\") && ctx.callbackName.length > 2) {\n const callbackSuffix = ctx.callbackName.slice(2);\n\n // If the callback suffix matches the event prefix (case-insensitive),\n // use the callback's casing for consistency\n // e.g., eventName: \"primaryAction\", callbackName: \"onPrimary\" → \"onClickPrimary\"\n if (callbackSuffix.toLowerCase() === prefix.toLowerCase()) {\n return `onClick${callbackSuffix}`;\n }\n\n // Otherwise, combine both for clarity\n // e.g., eventName: \"primaryAction\", callbackName: \"onConfirm\" → \"onClickPrimaryConfirm\"\n return `onClick${capitalize(prefix)}${callbackSuffix}`;\n }\n\n // For non-\"on\" prefixed callbacks, just use the event prefix\n // e.g., eventName: \"primaryAction\", callbackName: \"foo\" → \"onClickPrimary\"\n return `onClick${capitalize(prefix)}`;\n },\n};\n\n/**\n * Strategy: Handle `*Handler` suffix event names.\n *\n * When the event prop ends with \"Handler\" (e.g., \"clickHandler\", \"submitHandler\"),\n * the prop itself is poorly named. We extract the prefix and construct a proper onClick* name.\n *\n * @example\n * - eventName: \"clickHandler\", callbackName: \"onClose\" → \"onClickClose\"\n * - eventName: \"submitHandler\", callbackName: \"foo\" → \"onClickSubmit\"\n */\nconst handlerSuffixEventStrategy: NameSuggestionStrategy = {\n name: \"handlerSuffixEventStrategy\",\n\n matches: (ctx: NameSuggestionContext): boolean => {\n // Matches when the event name ends with \"Handler\" (case-sensitive)\n return ctx.eventName.endsWith(\"Handler\");\n },\n\n suggest: (ctx: NameSuggestionContext): string => {\n // Extract the prefix before \"Handler\" (e.g., \"click\" from \"clickHandler\")\n const prefix = ctx.eventName.slice(0, -\"Handler\".length);\n\n // If the callback starts with \"on\", extract its semantic part\n if (ctx.callbackName.startsWith(\"on\") && ctx.callbackName.length > 2) {\n const callbackSuffix = ctx.callbackName.slice(2);\n return `onClick${capitalize(prefix)}${callbackSuffix}`;\n }\n\n // For non-\"on\" prefixed callbacks\n return `onClick${capitalize(prefix)}`;\n },\n};\n\n/**\n * Strategy: Handle `on*` callback → `onClick` event.\n *\n * When a callback starts with \"on\" (but not \"onClick\") and is passed to an onClick handler,\n * we replace the \"on\" prefix with \"onClick\".\n *\n * @example\n * - eventName: \"onClick\", callbackName: \"onClose\" → \"onClickClose\"\n * - eventName: \"onClick\", callbackName: \"onCancel\" → \"onClickCancel\"\n */\nconst onPrefixCallbackToClickEventStrategy: NameSuggestionStrategy = {\n name: \"onPrefixCallbackToClickEventStrategy\",\n\n matches: (ctx: NameSuggestionContext): boolean => {\n return ctx.eventName === \"onClick\" && ctx.callbackName.startsWith(\"on\") && !ctx.callbackName.startsWith(\"onClick\");\n },\n\n suggest: (ctx: NameSuggestionContext): string => {\n // Replace \"on\" with \"onClick\"\n // e.g., \"onClose\" → \"onClickClose\"\n const suffix = ctx.callbackName.slice(2);\n return `onClick${suffix}`;\n },\n};\n\n/**\n * Strategy: Handle `on*` callback → generic `on*` event (not onClick).\n *\n * When a callback starts with \"on\" and is passed to a different on* event handler\n * (like onMouseDown, onSubmit, etc.), we replace the \"on\" prefix with the event name.\n *\n * @example\n * - eventName: \"onMouseDown\", callbackName: \"onClose\" → \"onMouseDownClose\"\n * - eventName: \"onSubmit\", callbackName: \"onConfirm\" → \"onSubmitConfirm\"\n */\nconst onPrefixCallbackToGenericEventStrategy: NameSuggestionStrategy = {\n name: \"onPrefixCallbackToGenericEventStrategy\",\n\n matches: (ctx: NameSuggestionContext): boolean => {\n return (\n ctx.eventName.startsWith(\"on\") &&\n ctx.eventName !== \"onClick\" &&\n ctx.callbackName.startsWith(\"on\") &&\n !ctx.callbackName.startsWith(ctx.eventName)\n );\n },\n\n suggest: (ctx: NameSuggestionContext): string => {\n const callbackSuffix = ctx.callbackName.slice(2);\n\n // Avoid duplication: if event name already ends with the suffix, just use the event name\n // e.g., eventName: \"onClickClose\", callbackName: \"onClose\" → \"onClickClose\"\n if (ctx.eventName.toLowerCase().endsWith(callbackSuffix.toLowerCase())) {\n return ctx.eventName;\n }\n\n return `${ctx.eventName}${callbackSuffix}`;\n },\n};\n\n/**\n * Strategy: Default fallback.\n *\n * For any callback that doesn't match other patterns, capitalize and prepend\n * the event name.\n *\n * @example\n * - eventName: \"onClick\", callbackName: \"myCallback\" → \"onClickMyCallback\"\n * - eventName: \"onClick\", callbackName: \"close\" → \"onClickClose\"\n */\nconst defaultCapitalizeStrategy: NameSuggestionStrategy = {\n name: \"defaultCapitalizeStrategy\",\n\n matches: (): boolean => {\n // Always matches as a fallback\n return true;\n },\n\n suggest: (ctx: NameSuggestionContext): string => {\n const capitalizedName = capitalize(ctx.callbackName);\n return `${ctx.eventName}${capitalizedName}`;\n },\n};\n\n// =============================================================================\n// Strategy Registry\n// =============================================================================\n\n/**\n * Ordered list of strategies. First matching strategy wins.\n *\n * Order matters:\n * 1. Specific patterns (Action suffix, Handler suffix) first\n * 2. General patterns (on* prefix) next\n * 3. Default fallback last\n */\nconst strategies: ReadonlyArray<NameSuggestionStrategy> = [\n actionSuffixEventStrategy,\n handlerSuffixEventStrategy,\n onPrefixCallbackToClickEventStrategy,\n onPrefixCallbackToGenericEventStrategy,\n defaultCapitalizeStrategy,\n];\n\n// =============================================================================\n// Public API\n// =============================================================================\n\n/**\n * Generates a suggested name for a callback based on the event it's passed to.\n *\n * Uses a strategy pattern to handle different naming conventions appropriately.\n * The first matching strategy determines the suggested name.\n *\n * @param ctx - The naming context with callback and event names\n * @returns The suggested name for the callback\n * @example\n * getSuggestedName({ callbackName: \"onClose\", eventName: \"onClick\" })\n * // Returns: \"onClickClose\"\n *\n * getSuggestedName({ callbackName: \"onPrimary\", eventName: \"primaryAction\" })\n * // Returns: \"onClickPrimary\"\n */\nexport const getSuggestedName = (ctx: NameSuggestionContext): string => {\n for (const strategy of strategies) {\n if (strategy.matches(ctx)) {\n return strategy.suggest(ctx);\n }\n }\n\n // Should never reach here since defaultCapitalizeStrategy always matches,\n // but TypeScript doesn't know that\n return defaultCapitalizeStrategy.suggest(ctx);\n};\n\n/**\n * Exported for testing purposes - allows testing individual strategies.\n */\nexport const testableStrategies = {\n actionSuffixEventStrategy,\n handlerSuffixEventStrategy,\n onPrefixCallbackToClickEventStrategy,\n onPrefixCallbackToGenericEventStrategy,\n defaultCapitalizeStrategy,\n};\n"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: prefer-event-specific-callback-naming
|
|
3
|
+
*
|
|
4
|
+
* Enforces that callbacks passed to event handler attributes use matching
|
|
5
|
+
* event-specific naming (e.g., onClick* for onClick, onSubmit* for onSubmit).
|
|
6
|
+
*
|
|
7
|
+
* This prevents confusing names like `onClose` (sounds like "when close happens")
|
|
8
|
+
* when the callback is actually triggered by a click event.
|
|
9
|
+
*
|
|
10
|
+
* Uses two detection strategies:
|
|
11
|
+
* 1. Name-based: Checks configured event attributes for mismatched callback names
|
|
12
|
+
* 2. Type-based: Detects any prop typed as MouseEventHandler with problematic naming
|
|
13
|
+
* (only active when "onClick" is in the events list)
|
|
14
|
+
*
|
|
15
|
+
* Configuration via the `events` option controls which event handler attributes
|
|
16
|
+
* are checked by both strategies.
|
|
17
|
+
*/
|
|
18
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
19
|
+
import { NameBasedMessageIds } from "./strategies/string-based";
|
|
20
|
+
import { TypeBasedMessageIds } from "./strategies/type-based";
|
|
21
|
+
type Options = [
|
|
22
|
+
{
|
|
23
|
+
/**
|
|
24
|
+
* Callback names to exempt from this rule.
|
|
25
|
+
*
|
|
26
|
+
* @example ["onToggle", "onTrigger"]
|
|
27
|
+
*/
|
|
28
|
+
allowedNames?: ReadonlyArray<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Event handler attributes to check for matching callback naming.
|
|
31
|
+
*
|
|
32
|
+
* For each event in this list, callbacks passed to that attribute must use
|
|
33
|
+
* matching naming (e.g., `onClick*` for `onClick`, `onSubmit*` for `onSubmit`).
|
|
34
|
+
*
|
|
35
|
+
* - `on*` callbacks that don't match the event prefix are always flagged
|
|
36
|
+
* - Other callbacks (`handle*`, `close`, etc.) are only flagged when they come from props
|
|
37
|
+
* - When "onClick" is included, type-based MouseEventHandler detection is also enabled
|
|
38
|
+
*
|
|
39
|
+
* @default ["onClick"]
|
|
40
|
+
* @example ["onClick", "onSubmit", "onDoubleClick"]
|
|
41
|
+
*/
|
|
42
|
+
events?: ReadonlyArray<string>;
|
|
43
|
+
}
|
|
44
|
+
];
|
|
45
|
+
type MessageIds = NameBasedMessageIds | TypeBasedMessageIds;
|
|
46
|
+
export declare const preferEventSpecificCallbackNaming: ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
|
|
47
|
+
name: string;
|
|
48
|
+
};
|
|
49
|
+
export {};
|
package/src/lib/rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ESLint rule: prefer-event-specific-callback-naming
|
|
4
|
+
*
|
|
5
|
+
* Enforces that callbacks passed to event handler attributes use matching
|
|
6
|
+
* event-specific naming (e.g., onClick* for onClick, onSubmit* for onSubmit).
|
|
7
|
+
*
|
|
8
|
+
* This prevents confusing names like `onClose` (sounds like "when close happens")
|
|
9
|
+
* when the callback is actually triggered by a click event.
|
|
10
|
+
*
|
|
11
|
+
* Uses two detection strategies:
|
|
12
|
+
* 1. Name-based: Checks configured event attributes for mismatched callback names
|
|
13
|
+
* 2. Type-based: Detects any prop typed as MouseEventHandler with problematic naming
|
|
14
|
+
* (only active when "onClick" is in the events list)
|
|
15
|
+
*
|
|
16
|
+
* Configuration via the `events` option controls which event handler attributes
|
|
17
|
+
* are checked by both strategies.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.preferEventSpecificCallbackNaming = void 0;
|
|
21
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
22
|
+
const string_based_1 = require("./strategies/string-based");
|
|
23
|
+
const type_based_1 = require("./strategies/type-based");
|
|
24
|
+
const createRule = utils_1.ESLintUtils.RuleCreator(name => `https://github.com/trackunit/manager/blob/main/libs/eslint/plugin-trackunit/src/lib/rules/${name}/${name}.ts`);
|
|
25
|
+
exports.preferEventSpecificCallbackNaming = createRule({
|
|
26
|
+
name: "prefer-event-specific-callback-naming",
|
|
27
|
+
meta: {
|
|
28
|
+
type: "suggestion",
|
|
29
|
+
hasSuggestions: true,
|
|
30
|
+
docs: {
|
|
31
|
+
description: "Enforce using event-specific naming for callbacks passed to event handlers. " +
|
|
32
|
+
"Names like onClose are confusing because they sound like lifecycle events rather than click handlers.",
|
|
33
|
+
},
|
|
34
|
+
schema: [
|
|
35
|
+
{
|
|
36
|
+
type: "object",
|
|
37
|
+
properties: {
|
|
38
|
+
allowedNames: {
|
|
39
|
+
type: "array",
|
|
40
|
+
items: { type: "string" },
|
|
41
|
+
description: "Callback names to exempt from this rule",
|
|
42
|
+
},
|
|
43
|
+
events: {
|
|
44
|
+
type: "array",
|
|
45
|
+
items: { type: "string", pattern: "^on[A-Z]" },
|
|
46
|
+
description: "Event handler attributes to check (e.g., ['onClick', 'onSubmit']). " +
|
|
47
|
+
"Callbacks passed to these handlers must use matching on[Event]* naming.",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
messages: {
|
|
54
|
+
...string_based_1.nameBasedMessages,
|
|
55
|
+
...type_based_1.typeBasedMessages,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
defaultOptions: [
|
|
59
|
+
{
|
|
60
|
+
allowedNames: [],
|
|
61
|
+
events: ["onClick"],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
create(context, [options]) {
|
|
65
|
+
const allowedNames = options.allowedNames ?? [];
|
|
66
|
+
const events = options.events ?? ["onClick"];
|
|
67
|
+
return {
|
|
68
|
+
JSXAttribute(node) {
|
|
69
|
+
(0, string_based_1.checkNameBased)(node, context, allowedNames, events);
|
|
70
|
+
(0, type_based_1.checkTypeBased)(node, context, allowedNames, events);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
//# sourceMappingURL=prefer-event-specific-callback-naming.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-event-specific-callback-naming.js","sourceRoot":"","sources":["../../../../../../../../libs/eslint/plugin-trackunit/src/lib/rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;;AAEH,oDAAiE;AAEjE,4DAAmG;AACnG,wDAAiG;AAEjG,MAAM,UAAU,GAAG,mBAAW,CAAC,WAAW,CACxC,IAAI,CAAC,EAAE,CAAC,6FAA6F,IAAI,IAAI,IAAI,KAAK,CACvH,CAAC;AA8BW,QAAA,iCAAiC,GAAG,UAAU,CAAsB;IAC/E,IAAI,EAAE,uCAAuC;IAC7C,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,cAAc,EAAE,IAAI;QACpB,IAAI,EAAE;YACJ,WAAW,EACT,8EAA8E;gBAC9E,uGAAuG;SAC1G;QACD,MAAM,EAAE;YACN;gBACE,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,YAAY,EAAE;wBACZ,IAAI,EAAE,OAAO;wBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;wBACzB,WAAW,EAAE,yCAAyC;qBACvD;oBACD,MAAM,EAAE;wBACN,IAAI,EAAE,OAAO;wBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE;wBAC9C,WAAW,EACT,qEAAqE;4BACrE,yEAAyE;qBAC5E;iBACF;gBACD,oBAAoB,EAAE,KAAK;aAC5B;SACF;QACD,QAAQ,EAAE;YACR,GAAG,gCAAiB;YACpB,GAAG,8BAAiB;SACrB;KACF;IACD,cAAc,EAAE;QACd;YACE,YAAY,EAAE,EAAE;YAChB,MAAM,EAAE,CAAC,SAAS,CAAC;SACpB;KACF;IACD,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC;QACvB,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;QAChD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC;QAE7C,OAAO;YACL,YAAY,CAAC,IAA2B;gBACtC,IAAA,6BAAc,EAAC,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC;gBACpD,IAAA,2BAAc,EAAC,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC;YACtD,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC","sourcesContent":["/**\n * ESLint rule: prefer-event-specific-callback-naming\n *\n * Enforces that callbacks passed to event handler attributes use matching\n * event-specific naming (e.g., onClick* for onClick, onSubmit* for onSubmit).\n *\n * This prevents confusing names like `onClose` (sounds like \"when close happens\")\n * when the callback is actually triggered by a click event.\n *\n * Uses two detection strategies:\n * 1. Name-based: Checks configured event attributes for mismatched callback names\n * 2. Type-based: Detects any prop typed as MouseEventHandler with problematic naming\n * (only active when \"onClick\" is in the events list)\n *\n * Configuration via the `events` option controls which event handler attributes\n * are checked by both strategies.\n */\n\nimport { ESLintUtils, TSESTree } from \"@typescript-eslint/utils\";\n\nimport { checkNameBased, NameBasedMessageIds, nameBasedMessages } from \"./strategies/string-based\";\nimport { checkTypeBased, TypeBasedMessageIds, typeBasedMessages } from \"./strategies/type-based\";\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 * Callback names to exempt from this rule.\n *\n * @example [\"onToggle\", \"onTrigger\"]\n */\n allowedNames?: ReadonlyArray<string>;\n\n /**\n * Event handler attributes to check for matching callback naming.\n *\n * For each event in this list, callbacks passed to that attribute must use\n * matching naming (e.g., `onClick*` for `onClick`, `onSubmit*` for `onSubmit`).\n *\n * - `on*` callbacks that don't match the event prefix are always flagged\n * - Other callbacks (`handle*`, `close`, etc.) are only flagged when they come from props\n * - When \"onClick\" is included, type-based MouseEventHandler detection is also enabled\n *\n * @default [\"onClick\"]\n * @example [\"onClick\", \"onSubmit\", \"onDoubleClick\"]\n */\n events?: ReadonlyArray<string>;\n },\n];\n\ntype MessageIds = NameBasedMessageIds | TypeBasedMessageIds;\n\nexport const preferEventSpecificCallbackNaming = createRule<Options, MessageIds>({\n name: \"prefer-event-specific-callback-naming\",\n meta: {\n type: \"suggestion\",\n hasSuggestions: true,\n docs: {\n description:\n \"Enforce using event-specific naming for callbacks passed to event handlers. \" +\n \"Names like onClose are confusing because they sound like lifecycle events rather than click handlers.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n allowedNames: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Callback names to exempt from this rule\",\n },\n events: {\n type: \"array\",\n items: { type: \"string\", pattern: \"^on[A-Z]\" },\n description:\n \"Event handler attributes to check (e.g., ['onClick', 'onSubmit']). \" +\n \"Callbacks passed to these handlers must use matching on[Event]* naming.\",\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n ...nameBasedMessages,\n ...typeBasedMessages,\n },\n },\n defaultOptions: [\n {\n allowedNames: [],\n events: [\"onClick\"],\n },\n ],\n create(context, [options]) {\n const allowedNames = options.allowedNames ?? [];\n const events = options.events ?? [\"onClick\"];\n\n return {\n JSXAttribute(node: TSESTree.JSXAttribute) {\n checkNameBased(node, context, allowedNames, events);\n checkTypeBased(node, context, allowedNames, events);\n },\n };\n },\n});\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name-based detection strategy for prefer-event-specific-callback-naming rule.
|
|
3
|
+
*
|
|
4
|
+
* Checks JSX attributes whose names appear in the configured `events` list
|
|
5
|
+
* and flags callbacks that don't use matching event prefix naming.
|
|
6
|
+
*
|
|
7
|
+
* Two levels of strictness:
|
|
8
|
+
* - Callbacks starting with `on*` that don't match the event prefix are always flagged
|
|
9
|
+
* (e.g., onClick={onClose} - regardless of source)
|
|
10
|
+
* - Other callbacks (handle*, close, etc.) are only flagged when they come from props
|
|
11
|
+
* (not internal functions or hook results)
|
|
12
|
+
*/
|
|
13
|
+
import { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
|
14
|
+
export type NameBasedMessageIds = "nameBasedEventNaming" | "nameBasedEventNamingWrapped" | "suggestRenameAllNameBased" | "suggestRenameLocalNameBased";
|
|
15
|
+
export declare const nameBasedMessages: Record<NameBasedMessageIds, string>;
|
|
16
|
+
/**
|
|
17
|
+
* Unified name-based check for event handler callback naming.
|
|
18
|
+
*
|
|
19
|
+
* Checks whether a callback passed to a configured event attribute uses the correct
|
|
20
|
+
* naming convention. The check applies two levels of strictness:
|
|
21
|
+
*
|
|
22
|
+
* 1. `on*` callbacks that don't match the event prefix are always flagged
|
|
23
|
+
* (e.g., `onClick={onClose}` - should be `onClickClose`)
|
|
24
|
+
* 2. Other callbacks (`handle*`, `close`, etc.) are only flagged when they come from
|
|
25
|
+
* component props (not internal functions, hook results, or render prop callbacks)
|
|
26
|
+
*
|
|
27
|
+
* @param node - The JSX attribute node to check
|
|
28
|
+
* @param context - The ESLint rule context
|
|
29
|
+
* @param allowedNames - Callback names to exempt from this rule
|
|
30
|
+
* @param events - Event handler attribute names to check (e.g., ["onClick", "onSubmit"])
|
|
31
|
+
*/
|
|
32
|
+
export declare const checkNameBased: (node: TSESTree.JSXAttribute, context: TSESLint.RuleContext<NameBasedMessageIds, ReadonlyArray<unknown>>, allowedNames: ReadonlyArray<string>, events: ReadonlyArray<string>) => void;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Name-based detection strategy for prefer-event-specific-callback-naming rule.
|
|
4
|
+
*
|
|
5
|
+
* Checks JSX attributes whose names appear in the configured `events` list
|
|
6
|
+
* and flags callbacks that don't use matching event prefix naming.
|
|
7
|
+
*
|
|
8
|
+
* Two levels of strictness:
|
|
9
|
+
* - Callbacks starting with `on*` that don't match the event prefix are always flagged
|
|
10
|
+
* (e.g., onClick={onClose} - regardless of source)
|
|
11
|
+
* - Other callbacks (handle*, close, etc.) are only flagged when they come from props
|
|
12
|
+
* (not internal functions or hook results)
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.checkNameBased = exports.nameBasedMessages = void 0;
|
|
16
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
17
|
+
const ast_utils_1 = require("../../../utils/ast-utils");
|
|
18
|
+
const utils_2 = require("../utils");
|
|
19
|
+
exports.nameBasedMessages = {
|
|
20
|
+
nameBasedEventNaming: "Callback '{{current}}' passed to {{event}} should use '{{expectedPrefix}}*' naming (e.g., '{{suggested}}'). " +
|
|
21
|
+
"Reported because {{event}} is an explicitly monitored event handler name (see rule 'events' option). Rename it at its definition.",
|
|
22
|
+
nameBasedEventNamingWrapped: "Callback '{{current}}' called in {{event}} should use '{{expectedPrefix}}*' naming (e.g., '{{suggested}}'). " +
|
|
23
|
+
"Reported because {{event}} is an explicitly monitored event handler name (see rule 'events' option). Rename it at its definition.",
|
|
24
|
+
suggestRenameAllNameBased: "Rename '{{current}}' to '{{suggested}}' (definition and all usages in this file)",
|
|
25
|
+
suggestRenameLocalNameBased: "Rename to '{{suggested}}' (this reference only - definition is in another file)",
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Extracts the expected naming prefix from a suggested name.
|
|
29
|
+
* This is used to show the correct expected pattern in error messages.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* getExpectedPrefix("onClickPrimary") // "onClick"
|
|
33
|
+
* getExpectedPrefix("onMouseDownClose") // "onMouseDown"
|
|
34
|
+
*/
|
|
35
|
+
const getExpectedPrefix = (suggestedName) => {
|
|
36
|
+
const match = suggestedName.match(/^(on[A-Z][a-z]*)/);
|
|
37
|
+
return match?.[1] ?? suggestedName;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Unified name-based check for event handler callback naming.
|
|
41
|
+
*
|
|
42
|
+
* Checks whether a callback passed to a configured event attribute uses the correct
|
|
43
|
+
* naming convention. The check applies two levels of strictness:
|
|
44
|
+
*
|
|
45
|
+
* 1. `on*` callbacks that don't match the event prefix are always flagged
|
|
46
|
+
* (e.g., `onClick={onClose}` - should be `onClickClose`)
|
|
47
|
+
* 2. Other callbacks (`handle*`, `close`, etc.) are only flagged when they come from
|
|
48
|
+
* component props (not internal functions, hook results, or render prop callbacks)
|
|
49
|
+
*
|
|
50
|
+
* @param node - The JSX attribute node to check
|
|
51
|
+
* @param context - The ESLint rule context
|
|
52
|
+
* @param allowedNames - Callback names to exempt from this rule
|
|
53
|
+
* @param events - Event handler attribute names to check (e.g., ["onClick", "onSubmit"])
|
|
54
|
+
*/
|
|
55
|
+
const checkNameBased = (node, context, allowedNames, events) => {
|
|
56
|
+
// Must be a JSX identifier attribute
|
|
57
|
+
if (node.name.type !== utils_1.AST_NODE_TYPES.JSXIdentifier) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const eventName = node.name.name;
|
|
61
|
+
// Only check attributes in the events list
|
|
62
|
+
if (!events.includes(eventName)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Must have a value that's an expression container
|
|
66
|
+
if (!node.value || node.value.type !== utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const { expression } = node.value;
|
|
70
|
+
if (expression.type === utils_1.AST_NODE_TYPES.JSXEmptyExpression) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Extract callback identifier (without checking specific naming patterns)
|
|
74
|
+
const result = (0, utils_2.extractCallbackIdentifier)(expression);
|
|
75
|
+
if (!result) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Check if this name is in the allowed list
|
|
79
|
+
if (allowedNames.includes(result.callbackName)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Check if the name already matches the event pattern (e.g., onClick* for onClick)
|
|
83
|
+
if (result.callbackName.startsWith(eventName)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Determine if this is an on* callback that doesn't match the event prefix
|
|
87
|
+
const isOnPrefixMismatch = result.callbackName.startsWith("on") && !result.callbackName.startsWith(eventName);
|
|
88
|
+
if (!isOnPrefixMismatch) {
|
|
89
|
+
// For non-on* callbacks (handle*, close, etc.), only flag if they come from props
|
|
90
|
+
// but NOT from render prop callbacks (e.g., {close => <MenuItem onClick={close} />})
|
|
91
|
+
if (!(0, ast_utils_1.isIdentifierFromParameter)(result.identifierNode, context)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if ((0, ast_utils_1.isIdentifierFromRenderPropCallback)(result.identifierNode, context)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const suggestedName = (0, utils_2.getSuggestedNameForEvent)(result.callbackName, eventName);
|
|
99
|
+
// Select the appropriate messageId based on whether the callback is wrapped
|
|
100
|
+
const messageId = result.isWrapped ? "nameBasedEventNamingWrapped" : "nameBasedEventNaming";
|
|
101
|
+
// Build suggestions based on whether the definition is in the same file
|
|
102
|
+
const suggestions = [];
|
|
103
|
+
const localInfo = (0, utils_2.findLocalDefinitionInfo)(result.identifierNode, context);
|
|
104
|
+
if (localInfo) {
|
|
105
|
+
// Definition is in this file - offer to rename all occurrences
|
|
106
|
+
suggestions.push({
|
|
107
|
+
messageId: "suggestRenameAllNameBased",
|
|
108
|
+
data: {
|
|
109
|
+
current: result.callbackName,
|
|
110
|
+
suggested: suggestedName,
|
|
111
|
+
},
|
|
112
|
+
fix: fixer => {
|
|
113
|
+
return localInfo.allNodesToRename.map(nodeToRename => fixer.replaceText(nodeToRename, suggestedName));
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// Definition is external - only offer to rename this reference
|
|
119
|
+
suggestions.push({
|
|
120
|
+
messageId: "suggestRenameLocalNameBased",
|
|
121
|
+
data: {
|
|
122
|
+
current: result.callbackName,
|
|
123
|
+
suggested: suggestedName,
|
|
124
|
+
},
|
|
125
|
+
fix: fixer => {
|
|
126
|
+
return fixer.replaceText(result.identifierNode, suggestedName);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
context.report({
|
|
131
|
+
node: result.node,
|
|
132
|
+
messageId,
|
|
133
|
+
data: {
|
|
134
|
+
current: result.callbackName,
|
|
135
|
+
suggested: suggestedName,
|
|
136
|
+
event: eventName,
|
|
137
|
+
expectedPrefix: getExpectedPrefix(suggestedName),
|
|
138
|
+
},
|
|
139
|
+
suggest: suggestions,
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
exports.checkNameBased = checkNameBased;
|
|
143
|
+
//# sourceMappingURL=string-based.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"string-based.js","sourceRoot":"","sources":["../../../../../../../../../libs/eslint/plugin-trackunit/src/lib/rules/prefer-event-specific-callback-naming/strategies/string-based.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Name-based detection strategy for prefer-event-specific-callback-naming rule.\n *\n * Checks JSX attributes whose names appear in the configured `events` list\n * and flags callbacks that don't use matching event prefix naming.\n *\n * Two levels of strictness:\n * - Callbacks starting with `on*` that don't match the event prefix are always flagged\n * (e.g., onClick={onClose} - regardless of source)\n * - Other callbacks (handle*, close, etc.) are only flagged when they come from props\n * (not internal functions or hook results)\n */\n\nimport { AST_NODE_TYPES, TSESLint, TSESTree } from \"@typescript-eslint/utils\";\n\nimport { isIdentifierFromParameter, isIdentifierFromRenderPropCallback } from \"../../../utils/ast-utils\";\nimport { extractCallbackIdentifier, findLocalDefinitionInfo, getSuggestedNameForEvent } from \"../utils\";\n\nexport type NameBasedMessageIds =\n | \"nameBasedEventNaming\"\n | \"nameBasedEventNamingWrapped\"\n | \"suggestRenameAllNameBased\"\n | \"suggestRenameLocalNameBased\";\n\nexport const nameBasedMessages: Record<NameBasedMessageIds, string> = {\n nameBasedEventNaming:\n \"Callback '{{current}}' passed to {{event}} should use '{{expectedPrefix}}*' naming (e.g., '{{suggested}}'). \" +\n \"Reported because {{event}} is an explicitly monitored event handler name (see rule 'events' option). Rename it at its definition.\",\n nameBasedEventNamingWrapped:\n \"Callback '{{current}}' called in {{event}} should use '{{expectedPrefix}}*' naming (e.g., '{{suggested}}'). \" +\n \"Reported because {{event}} is an explicitly monitored event handler name (see rule 'events' option). Rename it at its definition.\",\n suggestRenameAllNameBased: \"Rename '{{current}}' to '{{suggested}}' (definition and all usages in this file)\",\n suggestRenameLocalNameBased: \"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 const match = suggestedName.match(/^(on[A-Z][a-z]*)/);\n return match?.[1] ?? suggestedName;\n};\n\ntype SuggestionDescriptor = {\n messageId: NameBasedMessageIds;\n data: { current: string; suggested: string };\n fix: (fixer: TSESLint.RuleFixer) => TSESLint.RuleFix | ReadonlyArray<TSESLint.RuleFix>;\n};\n\n/**\n * Unified name-based check for event handler callback naming.\n *\n * Checks whether a callback passed to a configured event attribute uses the correct\n * naming convention. The check applies two levels of strictness:\n *\n * 1. `on*` callbacks that don't match the event prefix are always flagged\n * (e.g., `onClick={onClose}` - should be `onClickClose`)\n * 2. Other callbacks (`handle*`, `close`, etc.) are only flagged when they come from\n * component props (not internal functions, hook results, or render prop callbacks)\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 - Event handler attribute names to check (e.g., [\"onClick\", \"onSubmit\"])\n */\nexport const checkNameBased = (\n node: TSESTree.JSXAttribute,\n context: TSESLint.RuleContext<NameBasedMessageIds, ReadonlyArray<unknown>>,\n allowedNames: ReadonlyArray<string>,\n events: ReadonlyArray<string>\n): void => {\n // Must be a JSX identifier attribute\n if (node.name.type !== AST_NODE_TYPES.JSXIdentifier) {\n return;\n }\n\n const eventName = node.name.name;\n\n // Only check attributes in the events list\n if (!events.includes(eventName)) {\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 specific naming patterns)\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 // Check if the name already matches the event pattern (e.g., onClick* for onClick)\n if (result.callbackName.startsWith(eventName)) {\n return;\n }\n\n // Determine if this is an on* callback that doesn't match the event prefix\n const isOnPrefixMismatch = result.callbackName.startsWith(\"on\") && !result.callbackName.startsWith(eventName);\n\n if (!isOnPrefixMismatch) {\n // For non-on* callbacks (handle*, close, etc.), only flag if they come from props\n // but NOT from render prop callbacks (e.g., {close => <MenuItem onClick={close} />})\n if (!isIdentifierFromParameter(result.identifierNode, context)) {\n return;\n }\n if (isIdentifierFromRenderPropCallback(result.identifierNode, context)) {\n return;\n }\n }\n\n const suggestedName = getSuggestedNameForEvent(result.callbackName, eventName);\n\n // Select the appropriate messageId based on whether the callback is wrapped\n const messageId: NameBasedMessageIds = result.isWrapped ? \"nameBasedEventNamingWrapped\" : \"nameBasedEventNaming\";\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: \"suggestRenameAllNameBased\",\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: \"suggestRenameLocalNameBased\",\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: eventName,\n expectedPrefix: getExpectedPrefix(suggestedName),\n },\n suggest: suggestions,\n });\n};\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-based detection strategy for prefer-event-specific-callback-naming rule.
|
|
3
|
+
*
|
|
4
|
+
* Uses TypeScript's type checker to detect when a callback with problematic naming
|
|
5
|
+
* is passed to a prop typed as a mouse event handler (MouseEventHandler, MouseEvent, etc.).
|
|
6
|
+
*
|
|
7
|
+
* This catches cases where custom components use non-standard prop names for click handlers:
|
|
8
|
+
* - <MobileButton onTap={onClose} /> where onTap is typed as MouseEventHandler
|
|
9
|
+
* - <PressableArea onPress={onCancel} /> where onPress accepts MouseEvent
|
|
10
|
+
*/
|
|
11
|
+
import { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
|
12
|
+
export type TypeBasedMessageIds = "preferOnClickNamingTyped" | "preferOnClickNamingTypedWrapped" | "suggestRenameAllTyped" | "suggestRenameLocalTyped";
|
|
13
|
+
export declare const typeBasedMessages: Record<TypeBasedMessageIds, string>;
|
|
14
|
+
/**
|
|
15
|
+
* Checks a JSX attribute for type-based mouse event handler naming issues.
|
|
16
|
+
*
|
|
17
|
+
* Only runs when "onClick" is in the events list (since this check only detects
|
|
18
|
+
* MouseEventHandler types, which are click events).
|
|
19
|
+
*
|
|
20
|
+
* Skips props already in the events list (those are handled by the name-based strategy).
|
|
21
|
+
*
|
|
22
|
+
* @param node - The JSX attribute node to check
|
|
23
|
+
* @param context - The ESLint rule context
|
|
24
|
+
* @param allowedNames - Callback names to exempt from this rule
|
|
25
|
+
* @param events - Configured event names (used to skip already-covered props)
|
|
26
|
+
*/
|
|
27
|
+
export declare const checkTypeBased: (node: TSESTree.JSXAttribute, context: TSESLint.RuleContext<TypeBasedMessageIds, ReadonlyArray<unknown>>, allowedNames: ReadonlyArray<string>, events: ReadonlyArray<string>) => void;
|