@ai0x0/utils 0.0.26 → 0.0.27

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.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 本地 ESLint plugin:封装项目自定义规则。
3
+ */
4
+ import requireSectionDivider from "./require-section-divider.js";
5
+ import noHardcodedStyle from "./no-hardcoded-style.js";
6
+ import requireUseForm from "./require-use-form.js";
7
+ import requireFormConvention from "./require-form-convention.js";
8
+ import requireProComponents from "./require-pro-components.js";
9
+ import noAntdSpace from "./no-antd-space.js";
10
+ import noUseRequestRun from "./no-use-request-run.js";
11
+ import noThen from "./no-then.js";
12
+ import noOneLetterVars from "./no-one-letter-vars.js";
13
+ import noConsecutiveSetState from "./no-consecutive-setstate.js";
14
+
15
+ const rules = {
16
+ "require-section-divider": requireSectionDivider,
17
+ "no-hardcoded-style": noHardcodedStyle,
18
+ "require-use-form": requireUseForm,
19
+ "require-form-convention": requireFormConvention,
20
+ "require-pro-components": requireProComponents,
21
+ "no-antd-space": noAntdSpace,
22
+ "no-use-request-run": noUseRequestRun,
23
+ "no-then": noThen,
24
+ "no-one-letter-vars": noOneLetterVars,
25
+ "no-consecutive-setstate": noConsecutiveSetState,
26
+ };
27
+
28
+ const plugin = {
29
+ rules,
30
+ configs: {
31
+ recommended: {
32
+ plugins: { local: { rules } },
33
+ rules: Object.fromEntries(
34
+ Object.keys(rules).map((name) => [`local/${name}`, "error"]),
35
+ ),
36
+ },
37
+ },
38
+ };
39
+
40
+ export default plugin;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * no-antd-space
3
+ * -------------
4
+ * 禁止使用 antd <Space>,用 antd <Flex> 代替。
5
+ *
6
+ * 映射参考:
7
+ * - <Space direction="vertical"> → <Flex vertical>
8
+ * - <Space size={N}> → <Flex gap={N}>
9
+ * - <Space wrap> → <Flex wrap="wrap">
10
+ * - Space.Compact → 用 Flex 自行实现紧凑排列
11
+ *
12
+ * 本规则只负责检测,不负责自动修复。
13
+ */
14
+
15
+ /** @type {import('eslint').Rule.RuleModule} */
16
+ const rule = {
17
+ meta: {
18
+ type: "suggestion",
19
+ docs: {
20
+ description: "禁止使用 antd <Space>,请使用 antd <Flex> 代替。",
21
+ },
22
+ schema: [],
23
+ messages: {
24
+ useFlexInstead:
25
+ "禁止使用 antd <Space>,请使用 antd <Flex> 代替。<Space> 的 props 到 <Flex> 的映射:direction={vertical} → vertical, wrap → wrap='wrap', size={N} → gap={N}。",
26
+ useFlexInsteadCompact:
27
+ "禁止使用 antd <Space.Compact>,请使用 <Flex> 自行实现紧凑排列。",
28
+ },
29
+ },
30
+ create(context) {
31
+ const getTagName = (name) => {
32
+ if (name.type === "JSXIdentifier") return name.name;
33
+ if (
34
+ name.type === "JSXMemberExpression" &&
35
+ name.object.type === "JSXIdentifier" &&
36
+ name.property.type === "JSXIdentifier"
37
+ ) {
38
+ return `${name.object.name}.${name.property.name}`;
39
+ }
40
+ return "";
41
+ };
42
+
43
+ return {
44
+ JSXOpeningElement(node) {
45
+ const tagName = getTagName(node.name);
46
+ if (tagName === "Space") {
47
+ context.report({
48
+ node: node,
49
+ messageId: "useFlexInstead",
50
+ });
51
+ }
52
+ if (tagName === "Space.Compact") {
53
+ context.report({
54
+ node: node,
55
+ messageId: "useFlexInsteadCompact",
56
+ });
57
+ }
58
+ },
59
+ };
60
+ },
61
+ };
62
+
63
+ export default rule;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * no-consecutive-setstate
3
+ * ------------------------
4
+ * 禁止在同一个块中连续调用多个 setState()。
5
+ * ahooks useSetState 的 setState 会合并状态,连续多次调用应该合并为一次。
6
+ *
7
+ * 误:
8
+ * setState({ a: 1 });
9
+ * setState({ b: 2 });
10
+ *
11
+ * 正:
12
+ * setState({ a: 1, b: 2 });
13
+ */
14
+
15
+ /** @type {import('eslint').Rule.RuleModule} */
16
+ const rule = {
17
+ meta: {
18
+ type: "suggestion",
19
+ docs: {
20
+ description: "禁止连续调用多个 setState(),应合并为一次。",
21
+ },
22
+ schema: [],
23
+ messages: {
24
+ merge:
25
+ "连续多个 setState() 应合并为一次:将 {{prevField}} 和 {{currField}} 合并到一个 setState 中",
26
+ },
27
+ },
28
+ create(context) {
29
+ return {
30
+ BlockStatement(node) {
31
+ const body = node.body;
32
+ for (let i = 0; i + 1 < body.length; i++) {
33
+ const curr = body[i];
34
+ const next = body[i + 1];
35
+
36
+ if (
37
+ curr.type !== "ExpressionStatement" ||
38
+ next.type !== "ExpressionStatement"
39
+ )
40
+ continue;
41
+
42
+ const currExpr = curr.expression;
43
+ const nextExpr = next.expression;
44
+
45
+ const isCurrSetState =
46
+ currExpr.type === "CallExpression" &&
47
+ currExpr.callee.type === "Identifier" &&
48
+ currExpr.callee.name === "setState" &&
49
+ currExpr.arguments.length === 1 &&
50
+ currExpr.arguments[0].type === "ObjectExpression";
51
+
52
+ const isNextSetState =
53
+ nextExpr.type === "CallExpression" &&
54
+ nextExpr.callee.type === "Identifier" &&
55
+ nextExpr.callee.name === "setState" &&
56
+ nextExpr.arguments.length === 1 &&
57
+ nextExpr.arguments[0].type === "ObjectExpression";
58
+
59
+ if (!isCurrSetState || !isNextSetState) continue;
60
+
61
+ const prevField = currExpr.arguments[0];
62
+ const currField = nextExpr.arguments[0];
63
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
64
+ const prevFieldText = sourceCode
65
+ .getText(prevField)
66
+ .slice(1, -1)
67
+ .trim();
68
+ const currFieldText = sourceCode
69
+ .getText(currField)
70
+ .slice(1, -1)
71
+ .trim();
72
+
73
+ context.report({
74
+ node: next,
75
+ messageId: "merge",
76
+ data: {
77
+ prevField: prevFieldText,
78
+ currField: currFieldText,
79
+ },
80
+ });
81
+ }
82
+ },
83
+ };
84
+ },
85
+ };
86
+
87
+ export default rule;
@@ -0,0 +1,437 @@
1
+ /**
2
+ * no-hardcoded-style
3
+ * ------------------
4
+ * 禁止在 JSX style / createStyles / css`` 中直接写死样式值。
5
+ * 强制使用 antd token(theme.useToken() 或 createStyles(({token}) => ...))。
6
+ *
7
+ * 白名单:
8
+ * - token.xxx / token() / calc(...) 表达式
9
+ * - 0 值
10
+ * - CSS 功能性关键字(auto, none, hidden, pointer 等)
11
+ * - 百分比 / 带单位字符串
12
+ * - 条件/逻辑/变量/函数调用(无法静态判断)
13
+ * - css\`...\` 中的 \${token.xxx} 插值
14
+ */
15
+
16
+ // ==============================================================================
17
+ // 白名单
18
+ // ==============================================================================
19
+
20
+ const ALLOWED_LITERALS = new Set([
21
+ "auto",
22
+ "none",
23
+ "hidden",
24
+ "visible",
25
+ "pre-wrap",
26
+ "pre",
27
+ "normal",
28
+ "pointer",
29
+ "default",
30
+ "move",
31
+ "not-allowed",
32
+ "grab",
33
+ "grabbing",
34
+ "center",
35
+ "flex-start",
36
+ "flex-end",
37
+ "space-between",
38
+ "space-around",
39
+ "left",
40
+ "right",
41
+ "top",
42
+ "bottom",
43
+ "start",
44
+ "end",
45
+ "solid",
46
+ "dashed",
47
+ "dotted",
48
+ "double",
49
+ "groove",
50
+ "ridge",
51
+ "inset",
52
+ "outset",
53
+ "block",
54
+ "inline-block",
55
+ "inline",
56
+ "flex",
57
+ "grid",
58
+ "contents",
59
+ "row",
60
+ "column",
61
+ "row-reverse",
62
+ "column-reverse",
63
+ "wrap",
64
+ "nowrap",
65
+ "wrap-reverse",
66
+ "relative",
67
+ "absolute",
68
+ "fixed",
69
+ "sticky",
70
+ "static",
71
+ "bold",
72
+ "bolder",
73
+ "lighter",
74
+ "100",
75
+ "200",
76
+ "300",
77
+ "400",
78
+ "500",
79
+ "600",
80
+ "700",
81
+ "800",
82
+ "900",
83
+ "italic",
84
+ "oblique",
85
+ "uppercase",
86
+ "lowercase",
87
+ "capitalize",
88
+ "underline",
89
+ "overline",
90
+ "line-through",
91
+ "blink",
92
+ "collapse",
93
+ "separate",
94
+ "border-box",
95
+ "content-box",
96
+ "padding-box",
97
+ "contain",
98
+ "cover",
99
+ "fill",
100
+ "scale-down",
101
+ ]);
102
+
103
+ const TOKEN_KEYS = new Set([
104
+ "margin",
105
+ "marginTop",
106
+ "marginBottom",
107
+ "marginLeft",
108
+ "marginRight",
109
+ "marginInline",
110
+ "marginInlineStart",
111
+ "marginInlineEnd",
112
+ "marginBlock",
113
+ "marginBlockStart",
114
+ "marginBlockEnd",
115
+ "padding",
116
+ "paddingTop",
117
+ "paddingBottom",
118
+ "paddingLeft",
119
+ "paddingRight",
120
+ "paddingInline",
121
+ "paddingInlineStart",
122
+ "paddingInlineEnd",
123
+ "paddingBlock",
124
+ "paddingBlockStart",
125
+ "paddingBlockEnd",
126
+ "gap",
127
+ "rowGap",
128
+ "columnGap",
129
+ "color",
130
+ "background",
131
+ "backgroundColor",
132
+ "borderColor",
133
+ "borderTopColor",
134
+ "borderBottomColor",
135
+ "borderLeftColor",
136
+ "borderRightColor",
137
+ "outlineColor",
138
+ "border",
139
+ "borderTop",
140
+ "borderBottom",
141
+ "borderLeft",
142
+ "borderRight",
143
+ "borderWidth",
144
+ "borderTopWidth",
145
+ "borderBottomWidth",
146
+ "borderLeftWidth",
147
+ "borderRightWidth",
148
+ "borderStyle",
149
+ "borderTopStyle",
150
+ "borderBottomStyle",
151
+ "borderLeftStyle",
152
+ "borderRightStyle",
153
+ "borderRadius",
154
+ "borderTopLeftRadius",
155
+ "borderTopRightRadius",
156
+ "borderBottomLeftRadius",
157
+ "borderBottomRightRadius",
158
+ "fontSize",
159
+ "fontWeight",
160
+ "lineHeight",
161
+ "letterSpacing",
162
+ "boxShadow",
163
+ "textShadow",
164
+ "opacity",
165
+ "zIndex",
166
+ "width",
167
+ "height",
168
+ "minWidth",
169
+ "minHeight",
170
+ "maxWidth",
171
+ "maxHeight",
172
+ "top",
173
+ "bottom",
174
+ "left",
175
+ "right",
176
+ ]);
177
+
178
+ // ==============================================================================
179
+ // Token 属性 / 检查逻辑
180
+ // ==============================================================================
181
+
182
+ /** kebab-case → camelCase 映射(用于 css`` 模板字符串) */
183
+ const KEBAB_TO_CAMEL = {};
184
+ for (const key of TOKEN_KEYS) {
185
+ const kebab = key.replace(/([A-Z])/g, "-$1").toLowerCase();
186
+ KEBAB_TO_CAMEL[kebab] = key;
187
+ }
188
+
189
+ function isTokenExpr(node) {
190
+ if (!node) return false;
191
+ if (
192
+ node.type === "MemberExpression" &&
193
+ node.object.type === "Identifier" &&
194
+ node.object.name === "token"
195
+ )
196
+ return true;
197
+ if (
198
+ node.type === "CallExpression" &&
199
+ node.callee.type === "Identifier" &&
200
+ ["token", "calc", "max", "min", "clamp"].includes(node.callee.name)
201
+ )
202
+ return true;
203
+ return false;
204
+ }
205
+
206
+ function isAllowedValue(node) {
207
+ if (!node) return true;
208
+ if (isTokenExpr(node)) return true;
209
+ if (node.type === "Literal" && (node.value === 0 || node.value === "0"))
210
+ return true;
211
+ if (
212
+ node.type === "Literal" &&
213
+ typeof node.value === "string" &&
214
+ ALLOWED_LITERALS.has(node.value)
215
+ )
216
+ return true;
217
+ if (
218
+ node.type === "Literal" &&
219
+ typeof node.value === "string" &&
220
+ /^(\d+(\.\d+)?(px|em|rem|%|vh|vw|vmin|vmax|ex|ch|cm|mm|in|pt|pc)|\d+(\.\d+)?\s+(px|em|rem)\s+(solid|dashed|dotted)|\d+\.?\d*%)$/.test(
221
+ node.value,
222
+ )
223
+ )
224
+ return true;
225
+ if (
226
+ [
227
+ "ConditionalExpression",
228
+ "LogicalExpression",
229
+ "BinaryExpression",
230
+ "UnaryExpression",
231
+ "TemplateLiteral",
232
+ "CallExpression",
233
+ "Identifier",
234
+ "SpreadElement",
235
+ ].includes(node.type)
236
+ )
237
+ return true;
238
+ if (node.type === "ObjectExpression" || node.type === "ArrayExpression")
239
+ return true;
240
+ return false;
241
+ }
242
+
243
+ function checkStyleObject(context, objExpr) {
244
+ for (const prop of objExpr.properties) {
245
+ if (prop.type !== "Property") continue;
246
+ const key =
247
+ prop.key.type === "Identifier"
248
+ ? prop.key.name
249
+ : prop.key.type === "Literal"
250
+ ? prop.key.value
251
+ : null;
252
+ if (!key || !TOKEN_KEYS.has(key)) continue;
253
+ const val = prop.value;
254
+ if (isAllowedValue(val)) continue;
255
+ const valText = context.getSourceCode().getText(val);
256
+ context.report({
257
+ node: prop,
258
+ messageId: "noHardcoded",
259
+ data: { key, value: valText },
260
+ });
261
+ }
262
+ }
263
+
264
+ function checkCssTemplate(context, node) {
265
+ const template = node.quasi;
266
+ const quasis = template.quasis;
267
+ const expressions = template.expressions;
268
+
269
+ // 构建完整 CSS 文本,用占位符标记插值位置
270
+ let fullText = "";
271
+ for (let i = 0; i < quasis.length; i++) {
272
+ fullText += quasis[i].value.raw;
273
+ if (i < expressions.length) fullText += "${EXPR}";
274
+ }
275
+
276
+ // 按行分割,逐行检查
277
+ const lines = fullText.split(/\n/);
278
+ for (const line of lines) {
279
+ // 简单解析:找冒号分割的 property: value
280
+ const colonIdx = line.indexOf(":");
281
+ if (colonIdx === -1) continue;
282
+
283
+ const rawProp = line.slice(0, colonIdx).trim();
284
+ const rawVal = line
285
+ .slice(colonIdx + 1)
286
+ .trim()
287
+ .replace(/;$/, "")
288
+ .trim();
289
+
290
+ // 忽略 css 嵌套规则、@media 等
291
+ if (
292
+ rawProp.startsWith("&") ||
293
+ rawProp.startsWith("@") ||
294
+ rawProp.startsWith("//")
295
+ )
296
+ continue;
297
+
298
+ // 匹配 camelCase 或 kebab-case 属性
299
+ const camelProp = TOKEN_KEYS.has(rawProp)
300
+ ? rawProp
301
+ : KEBAB_TO_CAMEL[rawProp];
302
+ if (!camelProp) continue;
303
+
304
+ // 值包含 ${EXPR} → 被插值打断或完全由表达式提供 → 跳过
305
+ if (rawVal.includes("${EXPR}")) continue;
306
+
307
+ // 0 值
308
+ if (rawVal === "0" || rawVal === "0px") continue;
309
+
310
+ // CSS 关键字白名单
311
+ if (ALLOWED_LITERALS.has(rawVal)) continue;
312
+
313
+ // 纯百分比 → 允许(布局常用)
314
+ if (/^\d+\.?\d*%$/.test(rawVal)) continue;
315
+
316
+ // 100vh / 100vw → 允许(全屏布局)
317
+ if (/^100(vh|vw)$/.test(rawVal)) continue;
318
+
319
+ // 百分比 / 带单位字符串 → 硬编码(token 体系也有 sizeStep/sizeUnit)
320
+ if (
321
+ /^(\d+(\.\d+)?(px|em|rem|%|vh|vw|vmin|vmax|ex|ch|cm|mm|in|pt|pc)|\d+\.?\d*%)$/.test(
322
+ rawVal,
323
+ )
324
+ ) {
325
+ context.report({
326
+ node: template,
327
+ messageId: "noHardcoded",
328
+ data: { key: rawProp, value: rawVal },
329
+ });
330
+ continue;
331
+ }
332
+
333
+ // 剩余 → 硬编码
334
+ context.report({
335
+ node: template,
336
+ messageId: "noHardcoded",
337
+ data: { key: rawProp, value: rawVal },
338
+ });
339
+ }
340
+ }
341
+
342
+ /** @type {import('eslint').Rule.RuleModule} */
343
+ const rule = {
344
+ meta: {
345
+ type: "suggestion",
346
+ docs: {
347
+ description:
348
+ "禁止在 style / createStyles / css`` 中硬编码样式值,强制使用 antd token。",
349
+ },
350
+ schema: [],
351
+ messages: {
352
+ noHardcoded:
353
+ "禁止硬编码 `{{ {{key}}: {{value}} }}`。请使用 antd token(如 `token.marginSM`、`token.colorPrimary`)。可参考 .codex/skills/ant-design/SKILL.md 了解 token 体系。",
354
+ },
355
+ },
356
+ create(context) {
357
+ return {
358
+ JSXAttribute(node) {
359
+ if (node.name.name !== "style") return;
360
+ const expr = node.value?.expression;
361
+ if (!expr || expr.type !== "ObjectExpression") return;
362
+ checkStyleObject(context, expr);
363
+ },
364
+
365
+ TaggedTemplateExpression(node) {
366
+ if (node.tag.type === "Identifier" && node.tag.name === "css") {
367
+ checkCssTemplate(context, node);
368
+ }
369
+ },
370
+
371
+ CallExpression(node) {
372
+ if (
373
+ node.callee.type !== "Identifier" ||
374
+ node.callee.name !== "createStyles"
375
+ )
376
+ return;
377
+ const arg = node.arguments[0];
378
+ if (!arg) return;
379
+
380
+ if (arg.type === "ObjectExpression") {
381
+ for (const prop of arg.properties) {
382
+ if (prop.type !== "Property") continue;
383
+ if (prop.value.type === "ObjectExpression")
384
+ checkStyleObject(context, prop.value);
385
+ else if (
386
+ prop.value.type === "TaggedTemplateExpression" &&
387
+ prop.value.tag.name === "css"
388
+ )
389
+ checkCssTemplate(context, prop.value);
390
+ }
391
+ return;
392
+ }
393
+
394
+ let body = null;
395
+ if (arg.type === "ArrowFunctionExpression") body = arg.body;
396
+ else if (arg.type === "FunctionExpression") body = arg.body;
397
+ if (!body) return;
398
+
399
+ if (body.type === "ObjectExpression") {
400
+ for (const prop of body.properties) {
401
+ if (prop.type !== "Property") continue;
402
+ if (prop.value.type === "ObjectExpression")
403
+ checkStyleObject(context, prop.value);
404
+ else if (
405
+ prop.value.type === "TaggedTemplateExpression" &&
406
+ prop.value.tag.name === "css"
407
+ )
408
+ checkCssTemplate(context, prop.value);
409
+ }
410
+ return;
411
+ }
412
+
413
+ if (body.type === "BlockStatement") {
414
+ for (const stmt of body.body) {
415
+ if (
416
+ stmt.type === "ReturnStatement" &&
417
+ stmt.argument?.type === "ObjectExpression"
418
+ ) {
419
+ for (const prop of stmt.argument.properties) {
420
+ if (prop.type !== "Property") continue;
421
+ if (prop.value.type === "ObjectExpression")
422
+ checkStyleObject(context, prop.value);
423
+ else if (
424
+ prop.value.type === "TaggedTemplateExpression" &&
425
+ prop.value.tag.name === "css"
426
+ )
427
+ checkCssTemplate(context, prop.value);
428
+ }
429
+ }
430
+ }
431
+ }
432
+ },
433
+ };
434
+ },
435
+ };
436
+
437
+ export default rule;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * no-one-letter-vars
3
+ * ------------------
4
+ * 禁止单字母变量名(如 `e`、`i`、`g`、`x` 等),要求使用有意义的命名。
5
+ *
6
+ * 例外:
7
+ * - `_` 常用于未使用变量
8
+ * - for 循环的声名部分(for (let i = 0; ...))— 惯用用法
9
+ * - 泛型类型参数如 `<T>`
10
+ */
11
+
12
+ /** @type {import('eslint').Rule.RuleModule} */
13
+ const rule = {
14
+ meta: {
15
+ type: "suggestion",
16
+ docs: {
17
+ description: "禁止使用单字母变量,要求有意义的命名。",
18
+ },
19
+ schema: [],
20
+ messages: {
21
+ noOneLetter: "禁止使用单字母变量名 '{{ name }}',请使用有意义的名称。",
22
+ },
23
+ },
24
+ create(context) {
25
+ // Track function scopes to know if we're at the top level
26
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
27
+ let scopeStack = 0;
28
+
29
+ return {
30
+ // --- 变量声名 ---
31
+ VariableDeclarator(node) {
32
+ if (!node.id || node.id.type !== "Identifier") return;
33
+ const name = node.id.name;
34
+ if (name.length !== 1 || !/^[a-z]$/.test(name)) return;
35
+
36
+ // Skip `_`
37
+ if (name === "_") return;
38
+
39
+ // Skip if inside for loop (for (let i = 0..))
40
+ let parent = node.parent;
41
+ while (parent) {
42
+ if (
43
+ parent.type === "ForStatement" ||
44
+ parent.type === "ForInStatement" ||
45
+ parent.type === "ForOfStatement"
46
+ ) {
47
+ return;
48
+ }
49
+ parent = parent.parent;
50
+ }
51
+
52
+ context.report({
53
+ node: node.id,
54
+ messageId: "noOneLetter",
55
+ data: { name },
56
+ });
57
+ },
58
+
59
+ // --- 函数/箭头函数参数 ---
60
+ ":function"(node) {
61
+ scopeStack++;
62
+ for (const param of node.params) {
63
+ if (param.type === "Identifier") {
64
+ const name = param.name;
65
+ if (name.length === 1 && /^[a-z]$/.test(name) && name !== "_") {
66
+ context.report({
67
+ node: param,
68
+ messageId: "noOneLetter",
69
+ data: { name },
70
+ });
71
+ }
72
+ } else if (
73
+ param.type === "ObjectPattern" ||
74
+ param.type === "ArrayPattern"
75
+ ) {
76
+ // Check nested patterns
77
+ checkPattern(param, context);
78
+ }
79
+ }
80
+ },
81
+ ":function:exit"() {
82
+ scopeStack--;
83
+ },
84
+ };
85
+
86
+ function checkPattern(pattern, ctx) {
87
+ for (const prop of pattern.properties || pattern.elements || []) {
88
+ if (!prop) continue;
89
+ const value =
90
+ prop.type === "RestElement" ? prop.argument : prop.value || prop;
91
+ if (value.type === "Identifier") {
92
+ const name = value.name;
93
+ if (name.length === 1 && /^[a-z]$/.test(name) && name !== "_") {
94
+ ctx.report({
95
+ node: value,
96
+ messageId: "noOneLetter",
97
+ data: { name },
98
+ });
99
+ }
100
+ } else if (
101
+ value.type === "ObjectPattern" ||
102
+ value.type === "ArrayPattern"
103
+ ) {
104
+ checkPattern(value, ctx);
105
+ }
106
+ }
107
+ }
108
+ },
109
+ };
110
+
111
+ export default rule;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * no-then
3
+ * -------
4
+ * 禁止使用 .then(),强制使用 async/await。
5
+ *
6
+ * 例外(不检测):
7
+ * - 文件和 /apis/generator/ 下的代码(已有独立 override)
8
+ */
9
+
10
+ /** @type {import('eslint').Rule.RuleModule} */
11
+ const rule = {
12
+ meta: {
13
+ type: "suggestion",
14
+ docs: {
15
+ description: "禁止使用 .then(),必须使用 async/await。",
16
+ },
17
+ schema: [],
18
+ messages: {
19
+ noThen: "禁止使用 .then(),请使用 async/await 替代。",
20
+ },
21
+ },
22
+ create(context) {
23
+ return {
24
+ CallExpression(node) {
25
+ if (
26
+ node.callee.type === "MemberExpression" &&
27
+ node.callee.property.type === "Identifier" &&
28
+ node.callee.property.name === "then"
29
+ ) {
30
+ context.report({
31
+ node: node.callee.property,
32
+ messageId: "noThen",
33
+ });
34
+ }
35
+ },
36
+ };
37
+ },
38
+ };
39
+
40
+ export default rule;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * no-use-request-run
3
+ * -------------------
4
+ * 禁止在 useRequest 解构中使用 `run`(包括 `run: alias` 和 `run`)。
5
+ * 必须使用 `runAsync` 并配合 `await` 在调用处等待。
6
+ */
7
+
8
+ /** @type {import('eslint').Rule.RuleModule} */
9
+ const rule = {
10
+ meta: {
11
+ type: "suggestion",
12
+ docs: {
13
+ description: "禁止使用 useRequest 的 run,必须使用 runAsync + await。",
14
+ },
15
+ schema: [],
16
+ messages: {
17
+ useRunAsync:
18
+ "禁止使用 useRequest 的 run/run:xxx,请使用 runAsync/runAsync:xxx + await。",
19
+ },
20
+ },
21
+ create(context) {
22
+ return {
23
+ VariableDeclarator(node) {
24
+ if (
25
+ node.init &&
26
+ node.init.type === "CallExpression" &&
27
+ node.init.callee.type === "Identifier" &&
28
+ node.init.callee.name === "useRequest"
29
+ ) {
30
+ if (node.id.type === "ObjectPattern") {
31
+ for (const prop of node.id.properties) {
32
+ if (prop.type !== "Property") continue;
33
+ const key = prop.key;
34
+ if (
35
+ key.type === "Identifier" &&
36
+ (key.name === "run" || key.name === "runAsync")
37
+ ) {
38
+ // Only report for `run` / `run: foo`, not for `runAsync`
39
+ if (key.name === "run") {
40
+ context.report({
41
+ node: prop,
42
+ messageId: "useRunAsync",
43
+ });
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ },
50
+ };
51
+ },
52
+ };
53
+
54
+ export default rule;
@@ -0,0 +1,200 @@
1
+ /**
2
+ * require-form-convention
3
+ * -----------------------
4
+ * 约束表单组件必须遵守以下规范:
5
+ *
6
+ * 1. <Form> 必须使用 onFinish 提交
7
+ * 2. <Form.Item> 必须使用 rules 进行表单校验
8
+ * 3. 禁止在 useRequest 中使用 message.error 报错
9
+ * 4. Modal + Form 组合时,禁止用 Modal.onOk 提交
10
+ *
11
+ * 例外通过文件路径 overrides 处理(utils、hooks、theme 等已豁免)。
12
+ */
13
+
14
+ /** @type {import('eslint').Rule.RuleModule} */
15
+
16
+ // ==============================================================================
17
+ // 规则实现
18
+ // ==============================================================================
19
+
20
+ const rule = {
21
+ meta: {
22
+ type: "suggestion",
23
+ docs: {
24
+ description:
25
+ "表单组件必须使用 Form.onFinish,Form.Item 必须使用 rules,禁止 useRequest 内 message.error,禁止 Modal.onOk 提交表单。",
26
+ },
27
+ schema: [],
28
+ messages: {
29
+ needOnFinish:
30
+ "<Form> 必须使用 onFinish 属性提交表单,禁止用 form.validateFields() 或 Modal.onOk 手动提交。",
31
+ needItemRules:
32
+ "<Form.Item> 必须设置 rules 进行校验。如果确实不需要校验,请用 // eslint-disable-next-line 注明原因。",
33
+ noMessageError:
34
+ "禁止在 useRequest 的 onSuccess/onError 回调中使用 message.error。错误提示应通过 antd Form.Item 的 rules 或 Form onFinish 的错误处理来展示。",
35
+ noModalOnOk:
36
+ "Modal + Form 组合时,禁止使用 Modal.onOk 提交。请使用 Form.onFinish。",
37
+ noMessageErrorAny:
38
+ "禁止使用 app?.message.error 报错,请求拦截器已统一处理。请移除该行。",
39
+ },
40
+ },
41
+ create(context) {
42
+ const sourceCode = context.getSourceCode();
43
+
44
+ let hasFormTag = false;
45
+ let hasFormOnFinish = false;
46
+ const formItemNoRules = [];
47
+ const useRequestCalls = [];
48
+ const modalOnOkAttrs = [];
49
+ const validateFieldsCalls = [];
50
+
51
+ const getTagName = (name) => {
52
+ if (name.type === "JSXIdentifier") return name.name;
53
+ if (
54
+ name.type === "JSXMemberExpression" &&
55
+ name.object.type === "JSXIdentifier" &&
56
+ name.property.type === "JSXIdentifier"
57
+ ) {
58
+ return `${name.object.name}.${name.property.name}`;
59
+ }
60
+ return "";
61
+ };
62
+
63
+ return {
64
+ JSXOpeningElement(node) {
65
+ const tagName = getTagName(node.name);
66
+
67
+ // <Form>
68
+ if (tagName === "Form") {
69
+ hasFormTag = true;
70
+ for (const attr of node.attributes) {
71
+ if (attr.type !== "JSXAttribute" || !attr.name) continue;
72
+ if (attr.name.name === "onFinish") {
73
+ hasFormOnFinish = true;
74
+ }
75
+ }
76
+ if (!hasFormOnFinish) {
77
+ context.report({
78
+ node: node,
79
+ messageId: "needOnFinish",
80
+ });
81
+ }
82
+ }
83
+
84
+ // <Form.Item>
85
+ if (tagName === "Form.Item") {
86
+ let hasRules = false;
87
+ let isNoStyle = false;
88
+ let hasName = false;
89
+ for (const attr of node.attributes) {
90
+ if (attr.type !== "JSXAttribute" || !attr.name) continue;
91
+ if (attr.name.name === "rules") hasRules = true;
92
+ if (attr.name.name === "noStyle") isNoStyle = true;
93
+ if (attr.name.name === "name") hasName = true;
94
+ }
95
+ // noStyle 或 <Form.List> 内的 Item 不需要 rules
96
+ if (!hasRules && !isNoStyle && hasName) {
97
+ formItemNoRules.push(node);
98
+ }
99
+ }
100
+
101
+ // <Modal onOk={...}>
102
+ if (tagName === "Modal") {
103
+ for (const attr of node.attributes) {
104
+ if (attr.type !== "JSXAttribute" || !attr.name) continue;
105
+ if (attr.name.name === "onOk") {
106
+ modalOnOkAttrs.push(attr);
107
+ }
108
+ }
109
+ }
110
+ },
111
+
112
+ CallExpression(node) {
113
+ // app?.message.error() — 请求拦截器已统一处理,禁止手动调用
114
+ // app?.message.error() — 请求拦截器已统一处理,禁止手动调用
115
+ const callText = sourceCode.getText(node);
116
+ if (
117
+ callText.includes("message.error") &&
118
+ callText.startsWith("app") &&
119
+ callText.includes(".error(")
120
+ ) {
121
+ context.report({
122
+ node: node,
123
+ messageId: "noMessageErrorAny",
124
+ });
125
+ return;
126
+ }
127
+ // useRequest()
128
+ if (
129
+ node.callee.type === "Identifier" &&
130
+ node.callee.name === "useRequest"
131
+ ) {
132
+ useRequestCalls.push(node);
133
+ }
134
+
135
+ // .validateFields()
136
+ if (
137
+ node.callee.type === "MemberExpression" &&
138
+ node.callee.property.type === "Identifier" &&
139
+ node.callee.property.name === "validateFields"
140
+ ) {
141
+ validateFieldsCalls.push(node);
142
+ }
143
+ },
144
+
145
+ "Program:exit"() {
146
+ // 1. Form.Item without rules
147
+ for (const item of formItemNoRules) {
148
+ context.report({
149
+ node: item,
150
+ messageId: "needItemRules",
151
+ });
152
+ }
153
+
154
+ // 2. message.error in useRequest callbacks
155
+ for (const call of useRequestCalls) {
156
+ const options = call.arguments[1];
157
+ if (!options || options.type !== "ObjectExpression") continue;
158
+ for (const prop of options.properties) {
159
+ if (prop.type !== "Property") continue;
160
+ const key = prop.key;
161
+ if (
162
+ key.type !== "Identifier" ||
163
+ (key.name !== "onSuccess" &&
164
+ key.name !== "onError" &&
165
+ key.name !== "onFinally")
166
+ )
167
+ continue;
168
+ const text = sourceCode.getText(prop.value);
169
+ if (text.includes("message.error")) {
170
+ context.report({
171
+ node: prop,
172
+ messageId: "noMessageError",
173
+ });
174
+ }
175
+ }
176
+ }
177
+
178
+ // 3. Modal.onOk
179
+ for (const attr of modalOnOkAttrs) {
180
+ context.report({
181
+ node: attr,
182
+ messageId: "noModalOnOk",
183
+ });
184
+ }
185
+
186
+ // 4. form.validateFields()
187
+ if (hasFormTag && !hasFormOnFinish) {
188
+ for (const call of validateFieldsCalls) {
189
+ context.report({
190
+ node: call,
191
+ messageId: "needOnFinish",
192
+ });
193
+ }
194
+ }
195
+ },
196
+ };
197
+ },
198
+ };
199
+
200
+ export default rule;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * require-pro-components
3
+ * ----------------------
4
+ * 约束前端业务代码:
5
+ * 1. 禁止直接使用 antd <Modal>,必须用 @ant-design/pro-components 的 <ModalForm>
6
+ * 2. 禁止直接使用 <Form.Item>,必须用 @ant-design/pro-components 的 ProForm* 组件
7
+ * 3. ProForm* 组件必须设置 rules 进行校验,否则需要 eslint-disable 注明原因
8
+ *
9
+ * 例外场景:
10
+ * - noStyle 的 Form.Item(仅作布局容器)
11
+ * - ProForm 容器本身(不是字段组件)
12
+ * - ProFormDependency、ProFormFieldSet、ProFormList 等非字段组件
13
+ */
14
+
15
+ const PROFORM_CONTAINER_COMPONENTS = new Set([
16
+ "ProForm",
17
+ "ProFormDependency",
18
+ "ProFormFieldSet",
19
+ "ProFormList",
20
+ "ProFormGrid",
21
+ "ProFormGroup",
22
+ ]);
23
+
24
+ /** @type {import('eslint').Rule.RuleModule} */
25
+ const rule = {
26
+ meta: {
27
+ type: "suggestion",
28
+ docs: {
29
+ description:
30
+ "禁止直接使用 antd <Modal>(用 ModalForm 代替),禁止直接使用 <Form.Item>(用 ProForm* 代替),ProForm* 必须设置 rules",
31
+ },
32
+ schema: [],
33
+ messages: {
34
+ useModalForm:
35
+ "禁止直接使用 antd <Modal>,请使用 @ant-design/pro-components 的 <ModalForm> 代替。",
36
+ useProFormItem:
37
+ "禁止直接使用 <Form.Item>,请使用 @ant-design/pro-components 的 ProFormText / ProFormSelect / ProFormItem 等组件代替。",
38
+ needProFormRules:
39
+ "<{{tag}}> 必须设置 rules 进行校验。如果确实不需要校验,请用 // eslint-disable-next-line 注明原因。",
40
+ noSubmitHtmlType:
41
+ '禁止使用 <Button htmlType="submit">,请使用 ProForm 的 submitter 属性提交表单。',
42
+ },
43
+ },
44
+ create(context) {
45
+ const getTagName = (name) => {
46
+ if (name.type === "JSXIdentifier") return name.name;
47
+ if (
48
+ name.type === "JSXMemberExpression" &&
49
+ name.object.type === "JSXIdentifier" &&
50
+ name.property.type === "JSXIdentifier"
51
+ ) {
52
+ return `${name.object.name}.${name.property.name}`;
53
+ }
54
+ return "";
55
+ };
56
+
57
+ return {
58
+ JSXOpeningElement(node) {
59
+ const tagName = getTagName(node.name);
60
+
61
+ // <Modal>
62
+ if (tagName === "Modal") {
63
+ context.report({
64
+ node: node,
65
+ messageId: "useModalForm",
66
+ });
67
+ }
68
+
69
+ // <Form.Item>
70
+ if (tagName === "Form.Item") {
71
+ let isNoStyle = false;
72
+ for (const attr of node.attributes) {
73
+ if (attr.type !== "JSXAttribute" || !attr.name) continue;
74
+ if (attr.name.name === "noStyle") {
75
+ isNoStyle = true;
76
+ break;
77
+ }
78
+ }
79
+ if (!isNoStyle) {
80
+ context.report({
81
+ node: node,
82
+ messageId: "useProFormItem",
83
+ });
84
+ }
85
+ }
86
+
87
+ // <Button htmlType="submit">
88
+ if (tagName === "Button") {
89
+ for (const attr of node.attributes) {
90
+ if (attr.type !== "JSXAttribute" || !attr.name) continue;
91
+ if (
92
+ attr.name.name === "htmlType" &&
93
+ attr.value &&
94
+ attr.value.type === "Literal" &&
95
+ attr.value.value === "submit"
96
+ ) {
97
+ context.report({
98
+ node: attr,
99
+ messageId: "noSubmitHtmlType",
100
+ });
101
+ }
102
+ }
103
+ }
104
+
105
+ // ProForm* field components must have rules
106
+ if (
107
+ tagName.startsWith("ProForm") &&
108
+ !PROFORM_CONTAINER_COMPONENTS.has(tagName)
109
+ ) {
110
+ let hasRules = false;
111
+ for (const attr of node.attributes) {
112
+ if (attr.type !== "JSXAttribute" || !attr.name) continue;
113
+ if (attr.name.name === "rules") {
114
+ hasRules = true;
115
+ break;
116
+ }
117
+ }
118
+ if (!hasRules) {
119
+ context.report({
120
+ node: node,
121
+ messageId: "needProFormRules",
122
+ data: { tag: tagName },
123
+ });
124
+ }
125
+ }
126
+ },
127
+ };
128
+ },
129
+ };
130
+
131
+ export default rule;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * require-section-divider
3
+ * ------------------------
4
+ * 约束:大文件必须在顶层用 `=====` 等号注释块做分区。
5
+ *
6
+ * 分区要求(按行数):
7
+ * - 150 ~ 349 行:至少 1 个分区块
8
+ * - 350 ~ 549 行:至少 2 个分区块
9
+ * - 550 ~ 749 行:至少 3 个分区块
10
+ * 以此类推,每 200 行增加 1 个分区块。
11
+ *
12
+ * 分隔注释块格式(见 .agents/skills/dev-style/SKILL.md):
13
+ *
14
+ * // ==============================================================================
15
+ * // <中文标题>
16
+ * // ==============================================================================
17
+ *
18
+ * 本规则检查文件中所有 ==== 三行注释块的数量是否达到要求。
19
+ */
20
+
21
+ const DIVIDER_EQ = /^\s*={4,}\s*$/;
22
+
23
+ /** 相邻两行注释 + 之间中间一行文字:判定一个三行注释块为分隔块。 */
24
+ function isDividerBlock(commentA, commentB, commentC) {
25
+ if (!commentA || !commentB || !commentC) return false;
26
+ if (
27
+ commentA.type !== "Line" ||
28
+ commentB.type !== "Line" ||
29
+ commentC.type !== "Line"
30
+ )
31
+ return false;
32
+ if (!DIVIDER_EQ.test(commentA.value)) return false;
33
+ if (!DIVIDER_EQ.test(commentC.value)) return false;
34
+ if (!commentB.value.trim()) return false;
35
+ if (commentA.loc.end.line + 1 !== commentB.loc.start.line) return false;
36
+ if (commentB.loc.end.line + 1 !== commentC.loc.start.line) return false;
37
+ return true;
38
+ }
39
+
40
+ /** 计算文件需要多少个分区块(每 150 行 1 个,从 150 行起)。 */
41
+ function requiredDividers(totalLines, minLines) {
42
+ if (totalLines < minLines) return 0;
43
+ // 150-299: 1个, 300-449: 2个, 依此类推
44
+ return Math.floor(totalLines / minLines);
45
+ }
46
+
47
+ /** @type {import('eslint').Rule.RuleModule} */
48
+ const rule = {
49
+ meta: {
50
+ type: "suggestion",
51
+ docs: {
52
+ description:
53
+ "大文件(≥ 150 行)必须按行数用适当数量的 `===` 等号注释块分区。",
54
+ },
55
+ schema: [
56
+ {
57
+ type: "object",
58
+ properties: {
59
+ minLines: { type: "number" },
60
+ },
61
+ additionalProperties: false,
62
+ },
63
+ ],
64
+ messages: {
65
+ missing:
66
+ "大文件需要更多 `===` 等号注释块分区。当前需要 {{required}} 个,实际只有 {{actual}} 个。每约 150 行至少应有一个分区块。",
67
+ },
68
+ },
69
+ create(context) {
70
+ const options = context.options[0] || {};
71
+ const minLines = options.minLines ?? 150;
72
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
73
+
74
+ return {
75
+ Program(program) {
76
+ const totalLines = program.loc.end.line;
77
+ if (totalLines < minLines) return;
78
+
79
+ const required = requiredDividers(totalLines, minLines);
80
+ if (required <= 0) return;
81
+
82
+ const allComments = sourceCode.getAllComments();
83
+
84
+ let dividerCount = 0;
85
+ for (let i = 0; i + 2 < allComments.length; i += 1) {
86
+ if (
87
+ isDividerBlock(
88
+ allComments[i],
89
+ allComments[i + 1],
90
+ allComments[i + 2],
91
+ )
92
+ ) {
93
+ dividerCount += 1;
94
+ i += 2;
95
+ }
96
+ }
97
+
98
+ if (dividerCount < required) {
99
+ context.report({
100
+ loc: {
101
+ start: { line: 1, column: 0 },
102
+ end: { line: 1, column: 0 },
103
+ },
104
+ messageId: "missing",
105
+ data: {
106
+ required: String(required),
107
+ actual: String(dividerCount),
108
+ },
109
+ });
110
+ }
111
+ },
112
+ };
113
+ },
114
+ };
115
+
116
+ export default rule;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * require-use-form
3
+ * ----------------
4
+ * 约束:前端业务代码中,表单组件禁止用 useState/useSetState 管理字段值。
5
+ * 必须使用 Form.useForm() 或 rc-field-form 的 useForm。
6
+ *
7
+ * 检测方式:
8
+ * - 文件中出现 JSX 元素且同时包含 value 和 onChange props
9
+ * - 同时该文件在顶层使用了 useState 或 useSetState
10
+ * → 报错提示改用 useForm
11
+ *
12
+ * 例外通过文件路径 overrides 处理(utils、hooks、theme 等已豁免)。
13
+ */
14
+
15
+ /** @type {import('eslint').Rule.RuleModule} */
16
+ const rule = {
17
+ meta: {
18
+ type: "suggestion",
19
+ docs: {
20
+ description:
21
+ "表单组件必须使用 Form.useForm() 或 rc-field-form useForm 管理状态,禁止用 useState/useSetState 管理字段值。",
22
+ },
23
+ schema: [],
24
+ messages: {
25
+ useFormRequired:
26
+ "检测到表单场景使用了 useState/useSetState 管理字段值。请改用 Form.useForm() 或 rc-field-form 的 useForm。详见 .agents/skills/form-convention/SKILL.md。",
27
+ },
28
+ },
29
+ create(context) {
30
+ // sourceCode available via context if needed for future enhancement
31
+
32
+ // 1. 检测 JSX 元素是否同时包含 value 和 onChange props → 认为是表单组件
33
+ let hasFormPattern = false;
34
+
35
+ // 2. 检测是否有 useState / useSetState 调用
36
+ let hasStateHook = false;
37
+
38
+ const stateHooks = new Set(["useState", "useSetState"]);
39
+
40
+ return {
41
+ JSXOpeningElement(node) {
42
+ if (hasFormPattern) return;
43
+
44
+ let hasValue = false;
45
+ let hasOnChange = false;
46
+
47
+ for (const attr of node.attributes) {
48
+ if (attr.type !== "JSXAttribute" || !attr.name) continue;
49
+ const attrName = attr.name.name;
50
+ if (attrName === "value") hasValue = true;
51
+ else if (attrName === "onChange") hasOnChange = true;
52
+
53
+ if (hasValue && hasOnChange) {
54
+ hasFormPattern = true;
55
+ break;
56
+ }
57
+ }
58
+ },
59
+ CallExpression(node) {
60
+ const callee = node.callee;
61
+ if (callee.type === "Identifier" && stateHooks.has(callee.name)) {
62
+ hasStateHook = true;
63
+ }
64
+ },
65
+ "Program:exit"() {
66
+ if (hasFormPattern && hasStateHook) {
67
+ context.report({
68
+ loc: { line: 1, column: 0 },
69
+ messageId: "useFormRequired",
70
+ });
71
+ }
72
+ },
73
+ };
74
+ },
75
+ };
76
+
77
+ export default rule;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai0x0/utils",
3
- "version": "0.0.26",
3
+ "version": "0.0.27",
4
4
  "description": "AI0x0 utils",
5
5
  "keywords": [
6
6
  "ai0x0"
@@ -20,6 +20,7 @@
20
20
  "module": "./es/index.js",
21
21
  "types": "./lib/index.d.ts",
22
22
  "files": [
23
+ "eslint-rules",
23
24
  "lib",
24
25
  "es",
25
26
  "SKILL.md"