@ai0x0/utils 0.8.0 → 0.9.0
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/.agents/skills/antd/SKILL.md +0 -1
- package/.agents/skills/nextjs/SKILL.md +1 -1
- package/README.md +3 -2
- package/eslint-config/index.js +12 -1
- package/eslint-rules/index.js +12 -6
- package/eslint-rules/max-lines.js +14 -1
- package/eslint-rules/no-hardcoded-style.js +1 -1
- package/eslint-rules/no-hardcoded-text.js +157 -0
- package/eslint-rules/require-use-form.js +1 -1
- package/package.json +1 -1
- package/eslint-rules/require-pro-components.js +0 -131
|
@@ -239,7 +239,6 @@ setState({ open: false, current: undefined });
|
|
|
239
239
|
|
|
240
240
|
- `require-use-form`:受控表单字段必须交给 Form 管理。
|
|
241
241
|
- `require-form-convention`:Form/onFinish/rules/Modal 提交/错误提示约定。
|
|
242
|
-
- `require-pro-components`:ModalForm、ProForm 字段和 submitter 约定。
|
|
243
242
|
- `no-hardcoded-style`:token 样式约定。
|
|
244
243
|
- `no-antd-space`:Flex 替代 Space。
|
|
245
244
|
- `no-use-request-run`:`runAsync` 替代 `run`。
|
|
@@ -235,9 +235,9 @@ ESLint 按行数强制分区数量:
|
|
|
235
235
|
- `ai0x0/no-antd-space`:禁止 `<Space>` / `<Space.Compact>`,使用 `<Flex>`。
|
|
236
236
|
- `ai0x0/require-use-form`:表单字段状态不能用 `useState` / `useSetState` 管。
|
|
237
237
|
- `ai0x0/require-form-convention`:Form 必须 `onFinish`,字段必须 rules,禁止 Modal.onOk 提交。
|
|
238
|
-
- `ai0x0/require-pro-components`:业务表单优先 ProComponents。
|
|
239
238
|
- `ai0x0/no-use-request-run`:禁止解构 `run`,使用 `runAsync` 并在调用处 `await`。
|
|
240
239
|
- `ai0x0/no-consecutive-setstate`:连续 `setState({ ... })` 要合并成一次。
|
|
240
|
+
- scoped(不进 recommended,项目自行按 files 开启):`ai0x0/api-route-via-nrf`、`ai0x0/no-direct-api-url`、`ai0x0/no-hardcoded-text`(i18n 裸文案闸,legacy 旧账名单经选项传入)。
|
|
241
241
|
|
|
242
242
|
## 常用命令
|
|
243
243
|
|
package/README.md
CHANGED
|
@@ -110,7 +110,6 @@ export default [
|
|
|
110
110
|
| `no-one-letter-vars` | Forbid single-letter variable names |
|
|
111
111
|
| `no-then` | Prefer `async/await` over `.then()` |
|
|
112
112
|
| `no-antd-space` | Use `<Flex>` instead of `<Space>` |
|
|
113
|
-
| `require-pro-components` | Require ProComponents usage |
|
|
114
113
|
| `require-section-divider` | Large files need `===` comment dividers |
|
|
115
114
|
| `no-consecutive-setstate` | Merge consecutive `setState` calls |
|
|
116
115
|
| `no-use-request-run` | Forbid `useRequest.run()` |
|
|
@@ -118,6 +117,8 @@ export default [
|
|
|
118
117
|
| `require-form-convention` | Form naming conventions |
|
|
119
118
|
| `max-lines` | Limit each file to 500 code lines and prompt splitting into child files |
|
|
120
119
|
|
|
120
|
+
Scoped rules (not in `recommended`; enable per-file-glob in your own config): `api-route-via-nrf`, `no-direct-api-url`, `no-hardcoded-text` (i18n hardcoded-text gate with a `legacy` exemption list).
|
|
121
|
+
|
|
121
122
|
### Shared restrictions (`eslint-config/`)
|
|
122
123
|
|
|
123
124
|
Pre-defined `no-restricted-syntax` selectors for antd + ahooks convention enforcement:
|
|
@@ -147,7 +148,7 @@ export default [
|
|
|
147
148
|
];
|
|
148
149
|
```
|
|
149
150
|
|
|
150
|
-
Restrictions cover: `TryStatement`, `as` (non-const), `useState`, `useEffect`, `useMemo`, `useCallback`, native `div/span/p/h` tags, hardcoded colors, `fetch` and `new WebSocket()`.
|
|
151
|
+
Restrictions cover: `TryStatement`, `as` (non-const), `useState`, `useEffect`, `useMemo`, `useCallback`, `memo()`, native `div/span/p/h` tags, hardcoded colors, `fetch` and `new WebSocket()`.
|
|
151
152
|
|
|
152
153
|
## Installation
|
|
153
154
|
|
package/eslint-config/index.js
CHANGED
|
@@ -6,10 +6,15 @@
|
|
|
6
6
|
* import { ai0x0, allRestrictions, allRestrictionsWithNetworking }
|
|
7
7
|
* from "@ai0x0/utils/eslint-config/index.js";
|
|
8
8
|
*/
|
|
9
|
-
import localPlugin from "../eslint-rules/index.js";
|
|
9
|
+
import localPlugin, { SCOPED_RULES } from "../eslint-rules/index.js";
|
|
10
10
|
|
|
11
11
|
export { localPlugin as ai0x0 };
|
|
12
12
|
|
|
13
|
+
// 不进 recommended、需要调用方按 files 自行开启的规则名单。消费方按规则名全量铺 recommended
|
|
14
|
+
// 之外的规则时(比如自己组装 rules 表),用这个集合过滤,别在调用方手抄一份 —— 手抄的
|
|
15
|
+
// 那份在新增 scoped 规则时不会跟着更新。
|
|
16
|
+
export { SCOPED_RULES };
|
|
17
|
+
|
|
13
18
|
export const allRestrictions = [
|
|
14
19
|
{
|
|
15
20
|
selector: "TryStatement",
|
|
@@ -40,6 +45,12 @@ export const allRestrictions = [
|
|
|
40
45
|
selector: "CallExpression[callee.name='useCallback']",
|
|
41
46
|
message: "禁止 useCallback。React Compiler 会自动记忆化,无需手动优化。",
|
|
42
47
|
},
|
|
48
|
+
{
|
|
49
|
+
selector:
|
|
50
|
+
":matches(CallExpression[callee.name='memo'], CallExpression[callee.object.name='React'][callee.property.name='memo'])",
|
|
51
|
+
message:
|
|
52
|
+
"禁止 memo()。React Compiler 会自动记忆化;手写 memo 只会多一套独立的 props 浅比较,还容易把热点修在组件边界上、掩盖真正不稳定的 props。",
|
|
53
|
+
},
|
|
43
54
|
{
|
|
44
55
|
selector: "JSXOpeningElement[name.name=/^(div|span|p|h[1-6])$/]",
|
|
45
56
|
message:
|
package/eslint-rules/index.js
CHANGED
|
@@ -5,7 +5,6 @@ import requireSectionDivider from "./require-section-divider.js";
|
|
|
5
5
|
import noHardcodedStyle from "./no-hardcoded-style.js";
|
|
6
6
|
import requireUseForm from "./require-use-form.js";
|
|
7
7
|
import requireFormConvention from "./require-form-convention.js";
|
|
8
|
-
import requireProComponents from "./require-pro-components.js";
|
|
9
8
|
import noAntdSpace from "./no-antd-space.js";
|
|
10
9
|
import noUseRequestRun from "./no-use-request-run.js";
|
|
11
10
|
import noThen from "./no-then.js";
|
|
@@ -14,13 +13,13 @@ import noConsecutiveSetState from "./no-consecutive-setstate.js";
|
|
|
14
13
|
import maxLines from "./max-lines.js";
|
|
15
14
|
import apiRouteViaNrf from "./api-route-via-nrf.js";
|
|
16
15
|
import noDirectApiUrl from "./no-direct-api-url.js";
|
|
16
|
+
import noHardcodedText from "./no-hardcoded-text.js";
|
|
17
17
|
|
|
18
18
|
const rules = {
|
|
19
19
|
"require-section-divider": requireSectionDivider,
|
|
20
20
|
"no-hardcoded-style": noHardcodedStyle,
|
|
21
21
|
"require-use-form": requireUseForm,
|
|
22
22
|
"require-form-convention": requireFormConvention,
|
|
23
|
-
"require-pro-components": requireProComponents,
|
|
24
23
|
"no-antd-space": noAntdSpace,
|
|
25
24
|
"no-use-request-run": noUseRequestRun,
|
|
26
25
|
"no-then": noThen,
|
|
@@ -29,14 +28,21 @@ const rules = {
|
|
|
29
28
|
"max-lines": maxLines,
|
|
30
29
|
"api-route-via-nrf": apiRouteViaNrf,
|
|
31
30
|
"no-direct-api-url": noDirectApiUrl,
|
|
31
|
+
"no-hardcoded-text": noHardcodedText,
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
//
|
|
34
|
+
// 这三条只管「特定位置」的代码,不能跟着 recommended 全局开:
|
|
35
35
|
// · api-route-via-nrf 只该挂在 API 路由文件上(别的文件 export 一个叫 GET 的函数是自由的);
|
|
36
|
-
// · no-direct-api-url 只该挂在前端目录上(后端与中间件里出现 "/api/..."
|
|
36
|
+
// · no-direct-api-url 只该挂在前端目录上(后端与中间件里出现 "/api/..." 字面量是本分);
|
|
37
|
+
// · no-hardcoded-text 只该挂在做了国际化的项目上(没接语言包的项目开了就是满屏误报),
|
|
38
|
+
// 且旧账豁免名单由调用方经 legacy 选项传入。
|
|
37
39
|
// 调用方在 flat config 里用 files 圈定范围自行开启,豁免文件用 files override 关掉,
|
|
38
|
-
//
|
|
39
|
-
const SCOPED_RULES = new Set([
|
|
40
|
+
// 用法写在各条规则的文件头。
|
|
41
|
+
export const SCOPED_RULES = new Set([
|
|
42
|
+
"api-route-via-nrf",
|
|
43
|
+
"no-direct-api-url",
|
|
44
|
+
"no-hardcoded-text",
|
|
45
|
+
]);
|
|
40
46
|
|
|
41
47
|
const plugin = {
|
|
42
48
|
rules,
|
|
@@ -62,7 +62,7 @@ const rule = {
|
|
|
62
62
|
],
|
|
63
63
|
messages: {
|
|
64
64
|
tooManyLines:
|
|
65
|
-
"单文件代码行数过多({{ lineCount }} 行,最多 {{ max }}
|
|
65
|
+
"单文件代码行数过多({{ lineCount }} 行,最多 {{ max }} 行{{ skipped }}),请拆分为子文件。",
|
|
66
66
|
},
|
|
67
67
|
},
|
|
68
68
|
create(context) {
|
|
@@ -97,6 +97,18 @@ const rule = {
|
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// 行数口径要写进消息里:默认注释与空行都不算,不说明的话看消息的人会拿
|
|
101
|
+
// 编辑器底部的总行数去对,对不上。
|
|
102
|
+
const skippedParts = [];
|
|
103
|
+
if (options.skipComments) {
|
|
104
|
+
skippedParts.push("注释");
|
|
105
|
+
}
|
|
106
|
+
if (options.skipBlankLines) {
|
|
107
|
+
skippedParts.push("空行");
|
|
108
|
+
}
|
|
109
|
+
const skipped =
|
|
110
|
+
skippedParts.length > 0 ? `,${skippedParts.join("与")}不计入` : "";
|
|
111
|
+
|
|
100
112
|
context.report({
|
|
101
113
|
node,
|
|
102
114
|
loc: {
|
|
@@ -107,6 +119,7 @@ const rule = {
|
|
|
107
119
|
data: {
|
|
108
120
|
lineCount,
|
|
109
121
|
max: options.max,
|
|
122
|
+
skipped,
|
|
110
123
|
},
|
|
111
124
|
});
|
|
112
125
|
},
|
|
@@ -450,7 +450,7 @@ const rule = {
|
|
|
450
450
|
schema: [],
|
|
451
451
|
messages: {
|
|
452
452
|
noHardcoded:
|
|
453
|
-
"禁止硬编码 `{{ {{key}}: {{value}} }}
|
|
453
|
+
"禁止硬编码 `{{ {{key}}: {{value}} }}`。请用 antd token:间距用 `token.marginSM` / `token.paddingSM`,颜色用 `token.colorPrimary` / `token.colorText`,圆角用 `token.borderRadius`(经 `theme.useToken()` 或 createStyles 的 token 参数取)。",
|
|
454
454
|
},
|
|
455
455
|
},
|
|
456
456
|
create(context) {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// 旧账名单之外的文件里,JSX 正文与文案属性不许写裸中文
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// 国际化迁移要跨好几周,期间新代码还在往里加中文 —— 不拦的话分母是个移动靶,做得再快也
|
|
5
|
+
// 追不上。但把整仓库一起开成 error 是不现实的(当场几千个报错,只会被人整条关掉)。
|
|
6
|
+
//
|
|
7
|
+
// 所以这条规则带一份**只减不增的旧账名单**(由各项目的生成脚本维护,经 options.legacy
|
|
8
|
+
// 传入):名单里的文件一个字都不报,名单外的一条都不许有。**新建的文件天然在名单外**,
|
|
9
|
+
// 从第一行起就受管;一个文件迁完了就从名单里划掉,划掉之后它再也回不去。
|
|
10
|
+
//
|
|
11
|
+
// 名单是「文件还剩多少没迁」的唯一判据,不能拿「这个文件有没有 import i18n 库」代替 ——
|
|
12
|
+
// 迁移是分批的(先抽共用词表、再按模块走),一个文件很可能刚接上语言包、里头还剩二十条
|
|
13
|
+
// 没动。按 import 判的话,那二十条会在文件刚接上的当天集体变成 error。
|
|
14
|
+
//
|
|
15
|
+
// 只认两个位置 —— JSX 正文与文案属性。这不是偷懒:正是这两处不会与「中文当值用」相混
|
|
16
|
+
//(.describe() 的 API 文档、枚举值、css`` 里的注释都长不到这两个位置上),所以这条规则
|
|
17
|
+
// 不需要那一整套语法位置判断,也就不会误报。
|
|
18
|
+
//
|
|
19
|
+
// **这条规则不进 recommended**(见 index.js 的 SCOPED_RULES):没做国际化的项目不该被它
|
|
20
|
+
// 扫到。要用的项目在自己的 flat config 里按 files 圈定范围、把自己的旧账名单传给
|
|
21
|
+
// legacy 选项。
|
|
22
|
+
|
|
23
|
+
const CJK = /[一-鿿]/;
|
|
24
|
+
|
|
25
|
+
/** 摆文案的属性名。调用方若有配套的文案扫描脚本,两份名单要保持同源。 */
|
|
26
|
+
const TEXT_PROPS = new Set([
|
|
27
|
+
"label",
|
|
28
|
+
"title",
|
|
29
|
+
"placeholder",
|
|
30
|
+
"tooltip",
|
|
31
|
+
"desc",
|
|
32
|
+
"description",
|
|
33
|
+
"okText",
|
|
34
|
+
"cancelText",
|
|
35
|
+
"emptyText",
|
|
36
|
+
"extra",
|
|
37
|
+
"header",
|
|
38
|
+
"footer",
|
|
39
|
+
"alt",
|
|
40
|
+
"tip",
|
|
41
|
+
"subTitle",
|
|
42
|
+
"content",
|
|
43
|
+
"message",
|
|
44
|
+
"hint",
|
|
45
|
+
"heading",
|
|
46
|
+
"caption",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// 取值
|
|
51
|
+
// =============================================================================
|
|
52
|
+
|
|
53
|
+
/** 一个节点里的中文文本,没有就返回空串。模板串把静态片段拼起来看。 */
|
|
54
|
+
function chineseIn(node) {
|
|
55
|
+
if (!node) {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
59
|
+
return CJK.test(node.value) ? node.value : "";
|
|
60
|
+
}
|
|
61
|
+
if (node.type === "TemplateLiteral" && node.quasis) {
|
|
62
|
+
const text = node.quasis.map((part) => part.value.raw).join("${…}");
|
|
63
|
+
return CJK.test(text) ? text : "";
|
|
64
|
+
}
|
|
65
|
+
// JSX 属性里的 `{...}`:真正的值在里头。
|
|
66
|
+
if (node.type === "JSXExpressionContainer") {
|
|
67
|
+
return chineseIn(node.expression);
|
|
68
|
+
}
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 属性名。标识符(`label:`、`label=`)读 name,字符串键(`"label":`)读 value。 */
|
|
73
|
+
function nameOf(node) {
|
|
74
|
+
if (!node) {
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
if (typeof node.name === "string") {
|
|
78
|
+
return node.name;
|
|
79
|
+
}
|
|
80
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
81
|
+
return node.value;
|
|
82
|
+
}
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// 规则
|
|
88
|
+
// =============================================================================
|
|
89
|
+
|
|
90
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
91
|
+
const rule = {
|
|
92
|
+
meta: {
|
|
93
|
+
type: "problem",
|
|
94
|
+
docs: {
|
|
95
|
+
description: "旧账名单之外的文件里,JSX 正文与文案属性不许写裸中文。",
|
|
96
|
+
},
|
|
97
|
+
schema: [
|
|
98
|
+
{
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
/** 还没迁完的文件(仓库相对路径),这些文件里一个字都不报。 */
|
|
102
|
+
legacy: { type: "array", items: { type: "string" } },
|
|
103
|
+
},
|
|
104
|
+
additionalProperties: false,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
messages: {
|
|
108
|
+
hardcoded:
|
|
109
|
+
"界面文案要走语言包:把「{{text}}」搬进语言包文件,用 i18n 取值函数读取。这个文件不在旧账豁免名单里。",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
create(context) {
|
|
113
|
+
const options = context.options[0] ?? {};
|
|
114
|
+
// eslint 给的是绝对路径,名单里存的是仓库相对路径 —— 按后缀比,免得把 cwd 也写进名单
|
|
115
|
+
//(那样名单换台机器就全失效,而失效的样子是「突然多出几千个报错」)。
|
|
116
|
+
const here = context.filename.replaceAll("\\", "/");
|
|
117
|
+
// 测试文件里的中文是**夹具**不是界面文案:被测的输入本身就该是中文。名单是「只减不增」
|
|
118
|
+
// 的,把测试文件塞进去等于把它变成垃圾桶。
|
|
119
|
+
const isTest = /\.test\.tsx?$/.test(here);
|
|
120
|
+
const skipped =
|
|
121
|
+
isTest || (options.legacy ?? []).some((file) => here.endsWith(file));
|
|
122
|
+
|
|
123
|
+
const report = (node, text) => {
|
|
124
|
+
if (skipped || !text) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
context.report({
|
|
128
|
+
node,
|
|
129
|
+
messageId: "hardcoded",
|
|
130
|
+
data: { text: text.replace(/\s+/g, " ").trim().slice(0, 20) },
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
JSXText(node) {
|
|
136
|
+
const text = node.value ?? "";
|
|
137
|
+
if (CJK.test(text)) {
|
|
138
|
+
report(node, text);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
JSXAttribute(node) {
|
|
142
|
+
if (!TEXT_PROPS.has(nameOf(node.name))) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
report(node, chineseIn(node.value));
|
|
146
|
+
},
|
|
147
|
+
Property(node) {
|
|
148
|
+
if (!TEXT_PROPS.has(nameOf(node.key))) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
report(node, chineseIn(node.value));
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export default rule;
|
|
@@ -23,7 +23,7 @@ const rule = {
|
|
|
23
23
|
schema: [],
|
|
24
24
|
messages: {
|
|
25
25
|
useFormRequired:
|
|
26
|
-
"检测到表单场景使用了 useState/useSetState 管理字段值。请改用 Form.useForm()
|
|
26
|
+
"检测到表单场景使用了 useState/useSetState 管理字段值。请改用 Form.useForm()(ProForm 场景直接交给 ProForm 字段),不要手写受控状态。",
|
|
27
27
|
},
|
|
28
28
|
},
|
|
29
29
|
create(context) {
|
package/package.json
CHANGED
|
@@ -1,131 +0,0 @@
|
|
|
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;
|