eslint-plugin-sweepit 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/index.cjs +2769 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2732 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2732 @@
|
|
|
1
|
+
// src/configs/react.ts
|
|
2
|
+
import tsParser from "@typescript-eslint/parser";
|
|
3
|
+
import tsEslintPlugin from "@typescript-eslint/eslint-plugin";
|
|
4
|
+
import reactPlugin from "eslint-plugin-react";
|
|
5
|
+
import reactHooksPlugin from "eslint-plugin-react-hooks";
|
|
6
|
+
import reactNoEffectPlugin from "eslint-plugin-react-you-might-not-need-an-effect";
|
|
7
|
+
function createReactConfig(sweepitPlugin) {
|
|
8
|
+
const reactConfig = {
|
|
9
|
+
plugins: {
|
|
10
|
+
sweepit: sweepitPlugin,
|
|
11
|
+
react: reactPlugin,
|
|
12
|
+
"react-hooks": reactHooksPlugin,
|
|
13
|
+
"react-you-might-not-need-an-effect": reactNoEffectPlugin,
|
|
14
|
+
"@typescript-eslint": tsEslintPlugin
|
|
15
|
+
},
|
|
16
|
+
languageOptions: {
|
|
17
|
+
parser: tsParser,
|
|
18
|
+
parserOptions: {
|
|
19
|
+
ecmaVersion: "latest",
|
|
20
|
+
sourceType: "module",
|
|
21
|
+
ecmaFeatures: {
|
|
22
|
+
jsx: true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
settings: {
|
|
27
|
+
react: {
|
|
28
|
+
version: "detect"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
rules: {
|
|
32
|
+
"react/jsx-handler-names": [
|
|
33
|
+
"error",
|
|
34
|
+
{
|
|
35
|
+
eventHandlerPrefix: "handle",
|
|
36
|
+
eventHandlerPropPrefix: "on"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"react/jsx-no-constructed-context-values": "error",
|
|
40
|
+
"react/jsx-no-useless-fragment": "error",
|
|
41
|
+
"react/jsx-pascal-case": "error",
|
|
42
|
+
"react/no-unstable-nested-components": "error",
|
|
43
|
+
"react-hooks/rules-of-hooks": "error",
|
|
44
|
+
"react-hooks/exhaustive-deps": "error",
|
|
45
|
+
"react-you-might-not-need-an-effect/no-effect": "error",
|
|
46
|
+
"sweepit/no-title-case-props": "error",
|
|
47
|
+
"sweepit/no-custom-kebab-case-props": "error",
|
|
48
|
+
"sweepit/no-set-prefix-utils": "error",
|
|
49
|
+
"sweepit/no-useless-hook": "error",
|
|
50
|
+
"sweepit/no-hook-jsx": "error",
|
|
51
|
+
"sweepit/no-exported-context-hooks": "error",
|
|
52
|
+
"sweepit/no-handler-return-type": "error",
|
|
53
|
+
"sweepit/jsx-server-action-prop-suffix": "error",
|
|
54
|
+
"sweepit/jsx-on-handler-verb-suffix": "error",
|
|
55
|
+
"sweepit/no-render-helper-functions": "error",
|
|
56
|
+
"sweepit/no-element-props": "error",
|
|
57
|
+
"sweepit/no-componenttype-props": "error",
|
|
58
|
+
"sweepit/no-object-props": "error",
|
|
59
|
+
"sweepit/no-array-props": "error",
|
|
60
|
+
"sweepit/no-prefixed-prop-bundles": "error",
|
|
61
|
+
"sweepit/no-optional-props-without-defaults": "error",
|
|
62
|
+
"sweepit/no-boolean-capability-props": "error",
|
|
63
|
+
"sweepit/max-custom-props": "error",
|
|
64
|
+
"sweepit/jsx-bem-compound-naming": "error",
|
|
65
|
+
"sweepit/jsx-compound-part-export-naming": "error",
|
|
66
|
+
"sweepit/no-pass-through-props": "error",
|
|
67
|
+
"sweepit/jsx-flat-owner-tree": "error"
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return [reactConfig];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/rules/no-title-case-props.ts
|
|
74
|
+
function getPropName(node) {
|
|
75
|
+
if (node.type === "JSXIdentifier") {
|
|
76
|
+
return node.name;
|
|
77
|
+
}
|
|
78
|
+
if (node.type === "JSXNamespacedName") {
|
|
79
|
+
return `${node.namespace.name}:${node.name.name}`;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
function isTitleCase(name) {
|
|
84
|
+
if (name.length === 0) return false;
|
|
85
|
+
const first = name[0];
|
|
86
|
+
return first >= "A" && first <= "Z";
|
|
87
|
+
}
|
|
88
|
+
var rule = {
|
|
89
|
+
meta: {
|
|
90
|
+
type: "suggestion",
|
|
91
|
+
docs: {
|
|
92
|
+
description: "Disallow TitleCase JSX props",
|
|
93
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-title-case-props.md"
|
|
94
|
+
},
|
|
95
|
+
messages: {
|
|
96
|
+
noTitleCase: "Prop '{{prop}}' uses TitleCase. Use camelCase instead (e.g. {{suggestion}}). See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-title-case-props.md."
|
|
97
|
+
},
|
|
98
|
+
schema: []
|
|
99
|
+
},
|
|
100
|
+
create(context) {
|
|
101
|
+
return {
|
|
102
|
+
JSXAttribute(node) {
|
|
103
|
+
const attrName = node.name;
|
|
104
|
+
const propName = getPropName(attrName);
|
|
105
|
+
if (!propName || !isTitleCase(propName)) return;
|
|
106
|
+
const suggestion = propName.length > 1 ? propName[0].toLowerCase() + propName.slice(1) : propName.toLowerCase();
|
|
107
|
+
context.report({
|
|
108
|
+
node: node.name,
|
|
109
|
+
messageId: "noTitleCase",
|
|
110
|
+
data: { prop: propName, suggestion }
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var no_title_case_props_default = rule;
|
|
117
|
+
|
|
118
|
+
// src/rules/no-custom-kebab-case-props.ts
|
|
119
|
+
var DEFAULT_ALLOWED_KEBAB_PREFIXES = ["aria-", "data-"];
|
|
120
|
+
function getPropName2(node) {
|
|
121
|
+
if (node.type === "JSXIdentifier") {
|
|
122
|
+
return node.name;
|
|
123
|
+
}
|
|
124
|
+
if (node.type === "JSXNamespacedName") {
|
|
125
|
+
return `${node.namespace.name}:${node.name.name}`;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
function isKebabCase(name) {
|
|
130
|
+
return name.includes("-");
|
|
131
|
+
}
|
|
132
|
+
function kebabToCamelCase(name) {
|
|
133
|
+
const segments = name.split("-").filter(Boolean);
|
|
134
|
+
if (segments.length === 0) return name;
|
|
135
|
+
const first = segments[0] ?? "";
|
|
136
|
+
const tail = segments.slice(1).map((segment) => segment[0]?.toUpperCase() + segment.slice(1)).join("");
|
|
137
|
+
return `${first}${tail}`;
|
|
138
|
+
}
|
|
139
|
+
function resolveRuleOptions(context) {
|
|
140
|
+
const option = context.options[0] ?? {};
|
|
141
|
+
const allowedPrefixes = Array.from(
|
|
142
|
+
/* @__PURE__ */ new Set([...DEFAULT_ALLOWED_KEBAB_PREFIXES, ...option.extendPrefixes ?? []])
|
|
143
|
+
);
|
|
144
|
+
const allowedProps = new Set(option.allowedProps ?? []);
|
|
145
|
+
return {
|
|
146
|
+
allowedPrefixes,
|
|
147
|
+
allowedProps
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function isAllowedKebabProp(name, options) {
|
|
151
|
+
if (options.allowedProps.has(name)) return true;
|
|
152
|
+
return options.allowedPrefixes.some((prefix) => name.startsWith(prefix));
|
|
153
|
+
}
|
|
154
|
+
var rule2 = {
|
|
155
|
+
meta: {
|
|
156
|
+
type: "suggestion",
|
|
157
|
+
docs: {
|
|
158
|
+
description: "Disallow custom kebab-case JSX props (allows aria-* and data-* by default with configurable additions)",
|
|
159
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-custom-kebab-case-props.md"
|
|
160
|
+
},
|
|
161
|
+
messages: {
|
|
162
|
+
noCustomKebab: "Custom kebab-case prop '{{prop}}' is not allowed. Rename to camelCase (for example '{{suggestion}}'), or keep kebab-case only for allowed native/custom prefixes. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-custom-kebab-case-props.md."
|
|
163
|
+
},
|
|
164
|
+
schema: [
|
|
165
|
+
{
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {
|
|
168
|
+
extendPrefixes: {
|
|
169
|
+
type: "array",
|
|
170
|
+
items: { type: "string" }
|
|
171
|
+
},
|
|
172
|
+
allowedProps: {
|
|
173
|
+
type: "array",
|
|
174
|
+
items: { type: "string" }
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
additionalProperties: false
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
create(context) {
|
|
182
|
+
const options = resolveRuleOptions(context);
|
|
183
|
+
return {
|
|
184
|
+
JSXAttribute(node) {
|
|
185
|
+
const attrName = node.name;
|
|
186
|
+
const propName = getPropName2(attrName);
|
|
187
|
+
if (!propName || !isKebabCase(propName)) return;
|
|
188
|
+
if (isAllowedKebabProp(propName, options)) return;
|
|
189
|
+
context.report({
|
|
190
|
+
node: node.name,
|
|
191
|
+
messageId: "noCustomKebab",
|
|
192
|
+
data: { prop: propName, suggestion: kebabToCamelCase(propName) }
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
var no_custom_kebab_case_props_default = rule2;
|
|
199
|
+
|
|
200
|
+
// src/rules/no-set-prefix-utils.ts
|
|
201
|
+
var SET_PREFIX = "set";
|
|
202
|
+
function startsWithSet(name) {
|
|
203
|
+
return name.startsWith(SET_PREFIX) && name.length > SET_PREFIX.length;
|
|
204
|
+
}
|
|
205
|
+
function isUseStateCall(node) {
|
|
206
|
+
if (!node || node.type !== "CallExpression") return false;
|
|
207
|
+
const call = node;
|
|
208
|
+
const callee = call.callee;
|
|
209
|
+
if (callee.type === "Identifier") {
|
|
210
|
+
return callee.name === "useState";
|
|
211
|
+
}
|
|
212
|
+
if (callee.type === "MemberExpression") {
|
|
213
|
+
const mem = callee;
|
|
214
|
+
return mem.property.type === "Identifier" && mem.property.name === "useState";
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
function isFunctionInit(node) {
|
|
219
|
+
if (!node) return false;
|
|
220
|
+
return node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
|
|
221
|
+
}
|
|
222
|
+
var rule3 = {
|
|
223
|
+
meta: {
|
|
224
|
+
type: "suggestion",
|
|
225
|
+
docs: {
|
|
226
|
+
description: "Forbid util/helper functions prefixed with set*; allow React state setter identifiers from useState tuple destructuring",
|
|
227
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-set-prefix-utils.md"
|
|
228
|
+
},
|
|
229
|
+
messages: {
|
|
230
|
+
noSetPrefixUtil: "Util/helper function '{{name}}' should not use set* prefix. Reserve set* for React useState setters. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-set-prefix-utils.md."
|
|
231
|
+
},
|
|
232
|
+
schema: []
|
|
233
|
+
},
|
|
234
|
+
create(context) {
|
|
235
|
+
return {
|
|
236
|
+
FunctionDeclaration(node) {
|
|
237
|
+
const fn = node;
|
|
238
|
+
if (!fn.id || !startsWithSet(fn.id.name)) return;
|
|
239
|
+
context.report({
|
|
240
|
+
node: fn.id,
|
|
241
|
+
messageId: "noSetPrefixUtil",
|
|
242
|
+
data: { name: fn.id.name }
|
|
243
|
+
});
|
|
244
|
+
},
|
|
245
|
+
VariableDeclarator(node) {
|
|
246
|
+
const decl = node;
|
|
247
|
+
const { id, init } = decl;
|
|
248
|
+
if (id.type === "ArrayPattern") {
|
|
249
|
+
const arr = id;
|
|
250
|
+
if (arr.elements.length >= 2 && arr.elements[1]?.type === "Identifier" && isUseStateCall(init)) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (id.type === "Identifier" && startsWithSet(id.name) && isFunctionInit(init)) {
|
|
255
|
+
context.report({
|
|
256
|
+
node: id,
|
|
257
|
+
messageId: "noSetPrefixUtil",
|
|
258
|
+
data: { name: id.name }
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
var no_set_prefix_utils_default = rule3;
|
|
266
|
+
|
|
267
|
+
// src/rules/no-useless-hook.ts
|
|
268
|
+
var HOOK_NAMES = /* @__PURE__ */ new Set([
|
|
269
|
+
"useState",
|
|
270
|
+
"useEffect",
|
|
271
|
+
"useReducer",
|
|
272
|
+
"useRef",
|
|
273
|
+
"useContext",
|
|
274
|
+
"useLayoutEffect",
|
|
275
|
+
"useImperativeHandle",
|
|
276
|
+
"useDebugValue",
|
|
277
|
+
"useId",
|
|
278
|
+
"useSyncExternalStore",
|
|
279
|
+
"useTransition",
|
|
280
|
+
"useDeferredValue",
|
|
281
|
+
"useOptimistic",
|
|
282
|
+
"useActionState",
|
|
283
|
+
"useFormStatus"
|
|
284
|
+
]);
|
|
285
|
+
function isUseName(name) {
|
|
286
|
+
return name.startsWith("use") && name.length > 3;
|
|
287
|
+
}
|
|
288
|
+
function isBuiltInOrCustomHookName(name) {
|
|
289
|
+
return HOOK_NAMES.has(name) || isUseName(name);
|
|
290
|
+
}
|
|
291
|
+
function isHookCall(node) {
|
|
292
|
+
if (!node || node.type !== "CallExpression") return false;
|
|
293
|
+
const call = node;
|
|
294
|
+
const callee = call.callee;
|
|
295
|
+
if (callee.type === "Identifier") {
|
|
296
|
+
return isBuiltInOrCustomHookName(callee.name);
|
|
297
|
+
}
|
|
298
|
+
if (callee.type === "MemberExpression") {
|
|
299
|
+
const mem = callee;
|
|
300
|
+
return mem.property.type === "Identifier" && mem.property.name != null && isBuiltInOrCustomHookName(mem.property.name);
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
var CHILD_KEYS = {
|
|
305
|
+
Program: ["body"],
|
|
306
|
+
BlockStatement: ["body"],
|
|
307
|
+
FunctionDeclaration: ["params", "body"],
|
|
308
|
+
FunctionExpression: ["params", "body"],
|
|
309
|
+
ArrowFunctionExpression: ["params", "body"],
|
|
310
|
+
CallExpression: ["callee", "arguments"],
|
|
311
|
+
ReturnStatement: ["argument"],
|
|
312
|
+
VariableDeclaration: ["declarations"],
|
|
313
|
+
VariableDeclarator: ["id", "init"],
|
|
314
|
+
ExpressionStatement: ["expression"],
|
|
315
|
+
MemberExpression: ["object", "property"],
|
|
316
|
+
ArrayPattern: ["elements"],
|
|
317
|
+
ObjectExpression: ["properties"]
|
|
318
|
+
};
|
|
319
|
+
function* traverse(node) {
|
|
320
|
+
if (!node || typeof node !== "object") return;
|
|
321
|
+
const stack = [node];
|
|
322
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
323
|
+
while (stack.length > 0) {
|
|
324
|
+
const current = stack.pop();
|
|
325
|
+
if (seen.has(current)) continue;
|
|
326
|
+
seen.add(current);
|
|
327
|
+
yield current;
|
|
328
|
+
const keys = CHILD_KEYS[current.type ?? ""] ?? [];
|
|
329
|
+
const n = current;
|
|
330
|
+
for (const key of keys) {
|
|
331
|
+
const val = n[key];
|
|
332
|
+
if (val && typeof val === "object") {
|
|
333
|
+
if (Array.isArray(val)) {
|
|
334
|
+
for (let i = val.length - 1; i >= 0; i--) {
|
|
335
|
+
const child = val[i];
|
|
336
|
+
if (child && typeof child === "object" && "type" in child) {
|
|
337
|
+
stack.push(child);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
} else if ("type" in val) {
|
|
341
|
+
stack.push(val);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function callsAnyHook(body) {
|
|
348
|
+
for (const child of traverse(body)) {
|
|
349
|
+
if (isHookCall(child)) return true;
|
|
350
|
+
}
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
function getFunctionBody(node) {
|
|
354
|
+
return node.body;
|
|
355
|
+
}
|
|
356
|
+
var rule4 = {
|
|
357
|
+
meta: {
|
|
358
|
+
type: "suggestion",
|
|
359
|
+
docs: {
|
|
360
|
+
description: "Disallow use* functions that do not call any React hook (useState, useEffect, useReducer, useRef, useContext)",
|
|
361
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-useless-hook.md"
|
|
362
|
+
},
|
|
363
|
+
messages: {
|
|
364
|
+
noUselessHook: "Function '{{name}}' is named like a hook but does not call any React hook. Rename or add hook calls. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-useless-hook.md."
|
|
365
|
+
},
|
|
366
|
+
schema: []
|
|
367
|
+
},
|
|
368
|
+
create(context) {
|
|
369
|
+
function checkHookLikeFunction(name, body, reportNode) {
|
|
370
|
+
if (!isUseName(name)) return;
|
|
371
|
+
if (callsAnyHook(body)) return;
|
|
372
|
+
context.report({
|
|
373
|
+
node: reportNode,
|
|
374
|
+
messageId: "noUselessHook",
|
|
375
|
+
data: { name }
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
FunctionDeclaration(node) {
|
|
380
|
+
const fn = node;
|
|
381
|
+
if (!fn.id) return;
|
|
382
|
+
checkHookLikeFunction(fn.id.name, getFunctionBody(node), fn.id);
|
|
383
|
+
},
|
|
384
|
+
VariableDeclarator(node) {
|
|
385
|
+
const decl = node;
|
|
386
|
+
const { id, init } = decl;
|
|
387
|
+
if (id.type !== "Identifier") return;
|
|
388
|
+
const name = id.name;
|
|
389
|
+
if (!isUseName(name)) return;
|
|
390
|
+
if (init?.type !== "ArrowFunctionExpression" && init?.type !== "FunctionExpression") {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const body = getFunctionBody(init);
|
|
394
|
+
checkHookLikeFunction(name, body, id);
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
var no_useless_hook_default = rule4;
|
|
400
|
+
|
|
401
|
+
// src/rules/no-hook-jsx.ts
|
|
402
|
+
function isUseName2(name) {
|
|
403
|
+
return name.startsWith("use") && name.length > 3;
|
|
404
|
+
}
|
|
405
|
+
function isJSXNode(node) {
|
|
406
|
+
if (!node) return false;
|
|
407
|
+
const type = node.type;
|
|
408
|
+
return type === "JSXElement" || type === "JSXFragment";
|
|
409
|
+
}
|
|
410
|
+
var CHILD_KEYS2 = {
|
|
411
|
+
Program: ["body"],
|
|
412
|
+
BlockStatement: ["body"],
|
|
413
|
+
ReturnStatement: ["argument"],
|
|
414
|
+
VariableDeclarator: ["id", "init"],
|
|
415
|
+
ExpressionStatement: ["expression"],
|
|
416
|
+
IfStatement: ["test", "consequent", "alternate"],
|
|
417
|
+
ForStatement: ["init", "test", "update", "body"],
|
|
418
|
+
ForInStatement: ["left", "right", "body"],
|
|
419
|
+
ForOfStatement: ["left", "right", "body"],
|
|
420
|
+
WhileStatement: ["test", "body"],
|
|
421
|
+
DoWhileStatement: ["body", "test"],
|
|
422
|
+
SwitchStatement: ["discriminant", "cases"],
|
|
423
|
+
SwitchCase: ["test", "consequent"],
|
|
424
|
+
TryStatement: ["block", "handler", "finalizer"],
|
|
425
|
+
CatchClause: ["param", "body"],
|
|
426
|
+
// Expression nodes that may contain nested JSX
|
|
427
|
+
ArrowFunctionExpression: ["params", "body"],
|
|
428
|
+
FunctionExpression: ["params", "body"],
|
|
429
|
+
ObjectExpression: ["properties"],
|
|
430
|
+
Property: ["key", "value"],
|
|
431
|
+
ArrayExpression: ["elements"],
|
|
432
|
+
ConditionalExpression: ["test", "consequent", "alternate"],
|
|
433
|
+
LogicalExpression: ["left", "right"],
|
|
434
|
+
CallExpression: ["callee", "arguments"],
|
|
435
|
+
SequenceExpression: ["expressions"],
|
|
436
|
+
SpreadElement: ["argument"],
|
|
437
|
+
MemberExpression: ["object", "property"],
|
|
438
|
+
JSXElement: ["openingElement", "closingElement", "children"],
|
|
439
|
+
JSXFragment: ["openingFragment", "closingFragment", "children"]
|
|
440
|
+
};
|
|
441
|
+
var STATEMENT_CHILD_KEYS = {
|
|
442
|
+
...CHILD_KEYS2,
|
|
443
|
+
// Do not descend into nested function bodies when finding ReturnStatements
|
|
444
|
+
FunctionDeclaration: ["params"],
|
|
445
|
+
FunctionExpression: ["params"],
|
|
446
|
+
ArrowFunctionExpression: ["params"]
|
|
447
|
+
};
|
|
448
|
+
function* traverse2(node, opts) {
|
|
449
|
+
if (!node || typeof node !== "object") return;
|
|
450
|
+
const keysMap = opts?.skipFunctionBodies ? STATEMENT_CHILD_KEYS : CHILD_KEYS2;
|
|
451
|
+
const stack = [node];
|
|
452
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
453
|
+
while (stack.length > 0) {
|
|
454
|
+
const current = stack.pop();
|
|
455
|
+
if (!current || seen.has(current)) continue;
|
|
456
|
+
seen.add(current);
|
|
457
|
+
yield current;
|
|
458
|
+
const keys = keysMap[current.type ?? ""] ?? [];
|
|
459
|
+
const n = current;
|
|
460
|
+
for (const key of keys) {
|
|
461
|
+
const val = n[key];
|
|
462
|
+
if (val && typeof val === "object") {
|
|
463
|
+
if (Array.isArray(val)) {
|
|
464
|
+
for (let i = val.length - 1; i >= 0; i--) {
|
|
465
|
+
const child = val[i];
|
|
466
|
+
if (child && typeof child === "object" && "type" in child) {
|
|
467
|
+
stack.push(child);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} else if ("type" in val) {
|
|
471
|
+
stack.push(val);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function containsJSX(node) {
|
|
478
|
+
for (const n of traverse2(node)) {
|
|
479
|
+
if (isJSXNode(n)) return true;
|
|
480
|
+
}
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
function returnsJSX(body) {
|
|
484
|
+
if (!body) return false;
|
|
485
|
+
const b = body;
|
|
486
|
+
if (b.type === "JSXElement" || b.type === "JSXFragment") {
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
if (b.type === "BlockStatement") {
|
|
490
|
+
const block = body;
|
|
491
|
+
for (const statement of block.body ?? []) {
|
|
492
|
+
for (const child of traverse2(statement, {
|
|
493
|
+
skipFunctionBodies: true
|
|
494
|
+
})) {
|
|
495
|
+
const c = child;
|
|
496
|
+
if (c.type === "ReturnStatement" && c.argument) {
|
|
497
|
+
if (containsJSX(c.argument)) return true;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
return containsJSX(body);
|
|
504
|
+
}
|
|
505
|
+
var rule5 = {
|
|
506
|
+
meta: {
|
|
507
|
+
type: "suggestion",
|
|
508
|
+
docs: {
|
|
509
|
+
description: "Disallow use* functions that return JSX. Hooks should return values, not components.",
|
|
510
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-hook-jsx.md"
|
|
511
|
+
},
|
|
512
|
+
messages: {
|
|
513
|
+
noHookJsx: "Function '{{name}}' is named like a hook but returns JSX. Hooks should return data, not render. Use a component instead. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-hook-jsx.md."
|
|
514
|
+
},
|
|
515
|
+
schema: []
|
|
516
|
+
},
|
|
517
|
+
create(context) {
|
|
518
|
+
function checkHookLikeFunction(name, body, reportNode) {
|
|
519
|
+
if (!isUseName2(name)) return;
|
|
520
|
+
if (!returnsJSX(body)) return;
|
|
521
|
+
context.report({
|
|
522
|
+
node: reportNode,
|
|
523
|
+
messageId: "noHookJsx",
|
|
524
|
+
data: { name }
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
FunctionDeclaration(node) {
|
|
529
|
+
const fn = node;
|
|
530
|
+
if (!fn.id) return;
|
|
531
|
+
checkHookLikeFunction(fn.id.name, fn.body, fn.id);
|
|
532
|
+
},
|
|
533
|
+
VariableDeclarator(node) {
|
|
534
|
+
const decl = node;
|
|
535
|
+
const { id, init } = decl;
|
|
536
|
+
if (id.type !== "Identifier") return;
|
|
537
|
+
const name = id.name;
|
|
538
|
+
if (!isUseName2(name)) return;
|
|
539
|
+
if (init?.type !== "ArrowFunctionExpression" && init?.type !== "FunctionExpression") {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const body = init.body;
|
|
543
|
+
checkHookLikeFunction(name, body, id);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
var no_hook_jsx_default = rule5;
|
|
549
|
+
|
|
550
|
+
// src/rules/no-exported-context-hooks.ts
|
|
551
|
+
function isContextHookName(name) {
|
|
552
|
+
return name.startsWith("use") && name.endsWith("Context") && name.length > 10;
|
|
553
|
+
}
|
|
554
|
+
function reportIfContextHook(context, node, name) {
|
|
555
|
+
if (!isContextHookName(name)) return;
|
|
556
|
+
context.report({
|
|
557
|
+
node,
|
|
558
|
+
messageId: "noExportedContextHook",
|
|
559
|
+
data: { name }
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
var rule6 = {
|
|
563
|
+
meta: {
|
|
564
|
+
type: "suggestion",
|
|
565
|
+
docs: {
|
|
566
|
+
description: "Disallow exporting use*Context hooks. Keep context hooks private to the component module.",
|
|
567
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-exported-context-hooks.md"
|
|
568
|
+
},
|
|
569
|
+
messages: {
|
|
570
|
+
noExportedContextHook: "Do not export context hook '{{name}}'. Keep it private and expose a controlled component API instead (for example `open` + `onOpenChange`). See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-exported-context-hooks.md."
|
|
571
|
+
},
|
|
572
|
+
schema: []
|
|
573
|
+
},
|
|
574
|
+
create(context) {
|
|
575
|
+
function checkNamedExport(node) {
|
|
576
|
+
const exp = node;
|
|
577
|
+
const declaration = exp.declaration;
|
|
578
|
+
if (declaration?.type === "FunctionDeclaration") {
|
|
579
|
+
const fn = declaration;
|
|
580
|
+
if (!fn.id?.name) return;
|
|
581
|
+
reportIfContextHook(context, declaration, fn.id.name);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (declaration?.type === "VariableDeclaration") {
|
|
585
|
+
const decl = declaration;
|
|
586
|
+
for (const entry of decl.declarations ?? []) {
|
|
587
|
+
const d = entry;
|
|
588
|
+
if (d.id?.type !== "Identifier") continue;
|
|
589
|
+
const id = d.id;
|
|
590
|
+
reportIfContextHook(context, d.id, id.name);
|
|
591
|
+
}
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
for (const specifier of exp.specifiers ?? []) {
|
|
595
|
+
if (specifier.type !== "ExportSpecifier") continue;
|
|
596
|
+
const s = specifier;
|
|
597
|
+
if (s.local?.type === "Identifier") {
|
|
598
|
+
const local = s.local;
|
|
599
|
+
reportIfContextHook(context, s.local, local.name);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (s.exported?.type === "Identifier") {
|
|
603
|
+
const exported = s.exported;
|
|
604
|
+
reportIfContextHook(context, s.exported, exported.name);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function checkDefaultExport(node) {
|
|
609
|
+
const exp = node;
|
|
610
|
+
const declaration = exp.declaration;
|
|
611
|
+
if (!declaration) return;
|
|
612
|
+
if (declaration.type === "Identifier") {
|
|
613
|
+
const id = declaration;
|
|
614
|
+
reportIfContextHook(context, declaration, id.name);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (declaration.type === "FunctionDeclaration") {
|
|
618
|
+
const fn = declaration;
|
|
619
|
+
if (!fn.id?.name) return;
|
|
620
|
+
reportIfContextHook(context, declaration, fn.id.name);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
ExportNamedDeclaration: checkNamedExport,
|
|
625
|
+
ExportDefaultDeclaration: checkDefaultExport
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
var no_exported_context_hooks_default = rule6;
|
|
630
|
+
|
|
631
|
+
// src/rules/no-handler-return-type.ts
|
|
632
|
+
function getKeyName(node) {
|
|
633
|
+
if (node.type === "Identifier") return node.name;
|
|
634
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
function isHandlerProp(name) {
|
|
638
|
+
if (!name.startsWith("on") || name.length <= 2) return false;
|
|
639
|
+
const third = name[2];
|
|
640
|
+
return third >= "A" && third <= "Z";
|
|
641
|
+
}
|
|
642
|
+
function getReturnTypeAnnotation(node) {
|
|
643
|
+
const rt = node.returnType;
|
|
644
|
+
return rt?.typeAnnotation ?? null;
|
|
645
|
+
}
|
|
646
|
+
function isVoidType(typeNode) {
|
|
647
|
+
return typeNode.type === "TSVoidKeyword";
|
|
648
|
+
}
|
|
649
|
+
function isAllowedHandlerReturnType(typeNode) {
|
|
650
|
+
return isVoidType(typeNode);
|
|
651
|
+
}
|
|
652
|
+
function getFunctionReturnTypes(typeNode) {
|
|
653
|
+
const n = typeNode;
|
|
654
|
+
if (n.type === "TSFunctionType") {
|
|
655
|
+
const returnType = getReturnTypeAnnotation(typeNode);
|
|
656
|
+
return returnType ? [returnType] : [];
|
|
657
|
+
}
|
|
658
|
+
if (n.type === "TSParenthesizedType" && n.typeAnnotation) {
|
|
659
|
+
return getFunctionReturnTypes(n.typeAnnotation);
|
|
660
|
+
}
|
|
661
|
+
if ((n.type === "TSUnionType" || n.type === "TSIntersectionType") && Array.isArray(n.types)) {
|
|
662
|
+
const result = [];
|
|
663
|
+
for (const entry of n.types) result.push(...getFunctionReturnTypes(entry));
|
|
664
|
+
return result;
|
|
665
|
+
}
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
function getInvalidReturnType(typeNode) {
|
|
669
|
+
const returnTypes = getFunctionReturnTypes(typeNode);
|
|
670
|
+
for (const returnType of returnTypes) {
|
|
671
|
+
if (!isAllowedHandlerReturnType(returnType)) return returnType;
|
|
672
|
+
}
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
function reportInvalidReturnType(context, node, propName, returnType) {
|
|
676
|
+
context.report({
|
|
677
|
+
node,
|
|
678
|
+
messageId: "noHandlerReturnType",
|
|
679
|
+
data: {
|
|
680
|
+
prop: propName,
|
|
681
|
+
returnType: context.sourceCode.getText(returnType)
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
function isComponentPropsContractName(name) {
|
|
686
|
+
return name?.endsWith("Props") ?? false;
|
|
687
|
+
}
|
|
688
|
+
var rule7 = {
|
|
689
|
+
meta: {
|
|
690
|
+
type: "suggestion",
|
|
691
|
+
docs: {
|
|
692
|
+
description: "Disallow handler prop definitions (on*) that expect return values. Handler contracts must return void.",
|
|
693
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-handler-return-type.md"
|
|
694
|
+
},
|
|
695
|
+
messages: {
|
|
696
|
+
noHandlerReturnType: "Handler prop '{{prop}}' expects return type '{{returnType}}'. Handler prop definitions must not expect return values; use void. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-handler-return-type.md."
|
|
697
|
+
},
|
|
698
|
+
schema: []
|
|
699
|
+
},
|
|
700
|
+
create(context) {
|
|
701
|
+
function checkPropertySignature(node) {
|
|
702
|
+
const property = node;
|
|
703
|
+
if (!property.key || !property.typeAnnotation?.typeAnnotation) return;
|
|
704
|
+
const propName = getKeyName(property.key);
|
|
705
|
+
if (!propName || !isHandlerProp(propName)) return;
|
|
706
|
+
const invalidReturnType = getInvalidReturnType(property.typeAnnotation.typeAnnotation);
|
|
707
|
+
if (!invalidReturnType) return;
|
|
708
|
+
reportInvalidReturnType(context, node, propName, invalidReturnType);
|
|
709
|
+
}
|
|
710
|
+
function checkMethodSignature(node) {
|
|
711
|
+
const method = node;
|
|
712
|
+
if (!method.key || !method.returnType?.typeAnnotation) return;
|
|
713
|
+
const propName = getKeyName(method.key);
|
|
714
|
+
if (!propName || !isHandlerProp(propName)) return;
|
|
715
|
+
if (isAllowedHandlerReturnType(method.returnType.typeAnnotation)) return;
|
|
716
|
+
reportInvalidReturnType(context, node, propName, method.returnType.typeAnnotation);
|
|
717
|
+
}
|
|
718
|
+
function checkMembers(members) {
|
|
719
|
+
for (const member of members) {
|
|
720
|
+
const maybeMember = member;
|
|
721
|
+
if (maybeMember.type === "TSPropertySignature") {
|
|
722
|
+
checkPropertySignature(member);
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
if (maybeMember.type === "TSMethodSignature") {
|
|
726
|
+
checkMethodSignature(member);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return {
|
|
731
|
+
TSInterfaceDeclaration(node) {
|
|
732
|
+
const declaration = node;
|
|
733
|
+
if (!isComponentPropsContractName(declaration.id?.name)) return;
|
|
734
|
+
if (declaration.body?.type !== "TSInterfaceBody") return;
|
|
735
|
+
checkMembers(declaration.body.body ?? []);
|
|
736
|
+
},
|
|
737
|
+
TSTypeAliasDeclaration(node) {
|
|
738
|
+
const declaration = node;
|
|
739
|
+
if (!isComponentPropsContractName(declaration.id?.name)) return;
|
|
740
|
+
if (declaration.typeAnnotation?.type !== "TSTypeLiteral") return;
|
|
741
|
+
checkMembers(declaration.typeAnnotation.members ?? []);
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
var no_handler_return_type_default = rule7;
|
|
747
|
+
|
|
748
|
+
// src/rules/jsx-server-action-prop-suffix.ts
|
|
749
|
+
function getKeyName2(node) {
|
|
750
|
+
if (node.type === "Identifier") return node.name;
|
|
751
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
function isActionPropName(name) {
|
|
755
|
+
return name === "action" || name.endsWith("Action");
|
|
756
|
+
}
|
|
757
|
+
function getReturnTypeAnnotation2(node) {
|
|
758
|
+
return node.returnType?.typeAnnotation ?? null;
|
|
759
|
+
}
|
|
760
|
+
function isPromiseType(typeNode) {
|
|
761
|
+
const n = typeNode;
|
|
762
|
+
if (n.type !== "TSTypeReference") return false;
|
|
763
|
+
const typeName = n.typeName?.name ?? n.typeName?.right?.name;
|
|
764
|
+
return typeName === "Promise";
|
|
765
|
+
}
|
|
766
|
+
function isComponentPropsContractName2(name) {
|
|
767
|
+
return name?.endsWith("Props") ?? false;
|
|
768
|
+
}
|
|
769
|
+
function getFunctionReturnTypes2(typeNode) {
|
|
770
|
+
const n = typeNode;
|
|
771
|
+
if (n.type === "TSFunctionType") {
|
|
772
|
+
const returnType = getReturnTypeAnnotation2(typeNode);
|
|
773
|
+
return returnType ? [returnType] : [];
|
|
774
|
+
}
|
|
775
|
+
if (n.type === "TSParenthesizedType" && n.typeAnnotation) {
|
|
776
|
+
return getFunctionReturnTypes2(n.typeAnnotation);
|
|
777
|
+
}
|
|
778
|
+
if ((n.type === "TSUnionType" || n.type === "TSIntersectionType") && Array.isArray(n.types)) {
|
|
779
|
+
const result = [];
|
|
780
|
+
for (const entry of n.types) result.push(...getFunctionReturnTypes2(entry));
|
|
781
|
+
return result;
|
|
782
|
+
}
|
|
783
|
+
return [];
|
|
784
|
+
}
|
|
785
|
+
var rule8 = {
|
|
786
|
+
meta: {
|
|
787
|
+
type: "suggestion",
|
|
788
|
+
docs: {
|
|
789
|
+
description: "Enforce async function prop contracts to use action prop names (action or *Action).",
|
|
790
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/jsx-server-action-prop-suffix.md"
|
|
791
|
+
},
|
|
792
|
+
messages: {
|
|
793
|
+
asyncPropRequiresActionName: "Prop '{{prop}}' expects an async function type ('{{returnType}}'). Async function props must be named 'action' or end with 'Action'. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/jsx-server-action-prop-suffix.md."
|
|
794
|
+
},
|
|
795
|
+
schema: []
|
|
796
|
+
},
|
|
797
|
+
create(context) {
|
|
798
|
+
function reportIfNeeded(node, propName, returnType) {
|
|
799
|
+
if (isActionPropName(propName)) return;
|
|
800
|
+
context.report({
|
|
801
|
+
node,
|
|
802
|
+
messageId: "asyncPropRequiresActionName",
|
|
803
|
+
data: { prop: propName, returnType: context.sourceCode.getText(returnType) }
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
function checkPropertySignature(node) {
|
|
807
|
+
const property = node;
|
|
808
|
+
const key = property.key;
|
|
809
|
+
const typeNode = property.typeAnnotation?.typeAnnotation;
|
|
810
|
+
if (!key || !typeNode) return;
|
|
811
|
+
const propName = getKeyName2(key);
|
|
812
|
+
if (!propName) return;
|
|
813
|
+
for (const returnType of getFunctionReturnTypes2(typeNode)) {
|
|
814
|
+
if (isPromiseType(returnType)) {
|
|
815
|
+
reportIfNeeded(node, propName, returnType);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
function checkMethodSignature(node) {
|
|
821
|
+
const method = node;
|
|
822
|
+
const key = method.key;
|
|
823
|
+
const returnType = method.returnType?.typeAnnotation;
|
|
824
|
+
if (!key || !returnType) return;
|
|
825
|
+
if (!isPromiseType(returnType)) return;
|
|
826
|
+
const propName = getKeyName2(key);
|
|
827
|
+
if (!propName) return;
|
|
828
|
+
reportIfNeeded(node, propName, returnType);
|
|
829
|
+
}
|
|
830
|
+
function checkMembers(members) {
|
|
831
|
+
for (const member of members) {
|
|
832
|
+
const maybeMember = member;
|
|
833
|
+
if (maybeMember.type === "TSPropertySignature") {
|
|
834
|
+
checkPropertySignature(member);
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
if (maybeMember.type === "TSMethodSignature") {
|
|
838
|
+
checkMethodSignature(member);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
TSInterfaceDeclaration(node) {
|
|
844
|
+
const declaration = node;
|
|
845
|
+
if (!isComponentPropsContractName2(declaration.id?.name)) return;
|
|
846
|
+
if (declaration.body?.type !== "TSInterfaceBody") return;
|
|
847
|
+
checkMembers(declaration.body.body ?? []);
|
|
848
|
+
},
|
|
849
|
+
TSTypeAliasDeclaration(node) {
|
|
850
|
+
const declaration = node;
|
|
851
|
+
if (!isComponentPropsContractName2(declaration.id?.name)) return;
|
|
852
|
+
if (declaration.typeAnnotation?.type !== "TSTypeLiteral") return;
|
|
853
|
+
checkMembers(declaration.typeAnnotation.members ?? []);
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
var jsx_server_action_prop_suffix_default = rule8;
|
|
859
|
+
|
|
860
|
+
// src/rules/jsx-on-handler-verb-suffix.ts
|
|
861
|
+
var DEFAULT_VERBS = [
|
|
862
|
+
"abort",
|
|
863
|
+
"access",
|
|
864
|
+
"activate",
|
|
865
|
+
"add",
|
|
866
|
+
"blur",
|
|
867
|
+
"cancel",
|
|
868
|
+
"change",
|
|
869
|
+
"clear",
|
|
870
|
+
"click",
|
|
871
|
+
"close",
|
|
872
|
+
"collapse",
|
|
873
|
+
"complete",
|
|
874
|
+
"connect",
|
|
875
|
+
"copy",
|
|
876
|
+
"create",
|
|
877
|
+
"deactivate",
|
|
878
|
+
"delete",
|
|
879
|
+
"disable",
|
|
880
|
+
"dismiss",
|
|
881
|
+
"drag",
|
|
882
|
+
"drop",
|
|
883
|
+
"edit",
|
|
884
|
+
"enable",
|
|
885
|
+
"end",
|
|
886
|
+
"error",
|
|
887
|
+
"expand",
|
|
888
|
+
"finish",
|
|
889
|
+
"focus",
|
|
890
|
+
"generate",
|
|
891
|
+
"get",
|
|
892
|
+
"hide",
|
|
893
|
+
"hover",
|
|
894
|
+
"input",
|
|
895
|
+
"install",
|
|
896
|
+
"keydown",
|
|
897
|
+
"keyup",
|
|
898
|
+
"load",
|
|
899
|
+
"mount",
|
|
900
|
+
"move",
|
|
901
|
+
"open",
|
|
902
|
+
"paste",
|
|
903
|
+
"pause",
|
|
904
|
+
"persist",
|
|
905
|
+
"play",
|
|
906
|
+
"press",
|
|
907
|
+
"progress",
|
|
908
|
+
"query",
|
|
909
|
+
"ready",
|
|
910
|
+
"remove",
|
|
911
|
+
"rename",
|
|
912
|
+
"request",
|
|
913
|
+
"reset",
|
|
914
|
+
"resize",
|
|
915
|
+
"retry",
|
|
916
|
+
"revalidate",
|
|
917
|
+
"revert",
|
|
918
|
+
"save",
|
|
919
|
+
"scroll",
|
|
920
|
+
"seek",
|
|
921
|
+
"select",
|
|
922
|
+
"show",
|
|
923
|
+
"skip",
|
|
924
|
+
"start",
|
|
925
|
+
"submit",
|
|
926
|
+
"success",
|
|
927
|
+
"track",
|
|
928
|
+
"undo",
|
|
929
|
+
"update",
|
|
930
|
+
"upgrade",
|
|
931
|
+
"upload",
|
|
932
|
+
"validate",
|
|
933
|
+
"wheel"
|
|
934
|
+
];
|
|
935
|
+
function mergeAllowedValues(defaults, configured) {
|
|
936
|
+
const configuredValues = configured ?? [];
|
|
937
|
+
const allValues = [...defaults, ...configuredValues];
|
|
938
|
+
return [...new Set(allValues.map(normalizeConfigValue).filter(Boolean))];
|
|
939
|
+
}
|
|
940
|
+
function normalizeConfigValue(value) {
|
|
941
|
+
return value.trim().toLowerCase().replace(/\s+/g, "");
|
|
942
|
+
}
|
|
943
|
+
function getPropName3(node) {
|
|
944
|
+
if (node.type === "JSXIdentifier") {
|
|
945
|
+
return node.name;
|
|
946
|
+
}
|
|
947
|
+
if (node.type === "JSXNamespacedName") {
|
|
948
|
+
return `${node.namespace.name}:${node.name.name}`;
|
|
949
|
+
}
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
function isHandlerProp2(name) {
|
|
953
|
+
if (!name.startsWith("on") || name.length <= 2) return false;
|
|
954
|
+
const third = name[2];
|
|
955
|
+
return third >= "A" && third <= "Z";
|
|
956
|
+
}
|
|
957
|
+
function hasVerbSuffix(rest, verbs) {
|
|
958
|
+
const restLower = rest.toLowerCase();
|
|
959
|
+
return verbs.some((verb) => restLower.endsWith(verb));
|
|
960
|
+
}
|
|
961
|
+
function getVerbSuffixSuggestion(propName, verbs) {
|
|
962
|
+
if (!isHandlerProp2(propName)) return null;
|
|
963
|
+
const rest = propName.slice(2);
|
|
964
|
+
const restLower = rest.toLowerCase();
|
|
965
|
+
if (rest.length < 2) return null;
|
|
966
|
+
if (hasVerbSuffix(rest, verbs)) return null;
|
|
967
|
+
for (const verb of verbs) {
|
|
968
|
+
if (!restLower.startsWith(verb)) continue;
|
|
969
|
+
const verbOriginal = rest.slice(0, verb.length);
|
|
970
|
+
const subject = rest.slice(verb.length);
|
|
971
|
+
if (subject.length < 1) continue;
|
|
972
|
+
if (subject[0] !== subject[0].toUpperCase()) continue;
|
|
973
|
+
return `on${subject}${verbOriginal}`;
|
|
974
|
+
}
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
var rule9 = {
|
|
978
|
+
meta: {
|
|
979
|
+
type: "suggestion",
|
|
980
|
+
docs: {
|
|
981
|
+
description: "Ensure on* handler prop names end with a verb (for example onValueChange).",
|
|
982
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/jsx-on-handler-verb-suffix.md"
|
|
983
|
+
},
|
|
984
|
+
messages: {
|
|
985
|
+
mustEndWithVerb: "Handler prop '{{prop}}' should end with a verb (for example 'onValueChange'). If the suffix is a legitimate domain verb, add it to this rule's 'extendVerbs' option. AI agents: update rule options before renaming APIs. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/jsx-on-handler-verb-suffix.md.",
|
|
986
|
+
preferVerbSuffix: "Prefer '{{suggestion}}' over '{{prop}}' so the handler name ends with a verb. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/jsx-on-handler-verb-suffix.md."
|
|
987
|
+
},
|
|
988
|
+
schema: [
|
|
989
|
+
{
|
|
990
|
+
type: "object",
|
|
991
|
+
properties: {
|
|
992
|
+
extendVerbs: {
|
|
993
|
+
type: "array",
|
|
994
|
+
items: {
|
|
995
|
+
type: "string"
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
additionalProperties: false
|
|
1000
|
+
}
|
|
1001
|
+
]
|
|
1002
|
+
},
|
|
1003
|
+
create(context) {
|
|
1004
|
+
const options = context.options[0] ?? {};
|
|
1005
|
+
const verbs = mergeAllowedValues(DEFAULT_VERBS, options.extendVerbs);
|
|
1006
|
+
verbs.sort((a, b) => b.length - a.length);
|
|
1007
|
+
return {
|
|
1008
|
+
JSXAttribute(node) {
|
|
1009
|
+
const attr = node;
|
|
1010
|
+
const propName = getPropName3(attr.name);
|
|
1011
|
+
if (!propName) return;
|
|
1012
|
+
if (!isHandlerProp2(propName)) return;
|
|
1013
|
+
const rest = propName.slice(2);
|
|
1014
|
+
if (hasVerbSuffix(rest, verbs)) return;
|
|
1015
|
+
const suggestion = getVerbSuffixSuggestion(propName, verbs);
|
|
1016
|
+
context.report({
|
|
1017
|
+
node: attr.name,
|
|
1018
|
+
messageId: suggestion ? "preferVerbSuffix" : "mustEndWithVerb",
|
|
1019
|
+
data: suggestion ? { prop: propName, suggestion } : { prop: propName }
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
var jsx_on_handler_verb_suffix_default = rule9;
|
|
1026
|
+
|
|
1027
|
+
// src/rules/no-render-helper-functions.ts
|
|
1028
|
+
function isPascalCase(name) {
|
|
1029
|
+
if (!name || name.length === 0) return false;
|
|
1030
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(name);
|
|
1031
|
+
}
|
|
1032
|
+
function expressionReturnsJSX(node) {
|
|
1033
|
+
if (!node) return false;
|
|
1034
|
+
const n = node;
|
|
1035
|
+
const t = n.type;
|
|
1036
|
+
if (t === "JSXElement" || t === "JSXFragment") return true;
|
|
1037
|
+
if (t === "ParenthesizedExpression") {
|
|
1038
|
+
return expressionReturnsJSX(n.expression);
|
|
1039
|
+
}
|
|
1040
|
+
if (t === "ConditionalExpression") {
|
|
1041
|
+
return expressionReturnsJSX(n.consequent) || expressionReturnsJSX(n.alternate);
|
|
1042
|
+
}
|
|
1043
|
+
if (t === "LogicalExpression") {
|
|
1044
|
+
return expressionReturnsJSX(n.left) || expressionReturnsJSX(n.right);
|
|
1045
|
+
}
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
var STATEMENT_CHILD_KEYS2 = {
|
|
1049
|
+
BlockStatement: ["body"],
|
|
1050
|
+
IfStatement: ["test", "consequent", "alternate"],
|
|
1051
|
+
ForStatement: ["init", "test", "update", "body"],
|
|
1052
|
+
ForInStatement: ["left", "right", "body"],
|
|
1053
|
+
ForOfStatement: ["left", "right", "body"],
|
|
1054
|
+
WhileStatement: ["test", "body"],
|
|
1055
|
+
DoWhileStatement: ["body", "test"],
|
|
1056
|
+
SwitchStatement: ["discriminant", "cases"],
|
|
1057
|
+
SwitchCase: ["test", "consequent"],
|
|
1058
|
+
TryStatement: ["block", "handler", "finalizer"],
|
|
1059
|
+
CatchClause: ["param", "body"],
|
|
1060
|
+
ReturnStatement: ["argument"],
|
|
1061
|
+
ExpressionStatement: ["expression"],
|
|
1062
|
+
VariableDeclaration: ["declarations"]
|
|
1063
|
+
};
|
|
1064
|
+
function* traverseStatements(node) {
|
|
1065
|
+
if (!node || typeof node !== "object") return;
|
|
1066
|
+
const stack = [node];
|
|
1067
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1068
|
+
while (stack.length > 0) {
|
|
1069
|
+
const current = stack.pop();
|
|
1070
|
+
if (seen.has(current)) continue;
|
|
1071
|
+
seen.add(current);
|
|
1072
|
+
yield current;
|
|
1073
|
+
const keys = STATEMENT_CHILD_KEYS2[current.type ?? ""] ?? [];
|
|
1074
|
+
const n = current;
|
|
1075
|
+
for (const key of keys) {
|
|
1076
|
+
const val = n[key];
|
|
1077
|
+
if (val && typeof val === "object") {
|
|
1078
|
+
if (Array.isArray(val)) {
|
|
1079
|
+
for (let i = val.length - 1; i >= 0; i--) {
|
|
1080
|
+
const child = val[i];
|
|
1081
|
+
if (child && typeof child === "object" && "type" in child) {
|
|
1082
|
+
stack.push(child);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
} else if ("type" in val) {
|
|
1086
|
+
stack.push(val);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function functionReturnsJSX(body) {
|
|
1093
|
+
if (!body) return false;
|
|
1094
|
+
const b = body;
|
|
1095
|
+
if (b.type === "JSXElement" || b.type === "JSXFragment") {
|
|
1096
|
+
return true;
|
|
1097
|
+
}
|
|
1098
|
+
if (b.type === "BlockStatement") {
|
|
1099
|
+
for (const child of traverseStatements(body)) {
|
|
1100
|
+
const s = child;
|
|
1101
|
+
if (s.type === "ReturnStatement" && s.argument) {
|
|
1102
|
+
if (expressionReturnsJSX(s.argument)) return true;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
var rule10 = {
|
|
1109
|
+
meta: {
|
|
1110
|
+
type: "suggestion",
|
|
1111
|
+
docs: {
|
|
1112
|
+
description: "Forbid JSX returned from non-component functions. Functions that return JSX should use PascalCase names.",
|
|
1113
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-render-helper-functions.md"
|
|
1114
|
+
},
|
|
1115
|
+
messages: {
|
|
1116
|
+
noRenderHelperFunctions: "Function '{{name}}' returns JSX but is not PascalCase. Use a component name (PascalCase) or move the JSX elsewhere. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-render-helper-functions.md."
|
|
1117
|
+
},
|
|
1118
|
+
schema: []
|
|
1119
|
+
},
|
|
1120
|
+
create(context) {
|
|
1121
|
+
function checkFunction(name, body, reportNode) {
|
|
1122
|
+
if (isPascalCase(name)) return;
|
|
1123
|
+
if (!functionReturnsJSX(body)) return;
|
|
1124
|
+
context.report({
|
|
1125
|
+
node: reportNode,
|
|
1126
|
+
messageId: "noRenderHelperFunctions",
|
|
1127
|
+
data: { name }
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
FunctionDeclaration(node) {
|
|
1132
|
+
const fn = node;
|
|
1133
|
+
if (!fn.id) return;
|
|
1134
|
+
checkFunction(fn.id.name, fn.body, fn.id);
|
|
1135
|
+
},
|
|
1136
|
+
VariableDeclarator(node) {
|
|
1137
|
+
const decl = node;
|
|
1138
|
+
const { id, init } = decl;
|
|
1139
|
+
if (id.type !== "Identifier") return;
|
|
1140
|
+
const name = id.name;
|
|
1141
|
+
if (init?.type !== "ArrowFunctionExpression" && init?.type !== "FunctionExpression") {
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const body = init.body;
|
|
1145
|
+
checkFunction(name, body, id);
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
var no_render_helper_functions_default = rule10;
|
|
1151
|
+
|
|
1152
|
+
// src/rules/no-element-props.ts
|
|
1153
|
+
function getPropKeyName(prop) {
|
|
1154
|
+
const key = prop.key;
|
|
1155
|
+
if (key.type === "Identifier" && "name" in key) return key.name ?? null;
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
function getPropsFromInterfaceBody(body) {
|
|
1159
|
+
return body.body ?? [];
|
|
1160
|
+
}
|
|
1161
|
+
function getPropsFromTypeLiteral(typeNode) {
|
|
1162
|
+
const n = typeNode;
|
|
1163
|
+
if (n && n.type === "TSTypeLiteral" && Array.isArray(n.members)) return n.members;
|
|
1164
|
+
return [];
|
|
1165
|
+
}
|
|
1166
|
+
function isComponentPropsContractName3(name) {
|
|
1167
|
+
return name?.endsWith("Props") ?? false;
|
|
1168
|
+
}
|
|
1169
|
+
var rule11 = {
|
|
1170
|
+
meta: {
|
|
1171
|
+
type: "suggestion",
|
|
1172
|
+
docs: {
|
|
1173
|
+
description: "Disallow ReactNode/ReactElement-typed props except children/render.",
|
|
1174
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-element-props.md"
|
|
1175
|
+
},
|
|
1176
|
+
messages: {
|
|
1177
|
+
noElementProps: "Prop '{{prop}}' has an element type (ReactNode/ReactElement). Use compound composition instead: expose parts and compose via 'children' (for example <Card><Card.Header /></Card>) rather than passing '{{prop}}' as an element prop. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-element-props.md."
|
|
1178
|
+
},
|
|
1179
|
+
schema: []
|
|
1180
|
+
},
|
|
1181
|
+
create(context) {
|
|
1182
|
+
const aliasTypeMap = /* @__PURE__ */ new Map();
|
|
1183
|
+
function getTypeNameFromRef2(node) {
|
|
1184
|
+
const typeName = node.typeName;
|
|
1185
|
+
if ("name" in typeName && typeName.name) return typeName.name;
|
|
1186
|
+
if ("right" in typeName && typeName.right?.name) return typeName.right.name;
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
function astContainsReactType(typeNode, reactTypeName, visitedAliases) {
|
|
1190
|
+
if (!typeNode || typeof typeNode !== "object") return false;
|
|
1191
|
+
const node = typeNode;
|
|
1192
|
+
if (node.type === "TSTypeReference") {
|
|
1193
|
+
const ref = node;
|
|
1194
|
+
const typeName = getTypeNameFromRef2(ref);
|
|
1195
|
+
if (typeName === reactTypeName) return true;
|
|
1196
|
+
if (typeName && aliasTypeMap.has(typeName) && !visitedAliases.has(typeName)) {
|
|
1197
|
+
visitedAliases.add(typeName);
|
|
1198
|
+
return astContainsReactType(aliasTypeMap.get(typeName), reactTypeName, visitedAliases);
|
|
1199
|
+
}
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
if (node.type === "TSUnionType" || node.type === "TSIntersectionType") {
|
|
1203
|
+
const unionNode = node;
|
|
1204
|
+
return (unionNode.types ?? []).some(
|
|
1205
|
+
(entry) => astContainsReactType(entry, reactTypeName, visitedAliases)
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
if (node.type === "TSOptionalType" || node.type === "TSParenthesizedType") {
|
|
1209
|
+
const wrapped = node;
|
|
1210
|
+
return astContainsReactType(wrapped.typeAnnotation, reactTypeName, visitedAliases);
|
|
1211
|
+
}
|
|
1212
|
+
return false;
|
|
1213
|
+
}
|
|
1214
|
+
function checkProp(prop, reportNode) {
|
|
1215
|
+
const name = getPropKeyName(prop);
|
|
1216
|
+
if (!name) return;
|
|
1217
|
+
const typeAnn = prop.typeAnnotation?.typeAnnotation;
|
|
1218
|
+
const hasReactNodeType = astContainsReactType(typeAnn, "ReactNode", /* @__PURE__ */ new Set());
|
|
1219
|
+
const hasReactElementType = astContainsReactType(typeAnn, "ReactElement", /* @__PURE__ */ new Set());
|
|
1220
|
+
const hasElementType = hasReactNodeType || hasReactElementType;
|
|
1221
|
+
if (hasElementType && name !== "children" && name !== "render") {
|
|
1222
|
+
context.report({
|
|
1223
|
+
node: reportNode,
|
|
1224
|
+
messageId: "noElementProps",
|
|
1225
|
+
data: { prop: name }
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function visitProps(props) {
|
|
1230
|
+
for (const prop of props) {
|
|
1231
|
+
if (prop.type !== "TSPropertySignature") continue;
|
|
1232
|
+
checkProp(prop, prop.key);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
const pendingChecks = [];
|
|
1236
|
+
return {
|
|
1237
|
+
TSInterfaceDeclaration(node) {
|
|
1238
|
+
const decl = node;
|
|
1239
|
+
if (!isComponentPropsContractName3(decl.id?.name)) return;
|
|
1240
|
+
if (decl.body?.type === "TSInterfaceBody") {
|
|
1241
|
+
const props = getPropsFromInterfaceBody(decl.body);
|
|
1242
|
+
pendingChecks.push(() => visitProps(props));
|
|
1243
|
+
}
|
|
1244
|
+
},
|
|
1245
|
+
TSTypeAliasDeclaration(node) {
|
|
1246
|
+
const decl = node;
|
|
1247
|
+
if (decl.id?.type === "Identifier" && decl.id.name && decl.typeAnnotation) {
|
|
1248
|
+
aliasTypeMap.set(decl.id.name, decl.typeAnnotation);
|
|
1249
|
+
}
|
|
1250
|
+
if (!isComponentPropsContractName3(decl.id?.name)) return;
|
|
1251
|
+
const typeAnn = decl.typeAnnotation;
|
|
1252
|
+
if (typeAnn) {
|
|
1253
|
+
const props = getPropsFromTypeLiteral(typeAnn);
|
|
1254
|
+
pendingChecks.push(() => visitProps(props));
|
|
1255
|
+
}
|
|
1256
|
+
},
|
|
1257
|
+
"Program:exit"() {
|
|
1258
|
+
for (const check of pendingChecks) check();
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
var no_element_props_default = rule11;
|
|
1264
|
+
|
|
1265
|
+
// src/rules/no-componenttype-props.ts
|
|
1266
|
+
var FORBIDDEN_TYPE_NAMES = /* @__PURE__ */ new Set(["ComponentType", "FC", "FunctionComponent"]);
|
|
1267
|
+
function getTypeNameFromRef(node) {
|
|
1268
|
+
const tn = node.typeName;
|
|
1269
|
+
if ("name" in tn && tn.name) return tn.name;
|
|
1270
|
+
if ("right" in tn && tn.right) return tn.right.name;
|
|
1271
|
+
return null;
|
|
1272
|
+
}
|
|
1273
|
+
function isComponentTypeProp(typeNode, resolveAliasType, seenAliases = /* @__PURE__ */ new Set()) {
|
|
1274
|
+
if (!typeNode || typeof typeNode !== "object") return false;
|
|
1275
|
+
const n = typeNode;
|
|
1276
|
+
if (n.type === "TSTypeReference") {
|
|
1277
|
+
const ref = n;
|
|
1278
|
+
const name = getTypeNameFromRef(ref);
|
|
1279
|
+
if (name !== null && FORBIDDEN_TYPE_NAMES.has(name)) return true;
|
|
1280
|
+
if (!name || !resolveAliasType || seenAliases.has(name)) return false;
|
|
1281
|
+
const aliasType = resolveAliasType(name);
|
|
1282
|
+
if (!aliasType) return false;
|
|
1283
|
+
const nextSeenAliases = new Set(seenAliases);
|
|
1284
|
+
nextSeenAliases.add(name);
|
|
1285
|
+
return isComponentTypeProp(aliasType, resolveAliasType, nextSeenAliases);
|
|
1286
|
+
}
|
|
1287
|
+
if (n.type === "TSUnionType" || n.type === "TSIntersectionType") {
|
|
1288
|
+
const union = n;
|
|
1289
|
+
return (union.types ?? []).some((t) => isComponentTypeProp(t, resolveAliasType, seenAliases));
|
|
1290
|
+
}
|
|
1291
|
+
if (n.type === "TSOptionalType") {
|
|
1292
|
+
const opt = n;
|
|
1293
|
+
return isComponentTypeProp(opt.typeAnnotation, resolveAliasType, seenAliases);
|
|
1294
|
+
}
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
function getPropKeyName2(prop) {
|
|
1298
|
+
const key = prop.key;
|
|
1299
|
+
if (key.type === "Identifier" && "name" in key) return key.name ?? null;
|
|
1300
|
+
return null;
|
|
1301
|
+
}
|
|
1302
|
+
function getPropsFromInterfaceBody2(body) {
|
|
1303
|
+
return body.body ?? [];
|
|
1304
|
+
}
|
|
1305
|
+
function getPropsFromTypeLiteral2(typeNode) {
|
|
1306
|
+
const n = typeNode;
|
|
1307
|
+
if (n && n.type === "TSTypeLiteral" && Array.isArray(n.members)) return n.members;
|
|
1308
|
+
return [];
|
|
1309
|
+
}
|
|
1310
|
+
function isComponentPropsContractName4(name) {
|
|
1311
|
+
return name?.endsWith("Props") ?? false;
|
|
1312
|
+
}
|
|
1313
|
+
var rule12 = {
|
|
1314
|
+
meta: {
|
|
1315
|
+
type: "suggestion",
|
|
1316
|
+
docs: {
|
|
1317
|
+
description: "Disallow ComponentType/FC/FunctionComponent in prop definitions. Prefer asChild or render prop for polymorphism.",
|
|
1318
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-componenttype-props.md"
|
|
1319
|
+
},
|
|
1320
|
+
messages: {
|
|
1321
|
+
noComponentTypeProps: "Prop '{{prop}}' uses ComponentType/FC/FunctionComponent. Prefer asChild or a render prop for polymorphism. AI agents: replace component-constructor props with composition or explicit render callbacks. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-componenttype-props.md."
|
|
1322
|
+
},
|
|
1323
|
+
schema: []
|
|
1324
|
+
},
|
|
1325
|
+
create(context) {
|
|
1326
|
+
const localTypeAliases = /* @__PURE__ */ new Map();
|
|
1327
|
+
function checkProp(prop) {
|
|
1328
|
+
const name = getPropKeyName2(prop);
|
|
1329
|
+
if (!name) return;
|
|
1330
|
+
const typeAnn = prop.typeAnnotation?.typeAnnotation;
|
|
1331
|
+
if (!typeAnn) return;
|
|
1332
|
+
if (!isComponentTypeProp(typeAnn, (aliasName) => localTypeAliases.get(aliasName) ?? null)) {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
context.report({
|
|
1336
|
+
node: prop.key,
|
|
1337
|
+
messageId: "noComponentTypeProps",
|
|
1338
|
+
data: { prop: name }
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
function visitProps(props) {
|
|
1342
|
+
for (const prop of props) {
|
|
1343
|
+
if (prop.type !== "TSPropertySignature") continue;
|
|
1344
|
+
checkProp(prop);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
const pendingChecks = [];
|
|
1348
|
+
return {
|
|
1349
|
+
TSInterfaceDeclaration(node) {
|
|
1350
|
+
const decl = node;
|
|
1351
|
+
if (!isComponentPropsContractName4(decl.id?.name)) return;
|
|
1352
|
+
if (decl.body?.type === "TSInterfaceBody") {
|
|
1353
|
+
const props = getPropsFromInterfaceBody2(decl.body);
|
|
1354
|
+
pendingChecks.push(() => visitProps(props));
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1357
|
+
TSTypeAliasDeclaration(node) {
|
|
1358
|
+
const decl = node;
|
|
1359
|
+
const typeAliasName = decl.id?.name;
|
|
1360
|
+
if (typeAliasName) {
|
|
1361
|
+
localTypeAliases.set(typeAliasName, decl.typeAnnotation);
|
|
1362
|
+
}
|
|
1363
|
+
if (!isComponentPropsContractName4(typeAliasName)) return;
|
|
1364
|
+
const typeAnn = decl.typeAnnotation;
|
|
1365
|
+
if (typeAnn) {
|
|
1366
|
+
const props = getPropsFromTypeLiteral2(typeAnn);
|
|
1367
|
+
pendingChecks.push(() => visitProps(props));
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1370
|
+
"Program:exit"() {
|
|
1371
|
+
for (const check of pendingChecks) check();
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
var no_componenttype_props_default = rule12;
|
|
1377
|
+
|
|
1378
|
+
// src/rules/no-object-props.ts
|
|
1379
|
+
import ts from "typescript";
|
|
1380
|
+
var rule13 = {
|
|
1381
|
+
meta: {
|
|
1382
|
+
type: "suggestion",
|
|
1383
|
+
docs: {
|
|
1384
|
+
description: "Disallow object-valued JSX props (more accurate when TypeScript type information is available)",
|
|
1385
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-object-props.md"
|
|
1386
|
+
},
|
|
1387
|
+
messages: {
|
|
1388
|
+
noObjectProps: "Object value passed to prop '{{prop}}'. Avoid object props; prefer primitive props and compound composition. If object-shaped data must be shared across parts, use private context inside the compound root instead of passing object props. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-object-props.md."
|
|
1389
|
+
},
|
|
1390
|
+
schema: []
|
|
1391
|
+
},
|
|
1392
|
+
create(context) {
|
|
1393
|
+
const parserServices = context.sourceCode.parserServices ?? context.parserServices;
|
|
1394
|
+
const checker = parserServices?.program?.getTypeChecker();
|
|
1395
|
+
const hasTypeInformation = Boolean(checker && parserServices?.esTreeNodeToTSNodeMap);
|
|
1396
|
+
function isDisallowedObjectType(type) {
|
|
1397
|
+
if (!type || !checker) return false;
|
|
1398
|
+
if (type.isUnionOrIntersection()) {
|
|
1399
|
+
return type.types.some((entry) => isDisallowedObjectType(entry));
|
|
1400
|
+
}
|
|
1401
|
+
if (checker.isArrayType(type) || checker.isTupleType(type)) return false;
|
|
1402
|
+
if (checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0) return false;
|
|
1403
|
+
if (checker.getSignaturesOfType(type, ts.SignatureKind.Construct).length > 0) return false;
|
|
1404
|
+
const isObjectLike = (type.flags & ts.TypeFlags.Object) !== 0 || (type.flags & ts.TypeFlags.NonPrimitive) !== 0;
|
|
1405
|
+
return isObjectLike;
|
|
1406
|
+
}
|
|
1407
|
+
function expressionHasObjectType(expression) {
|
|
1408
|
+
if (!hasTypeInformation || !checker || !parserServices?.esTreeNodeToTSNodeMap) return false;
|
|
1409
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(expression);
|
|
1410
|
+
if (!tsNode) return false;
|
|
1411
|
+
return isDisallowedObjectType(checker.getTypeAtLocation(tsNode));
|
|
1412
|
+
}
|
|
1413
|
+
return {
|
|
1414
|
+
JSXAttribute(node) {
|
|
1415
|
+
const attr = node;
|
|
1416
|
+
if (attr.name?.type !== "JSXIdentifier" || !attr.name.name) return;
|
|
1417
|
+
if (attr.name.name === "style") return;
|
|
1418
|
+
if (!attr.value || attr.value.type !== "JSXExpressionContainer") return;
|
|
1419
|
+
const expression = attr.value.expression;
|
|
1420
|
+
if (!expression) return;
|
|
1421
|
+
const isInlineObject = expression.type === "ObjectExpression";
|
|
1422
|
+
const isObjectTyped = expressionHasObjectType(expression);
|
|
1423
|
+
if (!isInlineObject && !isObjectTyped) return;
|
|
1424
|
+
context.report({
|
|
1425
|
+
node: attr.value,
|
|
1426
|
+
messageId: "noObjectProps",
|
|
1427
|
+
data: { prop: attr.name.name }
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
var no_object_props_default = rule13;
|
|
1434
|
+
|
|
1435
|
+
// src/rules/no-array-props.ts
|
|
1436
|
+
var rule14 = {
|
|
1437
|
+
meta: {
|
|
1438
|
+
type: "suggestion",
|
|
1439
|
+
docs: {
|
|
1440
|
+
description: "Disallow array-valued JSX props (more accurate when TypeScript type information is available)",
|
|
1441
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-array-props.md"
|
|
1442
|
+
},
|
|
1443
|
+
messages: {
|
|
1444
|
+
noArrayProps: "Array value passed to prop '{{prop}}'. Avoid array props; prefer primitive props and compound composition. If array-shaped data must be shared across parts, use private context inside the compound root instead of passing array props. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-array-props.md."
|
|
1445
|
+
},
|
|
1446
|
+
schema: []
|
|
1447
|
+
},
|
|
1448
|
+
create(context) {
|
|
1449
|
+
const parserServices = context.sourceCode.parserServices ?? context.parserServices;
|
|
1450
|
+
const checker = parserServices?.program?.getTypeChecker();
|
|
1451
|
+
const hasTypeInformation = Boolean(checker && parserServices?.esTreeNodeToTSNodeMap);
|
|
1452
|
+
function isDisallowedArrayType(type) {
|
|
1453
|
+
if (!type || !checker) return false;
|
|
1454
|
+
if (type.isUnionOrIntersection()) {
|
|
1455
|
+
return type.types.some((entry) => isDisallowedArrayType(entry));
|
|
1456
|
+
}
|
|
1457
|
+
return checker.isArrayType(type) || checker.isTupleType(type);
|
|
1458
|
+
}
|
|
1459
|
+
function expressionHasArrayType(expression) {
|
|
1460
|
+
if (!hasTypeInformation || !checker || !parserServices?.esTreeNodeToTSNodeMap) return false;
|
|
1461
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(expression);
|
|
1462
|
+
if (!tsNode) return false;
|
|
1463
|
+
return isDisallowedArrayType(checker.getTypeAtLocation(tsNode));
|
|
1464
|
+
}
|
|
1465
|
+
return {
|
|
1466
|
+
JSXAttribute(node) {
|
|
1467
|
+
const attr = node;
|
|
1468
|
+
if (attr.name?.type !== "JSXIdentifier" || !attr.name.name) return;
|
|
1469
|
+
if (!attr.value || attr.value.type !== "JSXExpressionContainer") return;
|
|
1470
|
+
const expression = attr.value.expression;
|
|
1471
|
+
if (!expression) return;
|
|
1472
|
+
const isInlineArray = expression.type === "ArrayExpression";
|
|
1473
|
+
const isArrayTyped = expressionHasArrayType(expression);
|
|
1474
|
+
if (!isInlineArray && !isArrayTyped) return;
|
|
1475
|
+
context.report({
|
|
1476
|
+
node: attr.value,
|
|
1477
|
+
messageId: "noArrayProps",
|
|
1478
|
+
data: { prop: attr.name.name }
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
var no_array_props_default = rule14;
|
|
1485
|
+
|
|
1486
|
+
// src/rules/no-prefixed-prop-bundles.ts
|
|
1487
|
+
var DEFAULT_THRESHOLD = 3;
|
|
1488
|
+
var IGNORED_PREFIXES = /* @__PURE__ */ new Set(["aria", "can", "data", "has", "is", "on", "should"]);
|
|
1489
|
+
function getBundlePrefix(propName) {
|
|
1490
|
+
if (!propName) return null;
|
|
1491
|
+
const first = propName[0];
|
|
1492
|
+
if (!first || first < "a" || first > "z") return null;
|
|
1493
|
+
let firstUpperIndex = -1;
|
|
1494
|
+
for (let index = 1; index < propName.length; index += 1) {
|
|
1495
|
+
const char = propName[index];
|
|
1496
|
+
if (char >= "A" && char <= "Z") {
|
|
1497
|
+
firstUpperIndex = index;
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (firstUpperIndex <= 0 || firstUpperIndex >= propName.length) return null;
|
|
1502
|
+
const prefix = propName.slice(0, firstUpperIndex);
|
|
1503
|
+
if (!prefix || IGNORED_PREFIXES.has(prefix)) return null;
|
|
1504
|
+
return prefix;
|
|
1505
|
+
}
|
|
1506
|
+
function getPropName4(node) {
|
|
1507
|
+
const key = node.key;
|
|
1508
|
+
if (key.type === "Identifier") return key.name;
|
|
1509
|
+
if (key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
function reportPrefixedPropBundle(context, properties, prefix) {
|
|
1513
|
+
for (const property of properties) {
|
|
1514
|
+
const propName = getPropName4(property);
|
|
1515
|
+
if (!propName) continue;
|
|
1516
|
+
context.report({
|
|
1517
|
+
node: property.key,
|
|
1518
|
+
messageId: "noPrefixedPropBundle",
|
|
1519
|
+
data: {
|
|
1520
|
+
prop: propName,
|
|
1521
|
+
prefix,
|
|
1522
|
+
count: String(properties.length)
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
function checkMembersForPrefixedBundles(context, members, threshold) {
|
|
1528
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1529
|
+
for (const memberNode of members) {
|
|
1530
|
+
const maybeProperty = memberNode;
|
|
1531
|
+
if (maybeProperty.type !== "TSPropertySignature") continue;
|
|
1532
|
+
const property = memberNode;
|
|
1533
|
+
const propName = getPropName4(property);
|
|
1534
|
+
if (!propName) continue;
|
|
1535
|
+
const prefix = getBundlePrefix(propName);
|
|
1536
|
+
if (!prefix) continue;
|
|
1537
|
+
const existing = groups.get(prefix);
|
|
1538
|
+
if (existing) {
|
|
1539
|
+
existing.push(property);
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
groups.set(prefix, [property]);
|
|
1543
|
+
}
|
|
1544
|
+
for (const [prefix, properties] of groups.entries()) {
|
|
1545
|
+
if (properties.length < threshold) continue;
|
|
1546
|
+
reportPrefixedPropBundle(context, properties, prefix);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
function isComponentPropsContractName5(name) {
|
|
1550
|
+
return name?.endsWith("Props") ?? false;
|
|
1551
|
+
}
|
|
1552
|
+
var rule15 = {
|
|
1553
|
+
meta: {
|
|
1554
|
+
type: "suggestion",
|
|
1555
|
+
docs: {
|
|
1556
|
+
description: "Disallow bundles of similarly-prefixed prop type declarations that suggest over-grouped component APIs",
|
|
1557
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-prefixed-prop-bundles.md"
|
|
1558
|
+
},
|
|
1559
|
+
messages: {
|
|
1560
|
+
noPrefixedPropBundle: "Prop '{{prop}}' is part of a '{{prefix}}*' prop bundle ({{count}} props). Prefer unprefixed, explicit props and compound composition. If grouped data must be shared, use context. AI agents: split monolithic props into compound parts instead of introducing object bundles. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-prefixed-prop-bundles.md."
|
|
1561
|
+
},
|
|
1562
|
+
schema: [
|
|
1563
|
+
{
|
|
1564
|
+
type: "object",
|
|
1565
|
+
properties: {
|
|
1566
|
+
threshold: {
|
|
1567
|
+
type: "integer",
|
|
1568
|
+
minimum: 2
|
|
1569
|
+
}
|
|
1570
|
+
},
|
|
1571
|
+
additionalProperties: false
|
|
1572
|
+
}
|
|
1573
|
+
]
|
|
1574
|
+
},
|
|
1575
|
+
create(context) {
|
|
1576
|
+
const options = context.options[0] ?? {};
|
|
1577
|
+
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
1578
|
+
return {
|
|
1579
|
+
TSInterfaceDeclaration(node) {
|
|
1580
|
+
const declaration = node;
|
|
1581
|
+
if (!isComponentPropsContractName5(declaration.id?.name)) return;
|
|
1582
|
+
if (declaration.body?.type !== "TSInterfaceBody") return;
|
|
1583
|
+
checkMembersForPrefixedBundles(context, declaration.body.body ?? [], threshold);
|
|
1584
|
+
},
|
|
1585
|
+
TSTypeAliasDeclaration(node) {
|
|
1586
|
+
const declaration = node;
|
|
1587
|
+
if (!isComponentPropsContractName5(declaration.id?.name)) return;
|
|
1588
|
+
const annotation = declaration.typeAnnotation;
|
|
1589
|
+
if (!annotation || annotation.type !== "TSTypeLiteral") return;
|
|
1590
|
+
checkMembersForPrefixedBundles(context, annotation.members ?? [], threshold);
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
var no_prefixed_prop_bundles_default = rule15;
|
|
1596
|
+
|
|
1597
|
+
// src/rules/no-optional-props-without-defaults.ts
|
|
1598
|
+
import ts2 from "typescript";
|
|
1599
|
+
function getParserServices(context) {
|
|
1600
|
+
return context.sourceCode.parserServices ?? context.parserServices ?? null;
|
|
1601
|
+
}
|
|
1602
|
+
function isPascalCaseName(name) {
|
|
1603
|
+
const first = name[0];
|
|
1604
|
+
return Boolean(first && first >= "A" && first <= "Z");
|
|
1605
|
+
}
|
|
1606
|
+
function getPropertyName(node) {
|
|
1607
|
+
if (node.type === "Identifier") return node.name;
|
|
1608
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
1609
|
+
return null;
|
|
1610
|
+
}
|
|
1611
|
+
function getTypeReferenceName(typeNode) {
|
|
1612
|
+
const maybeRef = typeNode;
|
|
1613
|
+
if (maybeRef.type !== "TSTypeReference" || !maybeRef.typeName) return null;
|
|
1614
|
+
if (maybeRef.typeName.type === "Identifier" && maybeRef.typeName.name) {
|
|
1615
|
+
return maybeRef.typeName.name;
|
|
1616
|
+
}
|
|
1617
|
+
if (maybeRef.typeName.type === "TSQualifiedName" && maybeRef.typeName.right?.name) {
|
|
1618
|
+
return maybeRef.typeName.right.name;
|
|
1619
|
+
}
|
|
1620
|
+
return null;
|
|
1621
|
+
}
|
|
1622
|
+
function collectOptionalPropNamesFromMembers(members) {
|
|
1623
|
+
const optionalPropNames = /* @__PURE__ */ new Set();
|
|
1624
|
+
for (const member of members) {
|
|
1625
|
+
const maybeProperty = member;
|
|
1626
|
+
if (maybeProperty.type !== "TSPropertySignature" || !maybeProperty.optional) continue;
|
|
1627
|
+
const propName = getPropertyName(maybeProperty.key);
|
|
1628
|
+
if (!propName) continue;
|
|
1629
|
+
optionalPropNames.add(propName);
|
|
1630
|
+
}
|
|
1631
|
+
return optionalPropNames;
|
|
1632
|
+
}
|
|
1633
|
+
function getOptionalPropNamesFromTypeNode(typeNode, optionalPropMap) {
|
|
1634
|
+
if (!typeNode) return /* @__PURE__ */ new Set();
|
|
1635
|
+
const maybeTypeLiteral = typeNode;
|
|
1636
|
+
if (maybeTypeLiteral.type === "TSTypeLiteral") {
|
|
1637
|
+
return collectOptionalPropNamesFromMembers(maybeTypeLiteral.members ?? []);
|
|
1638
|
+
}
|
|
1639
|
+
const typeRefName = getTypeReferenceName(typeNode);
|
|
1640
|
+
if (typeRefName) {
|
|
1641
|
+
return new Set(optionalPropMap.get(typeRefName) ?? []);
|
|
1642
|
+
}
|
|
1643
|
+
return /* @__PURE__ */ new Set();
|
|
1644
|
+
}
|
|
1645
|
+
function collectDefaultedParamKeys(paramNode) {
|
|
1646
|
+
const defaultedKeys = /* @__PURE__ */ new Set();
|
|
1647
|
+
const maybeObjectPattern = paramNode;
|
|
1648
|
+
if (maybeObjectPattern.type !== "ObjectPattern") return defaultedKeys;
|
|
1649
|
+
for (const propertyNode of maybeObjectPattern.properties ?? []) {
|
|
1650
|
+
const maybeProperty = propertyNode;
|
|
1651
|
+
if (maybeProperty.type !== "Property") continue;
|
|
1652
|
+
const propName = getPropertyName(maybeProperty.key);
|
|
1653
|
+
if (!propName) continue;
|
|
1654
|
+
const maybeValue = maybeProperty.value;
|
|
1655
|
+
if (!maybeValue || maybeValue.type !== "AssignmentPattern") continue;
|
|
1656
|
+
defaultedKeys.add(propName);
|
|
1657
|
+
}
|
|
1658
|
+
return defaultedKeys;
|
|
1659
|
+
}
|
|
1660
|
+
function getParamTypeAnnotationNode(paramNode) {
|
|
1661
|
+
const maybeAnnotatedParam = paramNode;
|
|
1662
|
+
if (maybeAnnotatedParam.typeAnnotation?.typeAnnotation) {
|
|
1663
|
+
return maybeAnnotatedParam.typeAnnotation.typeAnnotation;
|
|
1664
|
+
}
|
|
1665
|
+
const maybeAssignment = paramNode;
|
|
1666
|
+
if (maybeAssignment.type !== "AssignmentPattern") return null;
|
|
1667
|
+
const maybeAssignmentLeft = maybeAssignment.left;
|
|
1668
|
+
return maybeAssignmentLeft.typeAnnotation?.typeAnnotation ?? null;
|
|
1669
|
+
}
|
|
1670
|
+
function getComponentOptionalPropNamesFromTypeChecker(paramNode, checker, parserServices) {
|
|
1671
|
+
if (!checker || !parserServices?.esTreeNodeToTSNodeMap) return /* @__PURE__ */ new Set();
|
|
1672
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(paramNode);
|
|
1673
|
+
if (!tsNode) return /* @__PURE__ */ new Set();
|
|
1674
|
+
const paramType = checker.getTypeAtLocation(tsNode);
|
|
1675
|
+
const optionalPropNames = /* @__PURE__ */ new Set();
|
|
1676
|
+
for (const symbol of checker.getPropertiesOfType(paramType)) {
|
|
1677
|
+
if ((symbol.flags & ts2.SymbolFlags.Optional) === 0) continue;
|
|
1678
|
+
const propName = symbol.getName();
|
|
1679
|
+
if (!propName || propName.startsWith("__@")) continue;
|
|
1680
|
+
optionalPropNames.add(propName);
|
|
1681
|
+
}
|
|
1682
|
+
return optionalPropNames;
|
|
1683
|
+
}
|
|
1684
|
+
var rule16 = {
|
|
1685
|
+
meta: {
|
|
1686
|
+
type: "suggestion",
|
|
1687
|
+
docs: {
|
|
1688
|
+
description: "Disallow optional component props unless they are defaulted at the component boundary (more accurate when TypeScript type information is available)",
|
|
1689
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-optional-props-without-defaults.md"
|
|
1690
|
+
},
|
|
1691
|
+
messages: {
|
|
1692
|
+
noOptionalPropWithoutDefault: "Component '{{component}}' prop '{{prop}}' is optional without a default at the component boundary. Default configurable props, and keep time-based readiness checks outside component prop contracts. AI agents: default this prop at the parameter boundary or move readiness checks upstream. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-optional-props-without-defaults.md."
|
|
1693
|
+
},
|
|
1694
|
+
schema: []
|
|
1695
|
+
},
|
|
1696
|
+
create(context) {
|
|
1697
|
+
const parserServices = getParserServices(context);
|
|
1698
|
+
const checker = parserServices?.program?.getTypeChecker();
|
|
1699
|
+
const hasTypeInformation = Boolean(checker && parserServices?.esTreeNodeToTSNodeMap);
|
|
1700
|
+
const optionalPropsByTypeName = /* @__PURE__ */ new Map();
|
|
1701
|
+
function storeTypeOptionalProps(typeName, members) {
|
|
1702
|
+
optionalPropsByTypeName.set(typeName, collectOptionalPropNamesFromMembers(members));
|
|
1703
|
+
}
|
|
1704
|
+
function reportOptionalPropsWithoutDefaults(componentName, paramNode) {
|
|
1705
|
+
const defaultedKeys = collectDefaultedParamKeys(paramNode);
|
|
1706
|
+
const paramTypeAnnotation = getParamTypeAnnotationNode(paramNode);
|
|
1707
|
+
const optionalPropNamesFromAst = getOptionalPropNamesFromTypeNode(
|
|
1708
|
+
paramTypeAnnotation ?? void 0,
|
|
1709
|
+
optionalPropsByTypeName
|
|
1710
|
+
);
|
|
1711
|
+
const optionalPropNamesFromChecker = getComponentOptionalPropNamesFromTypeChecker(
|
|
1712
|
+
paramNode,
|
|
1713
|
+
hasTypeInformation ? checker : void 0,
|
|
1714
|
+
hasTypeInformation ? parserServices : null
|
|
1715
|
+
);
|
|
1716
|
+
const optionalPropNames = /* @__PURE__ */ new Set([
|
|
1717
|
+
...optionalPropNamesFromAst,
|
|
1718
|
+
...optionalPropNamesFromChecker
|
|
1719
|
+
]);
|
|
1720
|
+
for (const propName of optionalPropNames) {
|
|
1721
|
+
if (defaultedKeys.has(propName)) continue;
|
|
1722
|
+
context.report({
|
|
1723
|
+
node: paramNode,
|
|
1724
|
+
messageId: "noOptionalPropWithoutDefault",
|
|
1725
|
+
data: {
|
|
1726
|
+
component: componentName,
|
|
1727
|
+
prop: propName
|
|
1728
|
+
}
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
return {
|
|
1733
|
+
TSInterfaceDeclaration(node) {
|
|
1734
|
+
const declaration = node;
|
|
1735
|
+
const typeName = declaration.id?.name;
|
|
1736
|
+
if (!typeName) return;
|
|
1737
|
+
if (declaration.body?.type !== "TSInterfaceBody") return;
|
|
1738
|
+
storeTypeOptionalProps(typeName, declaration.body.body ?? []);
|
|
1739
|
+
},
|
|
1740
|
+
TSTypeAliasDeclaration(node) {
|
|
1741
|
+
const declaration = node;
|
|
1742
|
+
const typeName = declaration.id?.name;
|
|
1743
|
+
if (!typeName) return;
|
|
1744
|
+
const typeAnnotation = declaration.typeAnnotation;
|
|
1745
|
+
if (typeAnnotation?.type !== "TSTypeLiteral") return;
|
|
1746
|
+
storeTypeOptionalProps(typeName, typeAnnotation.members ?? []);
|
|
1747
|
+
},
|
|
1748
|
+
FunctionDeclaration(node) {
|
|
1749
|
+
const declaration = node;
|
|
1750
|
+
const componentName = declaration.id?.name;
|
|
1751
|
+
if (!componentName || !isPascalCaseName(componentName)) return;
|
|
1752
|
+
const firstParam = declaration.params?.[0];
|
|
1753
|
+
if (!firstParam) return;
|
|
1754
|
+
reportOptionalPropsWithoutDefaults(componentName, firstParam);
|
|
1755
|
+
},
|
|
1756
|
+
VariableDeclarator(node) {
|
|
1757
|
+
const declaration = node;
|
|
1758
|
+
const componentName = declaration.id?.name;
|
|
1759
|
+
if (!componentName || !isPascalCaseName(componentName)) return;
|
|
1760
|
+
const init = declaration.init;
|
|
1761
|
+
if (!init) return;
|
|
1762
|
+
if (init.type !== "ArrowFunctionExpression" && init.type !== "FunctionExpression") return;
|
|
1763
|
+
const firstParam = init.params?.[0];
|
|
1764
|
+
if (!firstParam) return;
|
|
1765
|
+
reportOptionalPropsWithoutDefaults(componentName, firstParam);
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
var no_optional_props_without_defaults_default = rule16;
|
|
1771
|
+
|
|
1772
|
+
// src/rules/no-boolean-capability-props.ts
|
|
1773
|
+
function getPropName5(node) {
|
|
1774
|
+
if (node.type === "Identifier") return node.name;
|
|
1775
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
1776
|
+
return null;
|
|
1777
|
+
}
|
|
1778
|
+
function isBooleanLikeType(typeNode) {
|
|
1779
|
+
if (!typeNode) return false;
|
|
1780
|
+
const maybeType = typeNode;
|
|
1781
|
+
if (maybeType.type === "TSBooleanKeyword") return true;
|
|
1782
|
+
if (maybeType.type === "TSLiteralType") {
|
|
1783
|
+
const literalValue = maybeType.literal?.value;
|
|
1784
|
+
return literalValue === true || literalValue === false;
|
|
1785
|
+
}
|
|
1786
|
+
if (maybeType.type === "TSParenthesizedType" || maybeType.type === "TSOptionalType") {
|
|
1787
|
+
return isBooleanLikeType(maybeType.typeAnnotation);
|
|
1788
|
+
}
|
|
1789
|
+
if (maybeType.type === "TSUnionType" || maybeType.type === "TSIntersectionType") {
|
|
1790
|
+
return (maybeType.types ?? []).some((member) => isBooleanLikeType(member));
|
|
1791
|
+
}
|
|
1792
|
+
return false;
|
|
1793
|
+
}
|
|
1794
|
+
function isHandlerPropName(name) {
|
|
1795
|
+
if (!name.startsWith("on") || name.length <= 2) return false;
|
|
1796
|
+
const firstCharAfterPrefix = name[2];
|
|
1797
|
+
return firstCharAfterPrefix >= "A" && firstCharAfterPrefix <= "Z";
|
|
1798
|
+
}
|
|
1799
|
+
function isFunctionLikeType(typeNode) {
|
|
1800
|
+
if (!typeNode) return false;
|
|
1801
|
+
const maybeType = typeNode;
|
|
1802
|
+
if (maybeType.type === "TSFunctionType") return true;
|
|
1803
|
+
if (maybeType.type === "TSParenthesizedType" || maybeType.type === "TSOptionalType") {
|
|
1804
|
+
return isFunctionLikeType(maybeType.typeAnnotation);
|
|
1805
|
+
}
|
|
1806
|
+
if (maybeType.type === "TSUnionType" || maybeType.type === "TSIntersectionType") {
|
|
1807
|
+
return (maybeType.types ?? []).some((member) => isFunctionLikeType(member));
|
|
1808
|
+
}
|
|
1809
|
+
return false;
|
|
1810
|
+
}
|
|
1811
|
+
function capitalize(value) {
|
|
1812
|
+
if (!value) return value;
|
|
1813
|
+
return value[0].toUpperCase() + value.slice(1);
|
|
1814
|
+
}
|
|
1815
|
+
function isComponentPropsContractName6(name) {
|
|
1816
|
+
return name?.endsWith("Props") ?? false;
|
|
1817
|
+
}
|
|
1818
|
+
var rule17 = {
|
|
1819
|
+
meta: {
|
|
1820
|
+
type: "suggestion",
|
|
1821
|
+
docs: {
|
|
1822
|
+
description: "Disallow boolean component props without associated control handlers in prop contracts",
|
|
1823
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-boolean-capability-props.md"
|
|
1824
|
+
},
|
|
1825
|
+
messages: {
|
|
1826
|
+
noBooleanCapabilityProp: "Boolean prop '{{prop}}' has no associated control handler starting with '{{handlerPrefix}}'. Each boolean doubles possible states and adds hidden variants. Prefer explicit handlers or compound composition. AI agents: add a matching `on{Prop}` handler or split this into composed variants. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-boolean-capability-props.md."
|
|
1827
|
+
},
|
|
1828
|
+
schema: []
|
|
1829
|
+
},
|
|
1830
|
+
create(context) {
|
|
1831
|
+
function checkMembers(members) {
|
|
1832
|
+
const booleanProps = [];
|
|
1833
|
+
const handlerProps = /* @__PURE__ */ new Set();
|
|
1834
|
+
for (const memberNode of members) {
|
|
1835
|
+
const maybeProperty = memberNode;
|
|
1836
|
+
if (maybeProperty.type !== "TSPropertySignature") continue;
|
|
1837
|
+
const propName = getPropName5(maybeProperty.key);
|
|
1838
|
+
if (!propName) continue;
|
|
1839
|
+
const typeNode = maybeProperty.typeAnnotation?.typeAnnotation;
|
|
1840
|
+
if (isFunctionLikeType(typeNode) && isHandlerPropName(propName)) {
|
|
1841
|
+
handlerProps.add(propName);
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
if (!isBooleanLikeType(typeNode)) continue;
|
|
1845
|
+
booleanProps.push({ name: propName, node: maybeProperty });
|
|
1846
|
+
}
|
|
1847
|
+
for (const booleanProp of booleanProps) {
|
|
1848
|
+
const handlerPrefix = `on${capitalize(booleanProp.name)}`;
|
|
1849
|
+
const hasAssociatedHandler = [...handlerProps].some(
|
|
1850
|
+
(handlerName) => handlerName.startsWith(handlerPrefix)
|
|
1851
|
+
);
|
|
1852
|
+
if (hasAssociatedHandler) continue;
|
|
1853
|
+
context.report({
|
|
1854
|
+
node: booleanProp.node.key,
|
|
1855
|
+
messageId: "noBooleanCapabilityProp",
|
|
1856
|
+
data: { prop: booleanProp.name, handlerPrefix }
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
return {
|
|
1861
|
+
TSInterfaceDeclaration(node) {
|
|
1862
|
+
const declaration = node;
|
|
1863
|
+
if (!isComponentPropsContractName6(declaration.id?.name)) return;
|
|
1864
|
+
if (declaration.body?.type !== "TSInterfaceBody") return;
|
|
1865
|
+
checkMembers(declaration.body.body ?? []);
|
|
1866
|
+
},
|
|
1867
|
+
TSTypeAliasDeclaration(node) {
|
|
1868
|
+
const declaration = node;
|
|
1869
|
+
if (!isComponentPropsContractName6(declaration.id?.name)) return;
|
|
1870
|
+
const typeAnnotation = declaration.typeAnnotation;
|
|
1871
|
+
if (!typeAnnotation || typeAnnotation.type !== "TSTypeLiteral") return;
|
|
1872
|
+
checkMembers(typeAnnotation.members ?? []);
|
|
1873
|
+
}
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1877
|
+
var no_boolean_capability_props_default = rule17;
|
|
1878
|
+
|
|
1879
|
+
// src/rules/max-custom-props.ts
|
|
1880
|
+
var DEFAULT_THRESHOLD2 = 8;
|
|
1881
|
+
var DEFAULT_IGNORED_PROPS = /* @__PURE__ */ new Set(["children"]);
|
|
1882
|
+
function isComponentPropsContractName7(name) {
|
|
1883
|
+
return name.endsWith("Props");
|
|
1884
|
+
}
|
|
1885
|
+
function getCustomPropCount(members, ignoredProps) {
|
|
1886
|
+
let count = 0;
|
|
1887
|
+
for (const member of members) {
|
|
1888
|
+
const maybeMember = member;
|
|
1889
|
+
if (maybeMember.type !== "TSPropertySignature" && maybeMember.type !== "TSMethodSignature") {
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
const key = maybeMember.key;
|
|
1893
|
+
if (!key) continue;
|
|
1894
|
+
let propName = null;
|
|
1895
|
+
if (key.type === "Identifier" && key.name) propName = key.name;
|
|
1896
|
+
if (key.type === "Literal" && typeof key.value === "string") propName = key.value;
|
|
1897
|
+
if (!propName) continue;
|
|
1898
|
+
if (ignoredProps.has(propName)) continue;
|
|
1899
|
+
count += 1;
|
|
1900
|
+
}
|
|
1901
|
+
return count;
|
|
1902
|
+
}
|
|
1903
|
+
var rule18 = {
|
|
1904
|
+
meta: {
|
|
1905
|
+
type: "suggestion",
|
|
1906
|
+
docs: {
|
|
1907
|
+
description: "Limit the number of custom props in component prop contracts to encourage composition",
|
|
1908
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/max-custom-props.md"
|
|
1909
|
+
},
|
|
1910
|
+
messages: {
|
|
1911
|
+
maxCustomProps: "Component prop contract '{{name}}' declares {{count}} custom props (max {{threshold}}). This is a composition pressure signal; prefer splitting APIs into compound parts. AI agents: extract parts before adding more top-level props. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/max-custom-props.md."
|
|
1912
|
+
},
|
|
1913
|
+
schema: [
|
|
1914
|
+
{
|
|
1915
|
+
type: "object",
|
|
1916
|
+
properties: {
|
|
1917
|
+
threshold: {
|
|
1918
|
+
type: "integer",
|
|
1919
|
+
minimum: 1
|
|
1920
|
+
},
|
|
1921
|
+
ignore: {
|
|
1922
|
+
type: "array",
|
|
1923
|
+
items: {
|
|
1924
|
+
type: "string"
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
},
|
|
1928
|
+
additionalProperties: false
|
|
1929
|
+
}
|
|
1930
|
+
]
|
|
1931
|
+
},
|
|
1932
|
+
create(context) {
|
|
1933
|
+
const options = context.options[0] ?? {};
|
|
1934
|
+
const threshold = options.threshold ?? DEFAULT_THRESHOLD2;
|
|
1935
|
+
const ignoredProps = /* @__PURE__ */ new Set([...DEFAULT_IGNORED_PROPS, ...options.ignore ?? []]);
|
|
1936
|
+
function checkContract(contractName, members, node) {
|
|
1937
|
+
if (!contractName || !isComponentPropsContractName7(contractName)) return;
|
|
1938
|
+
const customPropCount = getCustomPropCount(members ?? [], ignoredProps);
|
|
1939
|
+
if (customPropCount <= threshold) return;
|
|
1940
|
+
context.report({
|
|
1941
|
+
node,
|
|
1942
|
+
messageId: "maxCustomProps",
|
|
1943
|
+
data: {
|
|
1944
|
+
name: contractName,
|
|
1945
|
+
count: String(customPropCount),
|
|
1946
|
+
threshold: String(threshold)
|
|
1947
|
+
}
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
return {
|
|
1951
|
+
TSInterfaceDeclaration(node) {
|
|
1952
|
+
const declaration = node;
|
|
1953
|
+
if (declaration.body?.type !== "TSInterfaceBody") return;
|
|
1954
|
+
checkContract(declaration.id?.name, declaration.body.body, node);
|
|
1955
|
+
},
|
|
1956
|
+
TSTypeAliasDeclaration(node) {
|
|
1957
|
+
const declaration = node;
|
|
1958
|
+
if (declaration.typeAnnotation?.type !== "TSTypeLiteral") return;
|
|
1959
|
+
checkContract(declaration.id?.name, declaration.typeAnnotation.members, node);
|
|
1960
|
+
}
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
var max_custom_props_default = rule18;
|
|
1965
|
+
|
|
1966
|
+
// src/rules/jsx-bem-compound-naming.ts
|
|
1967
|
+
import path from "path";
|
|
1968
|
+
function isPascalCase2(name) {
|
|
1969
|
+
if (name.length === 0) return false;
|
|
1970
|
+
const first = name[0];
|
|
1971
|
+
return first >= "A" && first <= "Z";
|
|
1972
|
+
}
|
|
1973
|
+
function isFunctionLikeComponentInit(node) {
|
|
1974
|
+
if (!node) return false;
|
|
1975
|
+
const typed = node;
|
|
1976
|
+
return typed.type === "ArrowFunctionExpression" || typed.type === "FunctionExpression";
|
|
1977
|
+
}
|
|
1978
|
+
function normalizeForComparison(value) {
|
|
1979
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1980
|
+
}
|
|
1981
|
+
function buildExampleName(block, localName) {
|
|
1982
|
+
if (localName.startsWith(block)) return localName;
|
|
1983
|
+
let overlapLength = 0;
|
|
1984
|
+
for (let index = 1; index < block.length; index += 1) {
|
|
1985
|
+
const suffix = block.slice(index);
|
|
1986
|
+
if (localName.startsWith(suffix)) {
|
|
1987
|
+
overlapLength = suffix.length;
|
|
1988
|
+
break;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
if (overlapLength === 0) return `${block}${localName}`;
|
|
1992
|
+
return `${block}${localName.slice(overlapLength)}`;
|
|
1993
|
+
}
|
|
1994
|
+
function getFileStem(filename) {
|
|
1995
|
+
if (!filename || filename === "<input>") return null;
|
|
1996
|
+
const base = path.basename(filename);
|
|
1997
|
+
const ext = path.extname(base);
|
|
1998
|
+
if (!ext) return base;
|
|
1999
|
+
return base.slice(0, -ext.length);
|
|
2000
|
+
}
|
|
2001
|
+
function getExportedIdentifierName(node) {
|
|
2002
|
+
if (!node) return null;
|
|
2003
|
+
const typedNode = node;
|
|
2004
|
+
if (typedNode.type === "Identifier" && typedNode.name) return typedNode.name;
|
|
2005
|
+
if (typedNode.type === "Literal" && typeof typedNode.value === "string") return typedNode.value;
|
|
2006
|
+
return null;
|
|
2007
|
+
}
|
|
2008
|
+
var rule19 = {
|
|
2009
|
+
meta: {
|
|
2010
|
+
type: "suggestion",
|
|
2011
|
+
docs: {
|
|
2012
|
+
description: "Enforce compound component export naming by matching exported component names to the file stem block prefix",
|
|
2013
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/jsx-bem-compound-naming.md"
|
|
2014
|
+
},
|
|
2015
|
+
messages: {
|
|
2016
|
+
exportedPartMustUseBlockPrefix: "Exported component '{{name}}' should be prefixed with compound block '{{block}}' in this file (for example '{{example}}'). See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/jsx-bem-compound-naming.md."
|
|
2017
|
+
},
|
|
2018
|
+
schema: []
|
|
2019
|
+
},
|
|
2020
|
+
create(context) {
|
|
2021
|
+
const localComponents = /* @__PURE__ */ new Map();
|
|
2022
|
+
const exportedComponents = [];
|
|
2023
|
+
function registerLocalComponent(name, node) {
|
|
2024
|
+
if (!isPascalCase2(name)) return;
|
|
2025
|
+
localComponents.set(name, node);
|
|
2026
|
+
}
|
|
2027
|
+
function recordExport(localName, exportedName, node) {
|
|
2028
|
+
if (!localComponents.has(localName)) return;
|
|
2029
|
+
if (!isPascalCase2(exportedName)) return;
|
|
2030
|
+
exportedComponents.push({ localName, exportedName, node });
|
|
2031
|
+
}
|
|
2032
|
+
return {
|
|
2033
|
+
FunctionDeclaration(node) {
|
|
2034
|
+
const fn = node;
|
|
2035
|
+
const idName = getExportedIdentifierName(fn.id);
|
|
2036
|
+
if (!idName) return;
|
|
2037
|
+
registerLocalComponent(idName, fn.id);
|
|
2038
|
+
},
|
|
2039
|
+
VariableDeclarator(node) {
|
|
2040
|
+
const declaration = node;
|
|
2041
|
+
if (!isFunctionLikeComponentInit(declaration.init)) return;
|
|
2042
|
+
const idName = getExportedIdentifierName(declaration.id);
|
|
2043
|
+
if (!idName || !declaration.id) return;
|
|
2044
|
+
registerLocalComponent(idName, declaration.id);
|
|
2045
|
+
},
|
|
2046
|
+
ExportNamedDeclaration(node) {
|
|
2047
|
+
const declaration = node;
|
|
2048
|
+
if (declaration.source) return;
|
|
2049
|
+
if (declaration.declaration?.type === "FunctionDeclaration") {
|
|
2050
|
+
const fn = declaration.declaration;
|
|
2051
|
+
const idName = getExportedIdentifierName(fn.id);
|
|
2052
|
+
if (!idName || !fn.id) return;
|
|
2053
|
+
registerLocalComponent(idName, fn.id);
|
|
2054
|
+
recordExport(idName, idName, fn.id);
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
if (declaration.declaration?.type === "VariableDeclaration") {
|
|
2058
|
+
const variableDeclaration = declaration.declaration;
|
|
2059
|
+
for (const entry of variableDeclaration.declarations ?? []) {
|
|
2060
|
+
if (!isFunctionLikeComponentInit(entry.init)) continue;
|
|
2061
|
+
const idName = getExportedIdentifierName(entry.id);
|
|
2062
|
+
if (!idName || !entry.id) continue;
|
|
2063
|
+
registerLocalComponent(idName, entry.id);
|
|
2064
|
+
recordExport(idName, idName, entry.id);
|
|
2065
|
+
}
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
for (const specifier of declaration.specifiers ?? []) {
|
|
2069
|
+
if (specifier.type !== "ExportSpecifier") continue;
|
|
2070
|
+
const exportSpecifier = specifier;
|
|
2071
|
+
const localName = getExportedIdentifierName(exportSpecifier.local);
|
|
2072
|
+
const exportedName = getExportedIdentifierName(exportSpecifier.exported);
|
|
2073
|
+
if (!localName || !exportedName) continue;
|
|
2074
|
+
if (!exportSpecifier.exported) continue;
|
|
2075
|
+
recordExport(localName, exportedName, exportSpecifier.exported);
|
|
2076
|
+
}
|
|
2077
|
+
},
|
|
2078
|
+
ExportDefaultDeclaration(node) {
|
|
2079
|
+
const declaration = node;
|
|
2080
|
+
const declared = declaration.declaration;
|
|
2081
|
+
if (!declared) return;
|
|
2082
|
+
if (declared.type === "FunctionDeclaration") {
|
|
2083
|
+
const fn = declared;
|
|
2084
|
+
const idName = getExportedIdentifierName(fn.id);
|
|
2085
|
+
if (!idName || !fn.id) return;
|
|
2086
|
+
registerLocalComponent(idName, fn.id);
|
|
2087
|
+
recordExport(idName, idName, fn.id);
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
if (declared.type === "Identifier") {
|
|
2091
|
+
const idName = getExportedIdentifierName(declared);
|
|
2092
|
+
if (!idName) return;
|
|
2093
|
+
recordExport(idName, idName, declared);
|
|
2094
|
+
}
|
|
2095
|
+
},
|
|
2096
|
+
"Program:exit"() {
|
|
2097
|
+
if (exportedComponents.length < 2) return;
|
|
2098
|
+
const stem = getFileStem(context.filename);
|
|
2099
|
+
if (!stem) return;
|
|
2100
|
+
if (stem.toLowerCase() === "index") return;
|
|
2101
|
+
const normalizedStem = normalizeForComparison(stem);
|
|
2102
|
+
const blockCandidates = exportedComponents.map((entry) => entry.localName).filter((name) => normalizeForComparison(name) === normalizedStem);
|
|
2103
|
+
if (blockCandidates.length === 0) return;
|
|
2104
|
+
for (const exported of exportedComponents) {
|
|
2105
|
+
const isBlockExport = blockCandidates.includes(exported.localName);
|
|
2106
|
+
if (isBlockExport) continue;
|
|
2107
|
+
const usesAnyBlockPrefix = blockCandidates.some(
|
|
2108
|
+
(block) => exported.localName.startsWith(block)
|
|
2109
|
+
);
|
|
2110
|
+
if (usesAnyBlockPrefix) continue;
|
|
2111
|
+
const preferredBlock = blockCandidates[0] ?? "Block";
|
|
2112
|
+
context.report({
|
|
2113
|
+
node: exported.node,
|
|
2114
|
+
messageId: "exportedPartMustUseBlockPrefix",
|
|
2115
|
+
data: {
|
|
2116
|
+
name: exported.localName,
|
|
2117
|
+
block: preferredBlock,
|
|
2118
|
+
example: buildExampleName(preferredBlock, exported.localName)
|
|
2119
|
+
}
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
};
|
|
2126
|
+
var jsx_bem_compound_naming_default = rule19;
|
|
2127
|
+
|
|
2128
|
+
// src/rules/jsx-compound-part-export-naming.ts
|
|
2129
|
+
import path2 from "path";
|
|
2130
|
+
function isPascalCase3(name) {
|
|
2131
|
+
if (name.length === 0) return false;
|
|
2132
|
+
const first = name[0];
|
|
2133
|
+
return first >= "A" && first <= "Z";
|
|
2134
|
+
}
|
|
2135
|
+
function normalizeForComparison2(value) {
|
|
2136
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2137
|
+
}
|
|
2138
|
+
function getFileStem2(filename) {
|
|
2139
|
+
if (!filename || filename === "<input>") return null;
|
|
2140
|
+
const base = path2.basename(filename);
|
|
2141
|
+
const ext = path2.extname(base);
|
|
2142
|
+
if (!ext) return base;
|
|
2143
|
+
return base.slice(0, -ext.length);
|
|
2144
|
+
}
|
|
2145
|
+
function isFunctionLikeComponentInit2(node) {
|
|
2146
|
+
if (!node) return false;
|
|
2147
|
+
const typed = node;
|
|
2148
|
+
return typed.type === "ArrowFunctionExpression" || typed.type === "FunctionExpression";
|
|
2149
|
+
}
|
|
2150
|
+
function getIdentifierLikeName(node) {
|
|
2151
|
+
if (!node) return null;
|
|
2152
|
+
const typedNode = node;
|
|
2153
|
+
if (typedNode.type === "Identifier" && typedNode.name) return typedNode.name;
|
|
2154
|
+
if (typedNode.type === "Literal" && typeof typedNode.value === "string") return typedNode.value;
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
var rule20 = {
|
|
2158
|
+
meta: {
|
|
2159
|
+
type: "suggestion",
|
|
2160
|
+
docs: {
|
|
2161
|
+
description: "Enforce compound export aliasing from file-stem block and disallow runtime object export APIs",
|
|
2162
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/jsx-compound-part-export-naming.md"
|
|
2163
|
+
},
|
|
2164
|
+
messages: {
|
|
2165
|
+
requirePartAlias: "Export part '{{local}}' as '{{part}}' for block '{{block}}' (export { {{local}} as {{part}} }). See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/jsx-compound-part-export-naming.md.",
|
|
2166
|
+
requireRootExport: "Compound block '{{block}}' exports parts. Also export its root namespace as `export { {{block}} as Root }`. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/jsx-compound-part-export-naming.md.",
|
|
2167
|
+
requireRootAlias: "Export block '{{block}}' as 'Root' (export { {{block}} as Root }). See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/jsx-compound-part-export-naming.md.",
|
|
2168
|
+
noRuntimeObjectExport: "Avoid exporting runtime object '{{name}}' for compound APIs. Export root/parts with aliases instead. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/jsx-compound-part-export-naming.md."
|
|
2169
|
+
},
|
|
2170
|
+
schema: []
|
|
2171
|
+
},
|
|
2172
|
+
create(context) {
|
|
2173
|
+
const localComponents = /* @__PURE__ */ new Set();
|
|
2174
|
+
const exports = [];
|
|
2175
|
+
const objectExports = [];
|
|
2176
|
+
function registerLocalComponent(name) {
|
|
2177
|
+
if (!isPascalCase3(name)) return;
|
|
2178
|
+
localComponents.add(name);
|
|
2179
|
+
}
|
|
2180
|
+
function recordExport(localName, exportedName, node) {
|
|
2181
|
+
exports.push({ localName, exportedName, node });
|
|
2182
|
+
}
|
|
2183
|
+
return {
|
|
2184
|
+
FunctionDeclaration(node) {
|
|
2185
|
+
const fn = node;
|
|
2186
|
+
const idName = getIdentifierLikeName(fn.id);
|
|
2187
|
+
if (!idName) return;
|
|
2188
|
+
registerLocalComponent(idName);
|
|
2189
|
+
},
|
|
2190
|
+
VariableDeclarator(node) {
|
|
2191
|
+
const declaration = node;
|
|
2192
|
+
if (!isFunctionLikeComponentInit2(declaration.init)) return;
|
|
2193
|
+
const idName = getIdentifierLikeName(declaration.id);
|
|
2194
|
+
if (!idName) return;
|
|
2195
|
+
registerLocalComponent(idName);
|
|
2196
|
+
},
|
|
2197
|
+
ExportNamedDeclaration(node) {
|
|
2198
|
+
const declaration = node;
|
|
2199
|
+
if (declaration.source) return;
|
|
2200
|
+
if (declaration.declaration?.type === "FunctionDeclaration") {
|
|
2201
|
+
const fn = declaration.declaration;
|
|
2202
|
+
const idName = getIdentifierLikeName(fn.id);
|
|
2203
|
+
if (!idName || !fn.id) return;
|
|
2204
|
+
registerLocalComponent(idName);
|
|
2205
|
+
recordExport(idName, idName, fn.id);
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
if (declaration.declaration?.type === "VariableDeclaration") {
|
|
2209
|
+
const variableDeclaration = declaration.declaration;
|
|
2210
|
+
for (const entry of variableDeclaration.declarations ?? []) {
|
|
2211
|
+
const idName = getIdentifierLikeName(entry.id);
|
|
2212
|
+
if (!idName || !entry.id) continue;
|
|
2213
|
+
if (entry.init?.type === "ObjectExpression" && isPascalCase3(idName)) {
|
|
2214
|
+
objectExports.push({ name: idName, node: entry.id });
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2217
|
+
if (!isFunctionLikeComponentInit2(entry.init)) continue;
|
|
2218
|
+
registerLocalComponent(idName);
|
|
2219
|
+
recordExport(idName, idName, entry.id);
|
|
2220
|
+
}
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
for (const specifier of declaration.specifiers ?? []) {
|
|
2224
|
+
if (specifier.type !== "ExportSpecifier") continue;
|
|
2225
|
+
const exportSpecifier = specifier;
|
|
2226
|
+
const localName = getIdentifierLikeName(exportSpecifier.local);
|
|
2227
|
+
const exportedName = getIdentifierLikeName(exportSpecifier.exported);
|
|
2228
|
+
if (!localName || !exportedName || !exportSpecifier.exported) continue;
|
|
2229
|
+
recordExport(localName, exportedName, exportSpecifier.exported);
|
|
2230
|
+
}
|
|
2231
|
+
},
|
|
2232
|
+
ExportDefaultDeclaration(node) {
|
|
2233
|
+
const declaration = node;
|
|
2234
|
+
const declared = declaration.declaration;
|
|
2235
|
+
if (!declared) return;
|
|
2236
|
+
if (declared.type === "FunctionDeclaration") {
|
|
2237
|
+
const fn = declared;
|
|
2238
|
+
const idName = getIdentifierLikeName(fn.id);
|
|
2239
|
+
if (!idName || !fn.id) return;
|
|
2240
|
+
registerLocalComponent(idName);
|
|
2241
|
+
recordExport(idName, idName, fn.id);
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
if (declared.type === "Identifier") {
|
|
2245
|
+
const idName = getIdentifierLikeName(declared);
|
|
2246
|
+
if (!idName) return;
|
|
2247
|
+
recordExport(idName, idName, declared);
|
|
2248
|
+
}
|
|
2249
|
+
},
|
|
2250
|
+
"Program:exit"() {
|
|
2251
|
+
const stem = getFileStem2(context.filename);
|
|
2252
|
+
if (!stem) return;
|
|
2253
|
+
if (stem.toLowerCase() === "index") return;
|
|
2254
|
+
const componentExports = exports.filter(
|
|
2255
|
+
(entry) => localComponents.has(entry.localName) && isPascalCase3(entry.localName)
|
|
2256
|
+
);
|
|
2257
|
+
if (componentExports.length < 2) return;
|
|
2258
|
+
const normalizedStem = normalizeForComparison2(stem);
|
|
2259
|
+
const blockCandidates = [
|
|
2260
|
+
.../* @__PURE__ */ new Set([...localComponents, ...objectExports.map((e) => e.name)])
|
|
2261
|
+
].filter((name) => normalizeForComparison2(name) === normalizedStem);
|
|
2262
|
+
if (blockCandidates.length === 0) return;
|
|
2263
|
+
const block = blockCandidates[0];
|
|
2264
|
+
if (!block) return;
|
|
2265
|
+
const blockExports = componentExports.filter((entry) => entry.localName === block);
|
|
2266
|
+
const partExports = componentExports.filter(
|
|
2267
|
+
(entry) => entry.localName !== block && entry.localName.startsWith(block)
|
|
2268
|
+
);
|
|
2269
|
+
for (const partExport of partExports) {
|
|
2270
|
+
const expectedPartAlias = partExport.localName.slice(block.length);
|
|
2271
|
+
if (expectedPartAlias.length === 0) continue;
|
|
2272
|
+
if (partExport.exportedName === expectedPartAlias) continue;
|
|
2273
|
+
context.report({
|
|
2274
|
+
node: partExport.node,
|
|
2275
|
+
messageId: "requirePartAlias",
|
|
2276
|
+
data: {
|
|
2277
|
+
local: partExport.localName,
|
|
2278
|
+
part: expectedPartAlias,
|
|
2279
|
+
block
|
|
2280
|
+
}
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
if (partExports.length > 0) {
|
|
2284
|
+
if (blockExports.length === 0) {
|
|
2285
|
+
const firstPartExport = partExports[0];
|
|
2286
|
+
if (!firstPartExport) return;
|
|
2287
|
+
context.report({
|
|
2288
|
+
node: firstPartExport.node,
|
|
2289
|
+
messageId: "requireRootExport",
|
|
2290
|
+
data: { block }
|
|
2291
|
+
});
|
|
2292
|
+
} else {
|
|
2293
|
+
const hasRootAlias = blockExports.some((entry) => entry.exportedName === "Root");
|
|
2294
|
+
if (!hasRootAlias) {
|
|
2295
|
+
const firstBlockExport = blockExports[0];
|
|
2296
|
+
if (!firstBlockExport) return;
|
|
2297
|
+
context.report({
|
|
2298
|
+
node: firstBlockExport.node,
|
|
2299
|
+
messageId: "requireRootAlias",
|
|
2300
|
+
data: { block }
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
if (partExports.length === 0) return;
|
|
2306
|
+
for (const objectExport of objectExports) {
|
|
2307
|
+
if (objectExport.name !== block) continue;
|
|
2308
|
+
context.report({
|
|
2309
|
+
node: objectExport.node,
|
|
2310
|
+
messageId: "noRuntimeObjectExport",
|
|
2311
|
+
data: { name: objectExport.name }
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
var jsx_compound_part_export_naming_default = rule20;
|
|
2319
|
+
|
|
2320
|
+
// src/rules/no-pass-through-props.ts
|
|
2321
|
+
function isPascalCase4(name) {
|
|
2322
|
+
if (name.length === 0) return false;
|
|
2323
|
+
const first = name[0];
|
|
2324
|
+
return first >= "A" && first <= "Z";
|
|
2325
|
+
}
|
|
2326
|
+
function walkAst(node, parent, seen, visitor) {
|
|
2327
|
+
if (!node || typeof node !== "object") return;
|
|
2328
|
+
if (seen.has(node)) return;
|
|
2329
|
+
seen.add(node);
|
|
2330
|
+
visitor(node, parent);
|
|
2331
|
+
const record = node;
|
|
2332
|
+
for (const [key, value] of Object.entries(record)) {
|
|
2333
|
+
if (key === "parent") continue;
|
|
2334
|
+
if (!value) continue;
|
|
2335
|
+
if (Array.isArray(value)) {
|
|
2336
|
+
for (const entry of value) {
|
|
2337
|
+
if (entry && typeof entry === "object" && "type" in entry) {
|
|
2338
|
+
walkAst(entry, node, seen, visitor);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
continue;
|
|
2342
|
+
}
|
|
2343
|
+
if (typeof value === "object" && "type" in value) {
|
|
2344
|
+
walkAst(value, node, seen, visitor);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
function getIdentifierFromPattern(pattern) {
|
|
2349
|
+
if (!pattern) return null;
|
|
2350
|
+
if (pattern.type === "Identifier") return pattern;
|
|
2351
|
+
if (pattern.type === "AssignmentPattern") {
|
|
2352
|
+
const assignmentPattern = pattern;
|
|
2353
|
+
if (assignmentPattern.left?.type === "Identifier") return assignmentPattern.left;
|
|
2354
|
+
}
|
|
2355
|
+
return null;
|
|
2356
|
+
}
|
|
2357
|
+
function getStaticPropKey(property) {
|
|
2358
|
+
if (property.type !== "Property") return null;
|
|
2359
|
+
const prop = property;
|
|
2360
|
+
if (!prop.key || prop.computed) return null;
|
|
2361
|
+
if (prop.key.type === "Identifier") {
|
|
2362
|
+
return prop.key.name;
|
|
2363
|
+
}
|
|
2364
|
+
if (prop.key.type === "Literal") {
|
|
2365
|
+
const literal = prop.key;
|
|
2366
|
+
return typeof literal.value === "string" ? literal.value : null;
|
|
2367
|
+
}
|
|
2368
|
+
return null;
|
|
2369
|
+
}
|
|
2370
|
+
function getPropBindings(param) {
|
|
2371
|
+
if (!param || param.type !== "ObjectPattern") return [];
|
|
2372
|
+
const objectPattern = param;
|
|
2373
|
+
const bindings = [];
|
|
2374
|
+
for (const property of objectPattern.properties ?? []) {
|
|
2375
|
+
if (property.type !== "Property") continue;
|
|
2376
|
+
const prop = property;
|
|
2377
|
+
if (!prop.value) continue;
|
|
2378
|
+
const identifier = getIdentifierFromPattern(prop.value);
|
|
2379
|
+
if (!identifier) continue;
|
|
2380
|
+
const localName = identifier.name;
|
|
2381
|
+
const propName = getStaticPropKey(property) ?? localName;
|
|
2382
|
+
bindings.push({
|
|
2383
|
+
propName,
|
|
2384
|
+
localName,
|
|
2385
|
+
node: identifier
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
return bindings;
|
|
2389
|
+
}
|
|
2390
|
+
function isDefinitionLikeIdentifier(node, parent, name) {
|
|
2391
|
+
if (!parent) return false;
|
|
2392
|
+
if (parent.type === "Property") {
|
|
2393
|
+
const property = parent;
|
|
2394
|
+
if (property.key === node && !property.computed) return true;
|
|
2395
|
+
if (property.value === node && property.value?.type === "Identifier") {
|
|
2396
|
+
const value = property.value;
|
|
2397
|
+
if (value.name === name && property.key === node) return true;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
if (parent.type === "VariableDeclarator") {
|
|
2401
|
+
const declarator = parent;
|
|
2402
|
+
if (declarator.id === node) return true;
|
|
2403
|
+
}
|
|
2404
|
+
if (parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression") {
|
|
2405
|
+
const fn = parent;
|
|
2406
|
+
if (fn.id === node) return true;
|
|
2407
|
+
if ((fn.params ?? []).includes(node)) return true;
|
|
2408
|
+
}
|
|
2409
|
+
if (parent.type === "ArrowFunctionExpression") {
|
|
2410
|
+
const arrow = parent;
|
|
2411
|
+
if ((arrow.params ?? []).includes(node)) return true;
|
|
2412
|
+
}
|
|
2413
|
+
return false;
|
|
2414
|
+
}
|
|
2415
|
+
function isDirectJsxAttributeForward(node, parent, parentMap) {
|
|
2416
|
+
if (!parent) return false;
|
|
2417
|
+
const typedParent = parent;
|
|
2418
|
+
if (typedParent.type !== "JSXExpressionContainer") return false;
|
|
2419
|
+
const container = parent;
|
|
2420
|
+
if (container.expression !== node) return false;
|
|
2421
|
+
const ref = parentMap.get(parent);
|
|
2422
|
+
if (!ref?.parent) return false;
|
|
2423
|
+
const typedGrandParent = ref.parent;
|
|
2424
|
+
return typedGrandParent.type === "JSXAttribute";
|
|
2425
|
+
}
|
|
2426
|
+
function getForwardedTargetProp(node, parent, parentMap) {
|
|
2427
|
+
if (!isDirectJsxAttributeForward(node, parent, parentMap)) return null;
|
|
2428
|
+
const ref = parent ? parentMap.get(parent) : null;
|
|
2429
|
+
const grandParent = ref?.parent;
|
|
2430
|
+
if (!grandParent || grandParent.type !== "JSXAttribute") return null;
|
|
2431
|
+
if (grandParent.name?.type !== "JSXIdentifier") return null;
|
|
2432
|
+
return grandParent.name.name ?? null;
|
|
2433
|
+
}
|
|
2434
|
+
function checkComponent(context, functionNode, name, params, body) {
|
|
2435
|
+
if (!name || !isPascalCase4(name)) return;
|
|
2436
|
+
const firstParam = (params ?? [])[0];
|
|
2437
|
+
const bindings = getPropBindings(firstParam);
|
|
2438
|
+
if (bindings.length === 0) return;
|
|
2439
|
+
const bindingMap = /* @__PURE__ */ new Map();
|
|
2440
|
+
for (const binding of bindings) {
|
|
2441
|
+
if (binding.propName === "children") continue;
|
|
2442
|
+
bindingMap.set(binding.localName, binding);
|
|
2443
|
+
}
|
|
2444
|
+
if (bindingMap.size === 0) return;
|
|
2445
|
+
const usageMap = /* @__PURE__ */ new Map();
|
|
2446
|
+
for (const localName of bindingMap.keys()) {
|
|
2447
|
+
usageMap.set(localName, { seen: false, owned: false, forwardedTo: /* @__PURE__ */ new Set() });
|
|
2448
|
+
}
|
|
2449
|
+
const parentMap = /* @__PURE__ */ new WeakMap();
|
|
2450
|
+
const seenForParents = /* @__PURE__ */ new WeakSet();
|
|
2451
|
+
walkAst(body, functionNode, seenForParents, (node, parent) => {
|
|
2452
|
+
parentMap.set(node, { parent });
|
|
2453
|
+
});
|
|
2454
|
+
const seenForUsage = /* @__PURE__ */ new WeakSet();
|
|
2455
|
+
walkAst(body, functionNode, seenForUsage, (node, parent) => {
|
|
2456
|
+
if (node.type !== "Identifier") return;
|
|
2457
|
+
const identifier = node;
|
|
2458
|
+
const usage = usageMap.get(identifier.name);
|
|
2459
|
+
if (!usage) return;
|
|
2460
|
+
if (isDefinitionLikeIdentifier(node, parent, identifier.name)) return;
|
|
2461
|
+
usage.seen = true;
|
|
2462
|
+
const forwardedTarget = getForwardedTargetProp(node, parent, parentMap);
|
|
2463
|
+
if (!forwardedTarget) {
|
|
2464
|
+
usage.owned = true;
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
usage.forwardedTo.add(forwardedTarget);
|
|
2468
|
+
});
|
|
2469
|
+
for (const [localName, usage] of usageMap.entries()) {
|
|
2470
|
+
if (!usage.seen || usage.owned) continue;
|
|
2471
|
+
const binding = bindingMap.get(localName);
|
|
2472
|
+
if (!binding) continue;
|
|
2473
|
+
context.report({
|
|
2474
|
+
node: binding.node,
|
|
2475
|
+
messageId: "noPassThroughProp",
|
|
2476
|
+
data: {
|
|
2477
|
+
prop: binding.propName,
|
|
2478
|
+
component: name,
|
|
2479
|
+
forwardedTo: usage.forwardedTo.size > 0 ? Array.from(usage.forwardedTo).sort().join(", ") : "child prop"
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
var rule21 = {
|
|
2485
|
+
meta: {
|
|
2486
|
+
type: "suggestion",
|
|
2487
|
+
docs: {
|
|
2488
|
+
description: "Disallow pass-through-only props in component owners (accepting props only to forward them)",
|
|
2489
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-pass-through-props.md"
|
|
2490
|
+
},
|
|
2491
|
+
messages: {
|
|
2492
|
+
noPassThroughProp: "Prop '{{prop}}' in '{{component}}' is only forwarded to '{{forwardedTo}}'. Remove the pass-through prop, derive a local value, or expose composition via children at the ownership boundary. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/no-pass-through-props.md."
|
|
2493
|
+
},
|
|
2494
|
+
schema: []
|
|
2495
|
+
},
|
|
2496
|
+
create(context) {
|
|
2497
|
+
return {
|
|
2498
|
+
FunctionDeclaration(node) {
|
|
2499
|
+
const fn = node;
|
|
2500
|
+
checkComponent(context, node, fn.id?.name, fn.params, fn.body);
|
|
2501
|
+
},
|
|
2502
|
+
VariableDeclarator(node) {
|
|
2503
|
+
const declaration = node;
|
|
2504
|
+
if (declaration.id?.type !== "Identifier") return;
|
|
2505
|
+
const id = declaration.id;
|
|
2506
|
+
const init = declaration.init;
|
|
2507
|
+
if (!init) return;
|
|
2508
|
+
if (init.type !== "ArrowFunctionExpression" && init.type !== "FunctionExpression") return;
|
|
2509
|
+
const component = init;
|
|
2510
|
+
checkComponent(context, init, id.name, component.params, component.body);
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
};
|
|
2515
|
+
var no_pass_through_props_default = rule21;
|
|
2516
|
+
|
|
2517
|
+
// src/rules/jsx-flat-owner-tree.ts
|
|
2518
|
+
var DEFAULT_ALLOWED_CHAIN_DEPTH = 2;
|
|
2519
|
+
function isPascalCase5(name) {
|
|
2520
|
+
if (name.length === 0) return false;
|
|
2521
|
+
const first = name[0];
|
|
2522
|
+
return first >= "A" && first <= "Z";
|
|
2523
|
+
}
|
|
2524
|
+
function getCustomJsxName(node) {
|
|
2525
|
+
if (!node) return null;
|
|
2526
|
+
const typedNode = node;
|
|
2527
|
+
if (typedNode.type === "JSXIdentifier" && typedNode.name) {
|
|
2528
|
+
const name = typedNode.name;
|
|
2529
|
+
return isPascalCase5(name) ? name : null;
|
|
2530
|
+
}
|
|
2531
|
+
if (typedNode.type === "JSXMemberExpression" && typedNode.object) {
|
|
2532
|
+
const member = typedNode;
|
|
2533
|
+
const typedObject = member.object;
|
|
2534
|
+
if (typedObject.type === "JSXIdentifier" && typedObject.name) {
|
|
2535
|
+
const object = typedObject;
|
|
2536
|
+
return isPascalCase5(object.name) ? object.name : null;
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
return null;
|
|
2540
|
+
}
|
|
2541
|
+
function collectSelfClosingCustomJsxNames(node, names) {
|
|
2542
|
+
if (!node) return;
|
|
2543
|
+
const typedNode = node;
|
|
2544
|
+
if (typedNode.type === "JSXElement") {
|
|
2545
|
+
const element = node;
|
|
2546
|
+
if (element.openingElement.selfClosing) {
|
|
2547
|
+
const customName = getCustomJsxName(element.openingElement.name);
|
|
2548
|
+
if (customName) names.add(customName);
|
|
2549
|
+
}
|
|
2550
|
+
for (const child of element.children ?? []) {
|
|
2551
|
+
const typedChild = child;
|
|
2552
|
+
if (typedChild.type !== "JSXElement" && typedChild.type !== "JSXFragment") continue;
|
|
2553
|
+
collectSelfClosingCustomJsxNames(child, names);
|
|
2554
|
+
}
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
if (typedNode.type === "JSXFragment") {
|
|
2558
|
+
const fragment = node;
|
|
2559
|
+
for (const child of fragment.children ?? []) {
|
|
2560
|
+
const typedChild = child;
|
|
2561
|
+
if (typedChild.type !== "JSXElement" && typedChild.type !== "JSXFragment") continue;
|
|
2562
|
+
collectSelfClosingCustomJsxNames(child, names);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
function getSelfClosingCustomChildren(body) {
|
|
2567
|
+
const names = /* @__PURE__ */ new Set();
|
|
2568
|
+
if (!body) return names;
|
|
2569
|
+
const typedBody = body;
|
|
2570
|
+
if (typedBody.type === "JSXElement" || typedBody.type === "JSXFragment") {
|
|
2571
|
+
collectSelfClosingCustomJsxNames(body, names);
|
|
2572
|
+
} else if (typedBody.type === "BlockStatement") {
|
|
2573
|
+
const block = body;
|
|
2574
|
+
for (const statement of block.body ?? []) {
|
|
2575
|
+
if (statement.type !== "ReturnStatement") continue;
|
|
2576
|
+
const returnStatement = statement;
|
|
2577
|
+
const argument = returnStatement.argument;
|
|
2578
|
+
if (!argument) continue;
|
|
2579
|
+
const typedArgument = argument;
|
|
2580
|
+
if (typedArgument.type !== "JSXElement" && typedArgument.type !== "JSXFragment") continue;
|
|
2581
|
+
collectSelfClosingCustomJsxNames(argument, names);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
return names;
|
|
2585
|
+
}
|
|
2586
|
+
function computeLongestChain(componentName, components, memo, visiting) {
|
|
2587
|
+
const cached = memo.get(componentName);
|
|
2588
|
+
if (cached != null) return cached;
|
|
2589
|
+
if (visiting.has(componentName)) {
|
|
2590
|
+
return [componentName];
|
|
2591
|
+
}
|
|
2592
|
+
visiting.add(componentName);
|
|
2593
|
+
const component = components.get(componentName);
|
|
2594
|
+
if (!component) {
|
|
2595
|
+
memo.set(componentName, [componentName]);
|
|
2596
|
+
visiting.delete(componentName);
|
|
2597
|
+
return [componentName];
|
|
2598
|
+
}
|
|
2599
|
+
if (component.selfClosingCustomChildren.size === 0) {
|
|
2600
|
+
const base = [componentName];
|
|
2601
|
+
memo.set(componentName, base);
|
|
2602
|
+
visiting.delete(componentName);
|
|
2603
|
+
return base;
|
|
2604
|
+
}
|
|
2605
|
+
let bestChildChain = [];
|
|
2606
|
+
for (const childName of component.selfClosingCustomChildren) {
|
|
2607
|
+
if (!components.has(childName)) continue;
|
|
2608
|
+
const childChain = computeLongestChain(childName, components, memo, visiting);
|
|
2609
|
+
if (childChain.length > bestChildChain.length) bestChildChain = childChain;
|
|
2610
|
+
}
|
|
2611
|
+
const fullChain = [componentName, ...bestChildChain];
|
|
2612
|
+
memo.set(componentName, fullChain);
|
|
2613
|
+
visiting.delete(componentName);
|
|
2614
|
+
return fullChain;
|
|
2615
|
+
}
|
|
2616
|
+
var rule22 = {
|
|
2617
|
+
meta: {
|
|
2618
|
+
type: "suggestion",
|
|
2619
|
+
docs: {
|
|
2620
|
+
description: "Encourage flatter parent component chains by reporting self-closing custom component handoffs deeper than allowedChainDepth",
|
|
2621
|
+
url: "https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/jsx-flat-owner-tree.md"
|
|
2622
|
+
},
|
|
2623
|
+
messages: {
|
|
2624
|
+
deepParentTree: "Component '{{component}}' is in a {{depth}}-deep self-closing handoff chain (allowed: {{allowedDepth}}): {{chain}}. Start flattening at '{{nextHandoff}}' by inlining/compounding intermediate relays. See: https://github.com/jjenzz/sweepit/tree/main/packages/eslint-plugin-sweepit/docs/rules/jsx-flat-owner-tree.md."
|
|
2625
|
+
},
|
|
2626
|
+
schema: [
|
|
2627
|
+
{
|
|
2628
|
+
type: "object",
|
|
2629
|
+
properties: {
|
|
2630
|
+
allowedChainDepth: {
|
|
2631
|
+
type: "integer",
|
|
2632
|
+
minimum: 1
|
|
2633
|
+
}
|
|
2634
|
+
},
|
|
2635
|
+
additionalProperties: false
|
|
2636
|
+
}
|
|
2637
|
+
]
|
|
2638
|
+
},
|
|
2639
|
+
create(context) {
|
|
2640
|
+
const rawOptions = context.options[0] ?? {};
|
|
2641
|
+
const allowedChainDepthRaw = rawOptions.allowedChainDepth;
|
|
2642
|
+
const allowedChainDepth = Number.isInteger(allowedChainDepthRaw) && (allowedChainDepthRaw ?? 0) >= 1 ? allowedChainDepthRaw : DEFAULT_ALLOWED_CHAIN_DEPTH;
|
|
2643
|
+
const minReportedChainDepth = allowedChainDepth + 1;
|
|
2644
|
+
const components = /* @__PURE__ */ new Map();
|
|
2645
|
+
function registerComponent(name, body, node) {
|
|
2646
|
+
if (!name || !isPascalCase5(name)) return;
|
|
2647
|
+
components.set(name, {
|
|
2648
|
+
name,
|
|
2649
|
+
node,
|
|
2650
|
+
selfClosingCustomChildren: getSelfClosingCustomChildren(body)
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
return {
|
|
2654
|
+
FunctionDeclaration(node) {
|
|
2655
|
+
const fn = node;
|
|
2656
|
+
registerComponent(fn.id?.name, fn.body, node);
|
|
2657
|
+
},
|
|
2658
|
+
VariableDeclarator(node) {
|
|
2659
|
+
const declaration = node;
|
|
2660
|
+
if (declaration.id?.type !== "Identifier") return;
|
|
2661
|
+
if (!declaration.init) return;
|
|
2662
|
+
if (declaration.init.type !== "ArrowFunctionExpression" && declaration.init.type !== "FunctionExpression") {
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
const id = declaration.id;
|
|
2666
|
+
const init = declaration.init;
|
|
2667
|
+
registerComponent(id.name, init.body, declaration.id);
|
|
2668
|
+
},
|
|
2669
|
+
"Program:exit"() {
|
|
2670
|
+
const memo = /* @__PURE__ */ new Map();
|
|
2671
|
+
for (const component of components.values()) {
|
|
2672
|
+
const chain = computeLongestChain(component.name, components, memo, /* @__PURE__ */ new Set());
|
|
2673
|
+
const depth = chain.length;
|
|
2674
|
+
if (depth < minReportedChainDepth) continue;
|
|
2675
|
+
context.report({
|
|
2676
|
+
node: component.node,
|
|
2677
|
+
messageId: "deepParentTree",
|
|
2678
|
+
data: {
|
|
2679
|
+
component: component.name,
|
|
2680
|
+
depth: String(depth),
|
|
2681
|
+
allowedDepth: String(allowedChainDepth),
|
|
2682
|
+
chain: chain.join(" -> "),
|
|
2683
|
+
nextHandoff: chain[1] ?? component.name
|
|
2684
|
+
}
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
};
|
|
2691
|
+
var jsx_flat_owner_tree_default = rule22;
|
|
2692
|
+
|
|
2693
|
+
// src/index.ts
|
|
2694
|
+
var plugin = {
|
|
2695
|
+
meta: {
|
|
2696
|
+
name: "eslint-plugin-sweepit",
|
|
2697
|
+
version: "0.0.0"
|
|
2698
|
+
},
|
|
2699
|
+
rules: {
|
|
2700
|
+
"no-title-case-props": no_title_case_props_default,
|
|
2701
|
+
"no-custom-kebab-case-props": no_custom_kebab_case_props_default,
|
|
2702
|
+
"no-set-prefix-utils": no_set_prefix_utils_default,
|
|
2703
|
+
"no-useless-hook": no_useless_hook_default,
|
|
2704
|
+
"no-hook-jsx": no_hook_jsx_default,
|
|
2705
|
+
"no-exported-context-hooks": no_exported_context_hooks_default,
|
|
2706
|
+
"no-handler-return-type": no_handler_return_type_default,
|
|
2707
|
+
"jsx-server-action-prop-suffix": jsx_server_action_prop_suffix_default,
|
|
2708
|
+
"jsx-on-handler-verb-suffix": jsx_on_handler_verb_suffix_default,
|
|
2709
|
+
"no-render-helper-functions": no_render_helper_functions_default,
|
|
2710
|
+
"no-element-props": no_element_props_default,
|
|
2711
|
+
"no-componenttype-props": no_componenttype_props_default,
|
|
2712
|
+
"no-object-props": no_object_props_default,
|
|
2713
|
+
"no-array-props": no_array_props_default,
|
|
2714
|
+
"no-prefixed-prop-bundles": no_prefixed_prop_bundles_default,
|
|
2715
|
+
"no-optional-props-without-defaults": no_optional_props_without_defaults_default,
|
|
2716
|
+
"no-boolean-capability-props": no_boolean_capability_props_default,
|
|
2717
|
+
"max-custom-props": max_custom_props_default,
|
|
2718
|
+
"jsx-bem-compound-naming": jsx_bem_compound_naming_default,
|
|
2719
|
+
"jsx-compound-part-export-naming": jsx_compound_part_export_naming_default,
|
|
2720
|
+
"no-pass-through-props": no_pass_through_props_default,
|
|
2721
|
+
"jsx-flat-owner-tree": jsx_flat_owner_tree_default
|
|
2722
|
+
},
|
|
2723
|
+
configs: {}
|
|
2724
|
+
};
|
|
2725
|
+
plugin.configs = {
|
|
2726
|
+
react: createReactConfig(plugin)
|
|
2727
|
+
};
|
|
2728
|
+
var index_default = plugin;
|
|
2729
|
+
export {
|
|
2730
|
+
index_default as default,
|
|
2731
|
+
plugin
|
|
2732
|
+
};
|