Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Tools Regression

on:
pull_request:
branches: [main, develop]
push:
branches: [main]
workflow_dispatch:

jobs:
test:
name: test (${{ matrix.label }})
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- label: '3.32.0'
flutter-version: '3.32.0'
- label: latest
flutter-version: ''
env:
TDESIGN_COMPONENT_ROOT: ${{ github.workspace }}/tdesign-flutter/tdesign-component
steps:
- uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: ${{ matrix.flutter-version }}
cache: true
- run: flutter pub get
- run: dart analyze --fatal-infos
- run: git clone --depth 1 --branch develop https://github.com/Tencent/tdesign-flutter.git tdesign-flutter
- name: Run complete tools suite including CLI roundtrip
run: DART_SDK="$FLUTTER_ROOT/bin/cache/dart-sdk" dart test --reporter expanded
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
3. **参数说明写在「方法注释」或「字段注释」**
- 顶层函数 / 静态方法 / 工厂 / 构造:推荐在该方法的 `///` 里写 `[paramName] 说明`。
- 构造参数:也可写在同名字段的 `///` 上。
- 参数声明前的行内 `///` 也会读取,包含带默认值的命名参数。
- 无注释时表格「说明」列为 `-`,属预期,应在源码补全,**不要**在工具里打补丁。

4. **不要把静态方法参数表只写在类简介里**
Expand All @@ -31,12 +32,19 @@

7. **顶层函数需在 `--name` 中显式列出**
工具会为任意公开顶层函数生成独立 API 区块,包含返回类型、参数类型、默认值和 dartdoc;私有函数、getter、setter 以及未登记函数不会被收录。
维护组件清单时加 `--strict-names`,只输出 `--name` 中登记的声明,避免同文件辅助枚举或 typedef 混入其他组件文档。文件夹扫描会递归读取 `.dart` 源文件。

公开普通实例方法、字段访问器、命名/工厂/无参构造与命名 extension 均会展示;Flutter 构建钩子、未自行补充文档的标准继承 override、`@internal` 和 `@visibleForTesting` 成员不收录。默认值保留声明值,Theme / Token 回退由源码注释说明;构造及方法参数另列是否必填。泛型在 Markdown 表格中转义,保留网页与 Demo 中的完整类型。类/extension 与可调用声明另外输出源码签名,包含泛型约束、位置/命名参数、默认值及 const/factory 标记;每个构造或方法独立列出完整参数。自定义 copyWith/lerp/Token 查询运算符会展示,即使源码尚未补注释也不会静默遗漏。参数说明仅从源码注释、构造/复制字段或 AST 可证明的透传获取;Widget key 的兜底说明仅适用于 Key/Key?,不会套用到 Token 的 String/Object 键。

8. **不对个别组件做特殊兼容**
工具只保留单一 AST / dartdoc 解析路线;注释位置或格式不对,应在 `tdesign-component` 修正。

9. **CI 抽测清单见 `.github/config/tdesign_api.yaml`**
本地 `validate` 与 CI 使用同一配置;`ERROR` 需为 0,`WARN` 多为 enum 成员缺注释等源码问题。
解析(`component_rule.dart`)、框架成员筛选(`public_member_policy.dart`)、签名格式化(`api_signature.dart`)与 Markdown 渲染(`api_markdown.dart`)分别维护。渲染不修改解析模型;构造/工厂/external 按 AST 标记处理。仅过滤能由所属基类识别的框架钩子,业务 `build` 和普通接口 `override` 不会因名字或缺注释被隐藏。函数类型参数保留完整类型;无法从源码推断的父类/字段类型显示 `-`,不伪造 `dynamic`。代码单元格保留字符串空白并转义 HTML 实体。

默认构造参数使用五级子标题,`validate` 同时兼容既有的四级参数标题。CI 在 Flutter 3.32.0 与 latest 下运行完整工具单测,包含真实 CLI generate → validate 正例及删参数反例。

9. **生成与验收共用消费仓库 `tool/components.json`**
`validate --component-root ...` 默认验收完整 manifest,支持 file/folder、声明及顶层函数。仍可用 `--config` 指定原有 YAML/JSON 抽测清单;`ERROR` 需为 0,`WARN` 多为 enum 成员缺注释等源码问题。

## 快速开始

Expand Down Expand Up @@ -70,12 +78,12 @@ cd $TOOLS

dart run bin/main.dart validate \
--component-root $COMPONENT \
--config .github/config/tdesign_api.yaml
--config $COMPONENT/tool/components.json

# 仅测部分组件
dart run bin/main.dart validate \
--component-root $COMPONENT \
--config .github/config/tdesign_api.yaml \
--config $COMPONENT/tool/components.json \
--components button,popup
```

Expand Down Expand Up @@ -183,7 +191,7 @@ dart run bin/main.dart generate \
```bash
dart run bin/main.dart validate \
--component-root <tdesign-component> \
--config .github/config/tdesign_api.yaml \
--config $COMPONENT/tool/components.json \
[--components button,popup]
```

Expand Down
15 changes: 11 additions & 4 deletions bin/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ class CreateCommand extends Command {
help: '[可选]生成的组件示例文件夹名称,默认文件夹名称是第一项name的下划线表示',
);
argParser.addOption('output', help: '文件输出路径');
argParser.addFlag(
'strict-names',
defaultsTo: false,
help: '只输出 --name 显式声明的公开类型和函数',
);
argParser.addFlag('only-api', defaultsTo: false, help: '是否只生成api文件');
argParser.addFlag(
'get-comments',
Expand All @@ -50,6 +55,7 @@ class CreateCommand extends Command {
bool onlyApi = argResults!['only-api'] ?? false;
commandInfo.isOnlyApi = onlyApi;
commandInfo.widgetNames = argResults!['name'].toString();
commandInfo.strictNames = argResults!['strict-names'] ?? false;
commandInfo.isGetComments = argResults!['get-comments'] ?? false;
return commandInfo;
}
Expand Down Expand Up @@ -101,12 +107,11 @@ class ValidateCommand extends Command {
);
argParser.addOption(
'config',
help: '审计清单 YAML/JSON 路径',
defaultsTo: '.github/config/tdesign_api.yaml',
help: '审计清单 YAML/JSON 路径;默认读取 component-root/tool/components.json',
);
argParser.addMultiOption(
'components',
help: '仅检测指定组件,如 button,picker(默认 5 组件全量)',
help: '仅检测指定组件,如 button,picker(默认清单全量)',
);
argParser.addFlag('verbose', abbr: 'v', help: '打印 analyzer 解析过程');
}
Expand All @@ -124,7 +129,9 @@ class ValidateCommand extends Command {
return;
}

final String configRaw = argResults!['config'] as String;
final String configRaw =
argResults!['config'] as String? ??
defaultAuditConfigPath(componentRoot: componentRoot);
final String configPath =
p.isAbsolute(configRaw)
? p.normalize(configRaw)
Expand Down
71 changes: 54 additions & 17 deletions lib/api_completeness.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class ComponentAuditConfig {
required this.classNames,
required this.sourceFolder,
required this.folderName,
this.sourceIsFile = false,
});

final String componentKey;
Expand All @@ -36,6 +37,7 @@ class ComponentAuditConfig {
/// 相对 component 根目录,如 lib/src/components/picker
final String sourceFolder;
final String folderName;
final bool sourceIsFile;
}

/// 从 YAML/JSON 配置文件加载审计清单(components 节点)
Expand All @@ -47,6 +49,30 @@ Future<List<ComponentAuditConfig>> loadAuditConfigsFromFile(String path) async {
final String content = await file.readAsString();
if (path.endsWith('.json')) {
final dynamic decoded = jsonDecode(content);
if (decoded is Map<String, dynamic> && decoded['components'] is List) {
if (decoded['schemaVersion'] != 1)
throw ArgumentError('Unsupported manifest schema: $path');
return (decoded['components'] as List).map((dynamic item) {
final component = Map<String, dynamic>.from(item as Map);
final source = Map<String, dynamic>.from(component['source'] as Map);
final api = Map<String, dynamic>.from(component['api'] as Map);
final names = <String>[
...(api['names'] as List).cast<String>(),
...?(api['functions'] as List?)?.cast<String>(),
];
if (names.isEmpty || !const {'file', 'folder'}.contains(source['type']))
throw ArgumentError(
'Invalid component manifest entry: ${component['slug']}',
);
return ComponentAuditConfig(
componentKey: component['slug'] as String,
classNames: names,
sourceFolder: source['path'] as String,
sourceIsFile: source['type'] == 'file',
folderName: component['slug'] as String,
);
}).toList();
}
if (decoded is Map<String, dynamic> && decoded['components'] is Map) {
return _configsFromMap(
Map<String, dynamic>.from(decoded['components'] as Map),
Expand Down Expand Up @@ -153,8 +179,10 @@ List<ComponentAuditConfig> _configsFromYaml(String yaml) {
);
}

/// 默认审计配置路径(相对 tools 仓库根目录)
String defaultAuditConfigPath() => '.github/config/tdesign_api.yaml';
/// Reuse the consuming component's manifest instead of a separate audit list.
String defaultAuditConfigPath({
String componentRoot = '../tdesign-flutter/tdesign-component',
}) => p.join(componentRoot, 'tool', 'components.json');

/// 从 Markdown API 文档解析 {类名: section 正文}
Map<String, String> parseMarkdownSections(String markdown) {
Expand All @@ -168,14 +196,28 @@ Map<String, String> parseMarkdownSections(String markdown) {
return sections;
}

/// Accept current child headings and the former sibling parameter heading.
/// Headings inside declaration code fences do not delimit the section.
String _defaultConstructorBlock(String section) {
final lines = section.split('\n');
final start = lines.indexWhere((line) => line.trim() == '#### 默认构造方法');
if (start < 0) return '';
final result = <String>[];
var inCode = false;
for (final line in lines.skip(start + 1)) {
if (line.trimLeft().startsWith('```')) inCode = !inCode;
if (!inCode) {
final heading = RegExp(r'^(#{1,4})\s+(.+)$').firstMatch(line);
if (heading != null && line.trim() != '#### 参数') break;
}
result.add(line);
}
return result.join('\n');
}

/// 读取「默认构造方法」表格中的参数名(不含公开属性 / 静态成员表)
Set<String> markdownDefaultCtorParamNames(String section) {
const String header = '#### 默认构造方法';
if (!section.contains(header)) {
return <String>{};
}
final String block =
section.split(header).skip(1).first.split(RegExp(r'\n#### ')).first;
final String block = _defaultConstructorBlock(section);
final Set<String> names = <String>{};
for (final String line in block.split('\n')) {
if (!line.startsWith('|') || line.startsWith('| ---')) {
Expand All @@ -200,12 +242,7 @@ Set<String> markdownDefaultCtorParamNames(String section) {

/// 构造表中类型列为 `-` 的参数名
List<String> markdownCtorParamsWithEmptyType(String section) {
const String header = '#### 默认构造方法';
if (!section.contains(header)) {
return <String>[];
}
final String block =
section.split(header).skip(1).first.split(RegExp(r'\n#### ')).first;
final String block = _defaultConstructorBlock(section);
final List<String> bad = <String>[];
for (final String line in block.split('\n')) {
if (!line.startsWith('|') || line.startsWith('| ---')) {
Expand Down Expand Up @@ -449,7 +486,7 @@ Future<List<CompletenessIssue>> auditComponent({
: '$root${Platform.pathSeparator}';

final List<ParsedComponentInfoInfo> parsed = await SmartCreator(
isFileMode: false,
isFileMode: config.sourceIsFile,
onlyApi: true,
nameList: config.classNames,
basePath: basePath,
Expand Down Expand Up @@ -630,13 +667,13 @@ Future<int> runCompletenessAudit({
final List<ComponentAuditConfig> auditConfigs =
configs ??
await loadAuditConfigsFromFile(
p.join(Directory.current.path, defaultAuditConfigPath()),
defaultAuditConfigPath(componentRoot: componentRoot),
);
int errorCount = 0;
int warnCount = 0;

stdout.writeln('=' * 60);
stdout.writeln('API 文档完备性检测(analyzer AST,5 组件)');
stdout.writeln('API 文档完备性检测(analyzer AST,${auditConfigs.length} 组件)');
stdout.writeln('=' * 60);

for (final ComponentAuditConfig config in auditConfigs) {
Expand Down
Loading
Loading