Skip to content

Commit c5571c8

Browse files
RSS1102cursoragent
andcommitted
fix: 完善 API 文档生成的通用解析规则
- 从 AST 正确提取构造参数类型与默认值,过滤 this.xxx 误识别 - 合并类字段到 API 表,隔离目标类解析作用域 - abstract 类实例方法写入文档;Markdown 表格转义与参数格式化 - super.key 无显式类型时统一展示为 Key? - 补充 README 工具职责边界说明与本地测试脚本 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 295e105 commit c5571c8

6 files changed

Lines changed: 380 additions & 146 deletions

File tree

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,50 @@
2121
/// 属性简介(必须)
2222
```
2323

24+
### 工具职责边界
25+
26+
工具只负责**通用 AST 解析规则**,不会对个别组件做特殊兼容,也不会修正源码里不合规的注释。
27+
28+
| 由工具负责(通用规则) | 由源码注释负责(需合规编写) |
29+
| --- | --- |
30+
| 从构造参数 AST 提取类型、默认值 | 字段/参数的 `///` 说明文案 |
31+
| 合并类字段到 API 表(含未入构造函数的 public 字段) | 注释内容与字段语义一致(如 content 不应写「标题」) |
32+
| 过滤 `this.xxx` 被误识别为默认值 | 错别字、遗漏注释、注释写在错误位置 |
33+
| 解析 `abstract class` 实例方法、工厂构造 | `super.key` 等场景的类型展示(后续可按 AST 规则增强) |
34+
| Markdown 表格转义、方法参数格式化 | 无注释时说明列显示 `-`(符合预期) |
35+
36+
**原则:** 注释不合规导致的文档问题,应在组件源码中补全/修正 `///` 注释,而不是在工具里打补丁。
37+
2438
### 组件demo注释示例
2539

2640
```dart
2741
/// demo名称(可以为空,为空的时候默认显示组件名称)
2842
/// demo示例介绍(可以为空)
2943
```
3044

45+
## 本地开发与测试
46+
47+
`tdesign-component/pubspec.yaml` 使用 path 依赖指向本仓库时,可在本地直接验证文档生成,无需发布到 git:
48+
49+
```bash
50+
# 1. 确保 component 的 pubspec 已配置:
51+
# tdesign_flutter_tools:
52+
# path: ../tdesign-flutter-tools
53+
54+
# 2. 在 component 目录解析依赖(需要网络)
55+
cd ../tdesign-component && dart pub get
56+
57+
# 3. 运行本地测试脚本(picker / calendar / dialog)
58+
./scripts/local_test.sh picker
59+
```
60+
61+
`dart pub get` 因网络不可用失败,可临时将 `tdesign-component/.dart_tool/package_config.json`
62+
`tdesign_flutter_tools``rootUri` 指向本地路径,脚本会通过 `--packages` 跳过联网校验:
63+
64+
```bash
65+
./scripts/local_test.sh calendar
66+
```
67+
3168
## 组件库工具使用方法
3269

3370
### 初始化工具调用命令

lib/component_rule.dart

Lines changed: 139 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -81,118 +81,119 @@ class ComponentAstVisitor extends RecursiveAstVisitor<void> {
8181
ComponentInfo? componentInfo;
8282
List<PropertyInfo> propertyList = [];
8383
Map<String,PropertyInfo> fieldMap = {};
84+
// 当前正在解析的目标类名(null 表示不在目标类内)
85+
String? _currentTargetClassName;
86+
// 当前正在解析的类是否是 abstract class(用于收集实例方法)
87+
bool _currentClassIsAbstract = false;
88+
89+
bool get _isInTargetClass => _currentTargetClassName != null;
90+
91+
void _resetClassState() {
92+
componentInfo = null;
93+
propertyList = [];
94+
fieldMap = {};
95+
_currentTargetClassName = null;
96+
_currentClassIsAbstract = false;
97+
}
98+
99+
PropertyInfo _buildPropertyFromParameter(FormalParameter param) {
100+
final PropertyInfo item = PropertyInfo();
101+
item.name = param.name?.lexeme.toString() ?? '';
102+
item.isRequired =
103+
param.isRequired || param.toSource().toString().startsWith('@required');
104+
item.isNamed = param.isNamed;
105+
item.type = extractFormalParameterType(param);
106+
item.defaultValue = formatDefaultValueForDoc(
107+
extractFormalParameterDefaultValue(param),
108+
paramName: item.name,
109+
isRequired: item.isRequired,
110+
);
111+
return item;
112+
}
113+
114+
void _mergeExtraFieldsIntoPropertyList() {
115+
for (final MapEntry<String, PropertyInfo> entry in fieldMap.entries) {
116+
if (entry.key.startsWith('_')) {
117+
continue;
118+
}
119+
final bool exists =
120+
propertyList.any((PropertyInfo element) => element.name == entry.key);
121+
if (!exists) {
122+
final PropertyInfo field = entry.value;
123+
field.name = entry.key;
124+
propertyList.add(field);
125+
}
126+
}
127+
}
128+
129+
void _fillPropertyFromFieldMap(PropertyInfo item) {
130+
final PropertyInfo? field = fieldMap[item.name];
131+
if (field == null) {
132+
return;
133+
}
134+
if (item.type.isEmpty && field.type.isNotEmpty) {
135+
item.type = field.type;
136+
}
137+
if (item.introduction.isEmpty && field.introduction.isNotEmpty) {
138+
item.introduction = field.introduction;
139+
}
140+
}
84141

85142
@override
86143
void visitConstructorDeclaration(ConstructorDeclaration node) {
144+
if (!_isInTargetClass) {
145+
node.visitChildren(this);
146+
return;
147+
}
87148
node.visitChildren(this);
88-
// 无命名构造函数
89-
// List childEntities = node.childEntities.toList();
90-
// bool isNormalConstructor = childEntities.isNotEmpty && nameList!.contains(childEntities[0].toString());
91-
// bool isConstConstructor = childEntities.length >= 2 && childEntities[0].toString() == 'const' && nameList!.contains(childEntities[1].toString());
92149
if (node.name == null) {
93150
for (final FormalParameter param in node.parameters.parameters) {
94-
List<String> strList = [];
95-
strList.add('identifier:' + (param.name?.lexeme.toString() ?? ""));
96-
strList.add('isNamed:' + param.isNamed.toString());
97-
strList.add('isOptional:' + param.isOptional.toString());
98-
strList.add('isOptionalNamed:' + param.isOptionalNamed.toString());
99-
strList.add('isOptionalPositional:' + param.isOptionalPositional.toString());
100-
strList.add('isPositional:' + param.isPositional.toString());
101-
strList.add('isRequired:' + param.isRequired.toString());
102-
strList.add('isRequiredNamed:' + param.isRequiredNamed.toString());
103-
strList.add('isRequiredPositional:' + param.isRequiredPositional.toString());
104-
strList.add('requiredKeyword:' + param.requiredKeyword.toString());
105-
strList.add('declaredElement:' + param.declaredElement.toString());
106-
strList.add('parent:' + param.toSource().toString());
107-
strList.add('beginToken:' + param.beginToken.toString());
108-
strList.add('endToken:' + param.endToken.toString());
109-
String tmp = '';
110-
if (param.childEntities.isNotEmpty) {
111-
tmp = param.childEntities.map((e) => e.toString()).toList().join('|');
112-
}
113-
strList.add('childEntities:' + tmp);
114-
Debug.yellow('构造参数[$folderName]: ${strList.join(', ')}');
115-
PropertyInfo item = PropertyInfo();
116-
item.name = param.name?.lexeme.toString() ?? "";
117-
item.isRequired = param.isRequired || param.toSource().toString().startsWith('@required');
118-
item.isNamed = param.isNamed;
119-
if (param.childEntities.length == 3) {
120-
item.defaultValue = param.childEntities.toList().last.toString();
121-
}
122-
if (tmp.startsWith('Key') && param.beginToken.toString() == 'Key') {
123-
item.type = 'Key';
124-
}
125-
propertyList.add(item);
151+
Debug.yellow('构造参数[$folderName]: ${param.toSource()}');
152+
propertyList.add(_buildPropertyFromParameter(param));
126153
}
127154
} else {
128155
// 记录工厂构造方法
129-
StaticMethodInfo staticMethodInfo = new StaticMethodInfo();
156+
final StaticMethodInfo staticMethodInfo = StaticMethodInfo();
130157
staticMethodInfo.name = node.name.toString();
131-
staticMethodInfo.introduction = removeDocumentationComment(node.documentationComment?.tokens.join("\n") ?? "");
132-
// staticMethodInfo.returnType = node.returnType.type.toString();
133-
node.parameters.parameters.forEach((element) {
134-
PropertyInfo info = PropertyInfo();
135-
info.name = element.name?.lexeme.toString() ?? "Null";
136-
// if (element is SimpleFormalParameter) {
137-
// info.type = element.type.toString();
138-
// } else if(element is DefaultFormalParameter && element.parameter is FieldFormalParameter){
139-
// info.type = (element.parameter as FieldFormalParameter).parameters.toString();
140-
// } else if(element is FieldFormalParameter) {
141-
// info.name = element.toString();
142-
// }
143-
144-
info.isRequired =
145-
element.isRequiredNamed || element.isRequiredPositional;
146-
info.isNamed = element.isNamed;
147-
148-
staticMethodInfo.params.add(info);
149-
});
158+
staticMethodInfo.introduction = removeDocumentationComment(
159+
node.documentationComment?.tokens.join('\n') ?? '');
160+
for (final FormalParameter element in node.parameters.parameters) {
161+
staticMethodInfo.params.add(_buildPropertyFromParameter(element));
162+
}
150163
componentInfo ??= ComponentInfo();
151164
componentInfo!.constructorMethodList.add(staticMethodInfo);
152165
}
153-
String tmp = '';
154-
if (node.childEntities.isNotEmpty) {
155-
tmp = node.childEntities.map((e) => e.toString()).toList().join('|');
156-
}
157-
List<String> strList = [];
158-
strList.add(node.firstTokenAfterCommentAndMetadata.toString());
159-
strList.add(node.parameters.parameters.map((e) => e.name?.lexeme.toString() ?? "").toList().join('|'));
160-
strList.add(node.childEntities.length.toString());
161-
strList.add(tmp);
162-
strList.add(node.beginToken.toString());
163-
strList.add(node.name.toString());
164-
// strList.add(node.toSource());
165-
Debug.green('构造函数[$folderName]: ${strList.join(', ')}');
166+
Debug.green(
167+
'构造函数[$folderName]: ${node.name ?? 'default'} | ${node.parameters.parameters.map((FormalParameter e) => e.name?.lexeme).join('|')}');
166168
}
167169

168170
@override
169171
void visitFieldDeclaration(FieldDeclaration node) {
170-
node.visitChildren(this);
171-
String tmp = '';
172-
if (node.childEntities.isNotEmpty) {
173-
tmp = node.childEntities.map((e) => e.toString()).toList().join('|');
172+
if (!_isInTargetClass) {
173+
node.visitChildren(this);
174+
return;
174175
}
175-
List<String> strList = [];
176-
strList.add(node.childEntities.length.toString());
177-
strList.add(tmp);
178-
strList.add(node.beginToken.toString());
179-
strList.add(node.toSource());
180-
strList.add('type:' + node.fields.type.toString());
181-
strList.add('fields:' + node.fields.variables.join(',').toString());
182-
Debug.blue('成员变量[$folderName]: ${strList.join(', ')}');
176+
node.visitChildren(this);
183177
String fieldName = node.fields.variables.join(',');
184-
if(fieldName.contains("=")){
185-
fieldName = fieldName.split("=")[0].trim();
178+
if (fieldName.contains('=')) {
179+
fieldName = fieldName.split('=')[0].trim();
186180
}
187-
PropertyInfo? item = propertyList.firstWhereOrNull((element) => element.name == fieldName);
188-
if (item == null) {
189-
item = PropertyInfo();
190-
fieldMap[fieldName] = item;
181+
if (fieldName.startsWith('_')) {
182+
return;
191183
}
184+
PropertyInfo? item =
185+
propertyList.firstWhereOrNull((PropertyInfo element) => element.name == fieldName);
186+
item ??= PropertyInfo()..name = fieldName;
187+
fieldMap[fieldName] = item;
192188
item.type = node.fields.type.toString();
193-
if (node.beginToken.toString().startsWith('///')) {
189+
if (node.documentationComment != null) {
190+
item.introduction = removeDocumentationComment(
191+
node.documentationComment!.tokens.join('\n'),
192+
);
193+
} else if (node.beginToken.toString().startsWith('///')) {
194194
item.introduction = removeDocumentationComment(node.beginToken.toString());
195195
}
196+
Debug.blue('成员变量[$folderName]: $fieldName | ${item.type}');
196197
}
197198

198199
@override
@@ -214,61 +215,70 @@ class ComponentAstVisitor extends RecursiveAstVisitor<void> {
214215

215216
@override
216217
void visitClassDeclaration(ClassDeclaration node) {
218+
final bool isTarget = nameList!.contains(node.name.toString());
219+
final String? previousTargetClassName = _currentTargetClassName;
220+
final bool previousClassIsAbstract = _currentClassIsAbstract;
221+
if (isTarget) {
222+
_currentTargetClassName = node.name.toString();
223+
_currentClassIsAbstract = node.abstractKeyword != null;
224+
}
217225
node.visitChildren(this);
218-
if (nameList!.contains(node.name.toString())) {
226+
if (isTarget) {
219227
componentInfo ??= ComponentInfo();
220-
List<String> strList = [];
221-
strList.add(node.name.toString());
222-
strList.add(node.beginToken.toString());
223-
if (node.name.toString() == 'TECheckBox') {
224-
// strList.add(node.getField('checked')!.parent!.parent!.beginToken.toString());
225-
strList.add(node.getProperty('checked')!.parent!.parent!.beginToken.toString());
226-
}
227-
Debug.red('类[$folderName]: ${strList.join(', ')}');
228228
componentInfo!.name = node.name.toString();
229229
if (node.documentationComment != null) {
230230
componentInfo!.introduction = removeDocumentationComment(
231-
node.documentationComment!.tokens.join("\n"));
231+
node.documentationComment!.tokens.join('\n'));
232+
}
233+
_mergeExtraFieldsIntoPropertyList();
234+
for (final PropertyInfo item in propertyList) {
235+
_fillPropertyFromFieldMap(item);
236+
if (item.type.isEmpty) {
237+
item.type = '-';
238+
}
232239
}
240+
propertyList.sort(
241+
(PropertyInfo a, PropertyInfo b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
233242
if (onParsedComponentInfoInfo != null) {
234-
// 按照属性名称的首字母排序
235-
propertyList.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
236-
237243
onParsedComponentInfoInfo!(ParsedComponentInfoInfo()
238244
..componentInfo = componentInfo
239245
..propertyList = propertyList
240246
..fieldMap = fieldMap);
241247
}
248+
_resetClassState();
242249
}
243-
componentInfo = null;
244-
propertyList = [];
250+
_currentTargetClassName = previousTargetClassName;
251+
_currentClassIsAbstract = previousClassIsAbstract;
245252
}
246253

247254
@override
248255
void visitMethodDeclaration(MethodDeclaration node) {
256+
if (!_isInTargetClass) {
257+
super.visitMethodDeclaration(node);
258+
return;
259+
}
249260
super.visitMethodDeclaration(node);
250-
if(node.isStatic && !node.name.toString().startsWith("_")){
251-
StaticMethodInfo staticMethodInfo = new StaticMethodInfo();
252-
staticMethodInfo.name = node.name.toString();
253-
staticMethodInfo.introduction = removeDocumentationComment(node.documentationComment?.tokens.join("\n") ?? "");
254-
staticMethodInfo.returnType = node.returnType?.type.toString();
255-
node.parameters?.parameters.forEach((element) {
256-
PropertyInfo info = PropertyInfo();
257-
info.name = element.name?.lexeme.toString() ?? "Null";
258-
if (element is SimpleFormalParameter) {
259-
info.type = element.type.toString();
260-
} else if(element is DefaultFormalParameter && element.parameter is SimpleFormalParameter){
261-
info.type = (element.parameter as SimpleFormalParameter).type.toString();
262-
}
261+
final String methodName = node.name.toString();
262+
// 私有方法不收录
263+
if (methodName.startsWith('_')) {
264+
return;
265+
}
263266

264-
info.isRequired =
265-
element.isRequiredNamed || element.isRequiredPositional;
266-
info.isNamed = element.isNamed;
267+
StaticMethodInfo methodInfo = StaticMethodInfo();
268+
methodInfo.name = methodName;
269+
methodInfo.introduction = removeDocumentationComment(
270+
node.documentationComment?.tokens.join('\n') ?? '');
271+
methodInfo.returnType = node.returnType?.toSource();
272+
node.parameters?.parameters.forEach((FormalParameter element) {
273+
methodInfo.params.add(_buildPropertyFromParameter(element));
274+
});
267275

268-
staticMethodInfo.params.add(info);
269-
});
270-
componentInfo ??= ComponentInfo();
271-
componentInfo!.staticMethodList.add(staticMethodInfo);
276+
componentInfo ??= ComponentInfo();
277+
if (node.isStatic) {
278+
componentInfo!.staticMethodList.add(methodInfo);
279+
} else if (_currentClassIsAbstract) {
280+
// abstract class 的实例方法(含 abstract 方法和带默认实现的可覆写方法)
281+
componentInfo!.instanceMethodList.add(methodInfo);
272282
}
273283
}
274284
}
@@ -341,16 +351,16 @@ class ComponentVisitor extends RecursiveElementVisitor<void> {
341351
}
342352

343353
String getDefaultValue(ParameterElement param) {
344-
bool paramIsString = param.type.getDisplayString(withNullability: false) == 'String';
354+
final bool paramIsString =
355+
param.type.getDisplayString(withNullability: false) == 'String';
345356
String defaultValue = param.defaultValueCode!;
346-
// if (defaultValue == '\'\'') {
347-
// defaultValue = '""';
348-
// }
357+
if (defaultValue == param.name || defaultValue == 'this.${param.name}') {
358+
return '-';
359+
}
349360
if (defaultValue.startsWith("'")) {
350361
defaultValue = defaultValue.substring(1, defaultValue.length - 1);
351362
}
352-
// print('paramIsString=$paramIsString, defaultValue=$defaultValue');
353-
return paramIsString ? defaultValue : '$defaultValue';
363+
return paramIsString ? defaultValue : defaultValue;
354364
}
355365

356366
String? getDescription(String name, List<FieldElement> fields) {

lib/model.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ class ComponentInfo {
6262
// 其他构造方法信息
6363
List<StaticMethodInfo> constructorMethodList = [];
6464

65+
// 实例方法信息(用于 abstract class 的接口方法文档)
66+
List<StaticMethodInfo> instanceMethodList = [];
67+
6568
Map<String, dynamic> toJson() {
6669
return <String, dynamic>{
6770
'name': name,

0 commit comments

Comments
 (0)