嵌套对象处理
All checks were successful
CodeChecker 变更检测 / code-check (push) Successful in 27s

This commit is contained in:
2026-06-09 17:42:57 +08:00
parent 8b60a84b56
commit a51ab9098a
8 changed files with 105 additions and 18 deletions

View File

@@ -37,10 +37,12 @@ Push 触发 CI 后,按变更类的后缀(`Dto` / `Vo` / `Entity` / `Model`
| 类类型 | request | response | 类转换 |
|--------|:-------:|:--------:|:------:|
| Dto | ✅ | | ✅ |
| Vo | | ✅ | ✅ |
| Dto | ✅ | | ✅ |
| Vo | | ✅ | ✅ |
| Entity / Model | ❌ | ❌ | ✅ |
Dto/Vo 均固定展示 request、response 两栏;无匹配接口时显示「无」。类转换栏仅在 `dto_entity_conversion.enabled: true` 时展示,关闭时不出现该小节。实际影响由接口索引 + 嵌套关系传播,不假定 Dto 仅 request、Vo 仅 response。
## 模版文件
| 文件 | 场景 |

View File

@@ -1,7 +1,8 @@
# Dto 类变更通知模版
**识别规则**:类名以 `Dto` 结尾。
**影响范围**request + 类转换。
**影响范围**request + response + 类转换(无匹配时对应栏显示「无」)。
**嵌套标识**:被其他 Dto/Vo 嵌套时在「变更对象」行追加 `(嵌套对象)`;若同时直接作接口入参/返回值根类型,再追加 `(顶层对象)`。纯顶层不标注。
---
@@ -39,6 +40,20 @@
---
## 示例(嵌套对象)
```
> **变更对象: <font color="info">UserSelfDto</font><font color="comment">嵌套对象</font>**
```
若该类同时直接出现在某接口 `@RequestBody` 或返回值类型中:
```
> **变更对象: <font color="info">SomeDto</font><font color="comment">说明</font><font color="comment">嵌套对象</font><font color="comment">顶层对象</font>**
```
---
## 示例(类删除)
```

View File

@@ -1,7 +1,8 @@
# Vo 类变更通知模版
**识别规则**:类名以 `Vo``VO` 结尾。
**影响范围**response + 类转换
**影响范围**request + response + 类转换(无匹配时对应栏显示「无」)。
**嵌套标识**:规则同 Dto——仅嵌套时标注 `(嵌套对象)`,嵌套且直接作接口根类型时追加 `(顶层对象)`
---

View File

@@ -81,6 +81,12 @@ public class DtoNestIndex {
return names;
}
/** 是否被其他 Dto/Vo 嵌套引用(存在至少一个祖先容器) */
public boolean hasAncestors(String className) {
Set<String> ancestors = ancestorsOf.get(className);
return ancestors != null && !ancestors.isEmpty();
}
/** 嵌套类型的 @RequestBody 根 Dto 祖先(仅 Dto 后缀) */
public Set<String> findRequestBodyRoots(String className) {
Set<String> roots = new LinkedHashSet<>();

View File

@@ -35,6 +35,8 @@ public class ImpactAnalyzer {
matchEndpoints(report, endpointIndex, matchNames);
}
report.setObjectRoleLabels(NestedObjectRoleResolver.resolve(report, nestIndex, endpointIndex));
if (!config.isDtoEntityConversionEnabled()) {
return;
}

View File

@@ -0,0 +1,54 @@
package com.codechecker.analyzer;
import com.codechecker.model.ApiEndpoint;
import com.codechecker.model.ClassChangeReport;
import com.codechecker.model.ClassType;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 判定 Dto/Vo 在类变更通知中的对象角色标签(方案 B嵌套 + 可选顶层)。
* <p>
* 仅当存在嵌套祖先时标注;纯顶层不标注;既嵌套又直接作接口根类型时同时标注。
*/
public final class NestedObjectRoleResolver {
private NestedObjectRoleResolver() {
}
public static List<String> resolve(ClassChangeReport report, DtoNestIndex nestIndex,
Map<String, ApiEndpoint> endpointIndex) {
if (report.getClassType() != ClassType.DTO && report.getClassType() != ClassType.VO) {
return List.of();
}
if (nestIndex == null) {
return List.of();
}
String className = report.getClassName();
if (!nestIndex.hasAncestors(className)) {
return List.of();
}
List<String> labels = new ArrayList<>();
labels.add("嵌套对象");
if (isDirectEndpointType(className, endpointIndex)) {
labels.add("顶层对象");
}
return List.copyOf(labels);
}
/** 是否直接出现在接口入参或返回值类型(非仅经祖先传播) */
private static boolean isDirectEndpointType(String className, Map<String, ApiEndpoint> endpointIndex) {
if (className == null || className.isBlank() || endpointIndex == null) {
return false;
}
for (ApiEndpoint endpoint : endpointIndex.values()) {
if (endpoint.getParamTypes().contains(className)
|| endpoint.getReturnTypes().contains(className)) {
return true;
}
}
return false;
}
}

View File

@@ -18,6 +18,7 @@ public class ClassChangeReport {
private final List<String> conversionEntities = new ArrayList<>();
private final List<ApiEndpoint> frontendImpactEndpoints = new ArrayList<>();
private final boolean conversionCheckEnabled;
private List<String> objectRoleLabels = List.of();
public ClassChangeReport(String className, String oldClassName, ClassType classType,
ClassChangeKind changeKind, String sourceFile,
@@ -98,6 +99,15 @@ public class ClassChangeReport {
return conversionCheckEnabled;
}
/** 对象角色标签(如「嵌套对象」「顶层对象」),仅 Dto/Vo 且存在嵌套时非空 */
public List<String> getObjectRoleLabels() {
return objectRoleLabels;
}
public void setObjectRoleLabels(List<String> labels) {
this.objectRoleLabels = labels == null || labels.isEmpty() ? List.of() : List.copyOf(labels);
}
/** 追加一条字段变更 */
public void addFieldChange(FieldChange change) {
fieldChanges.add(change);

View File

@@ -82,14 +82,17 @@ public class WeComNotifier {
return truncate(sb.toString());
}
/** 变更对象行:类名(绿)+ 可选中文说明(灰,整行加粗) */
/** 变更对象行:类名(绿)+ 可选中文说明 + 嵌套角色标签(灰,整行加粗) */
private String formatChangeTarget(ClassChangeReport report) {
String name = colorInfo(safe(report.getClassName()));
StringBuilder line = new StringBuilder(colorInfo(safe(report.getClassName())));
String description = report.getClassDescription();
if (description == null || description.isBlank()) {
return name;
if (description != null && !description.isBlank()) {
line.append("").append(colorComment(description)).append("");
}
return name + "" + colorComment(description) + "";
for (String role : report.getObjectRoleLabels()) {
line.append("").append(colorComment(role)).append("");
}
return line.toString();
}
/** 头部元信息,每项一行引用(加粗) */
@@ -139,14 +142,12 @@ public class WeComNotifier {
appendImpactByType(sb, report);
}
/** Dto/Vo/Entity/Model 各展示不同的 request/response/转换段落 */
/** Dto/Vo 均展示 request + response二者可能交叉Entity/Model 仅类转换 */
private void appendImpactByType(StringBuilder sb, ClassChangeReport report) {
switch (report.getClassType()) {
case DTO:
appendSectionIfNeeded(sb, report, true, false, true);
break;
case VO:
appendSectionIfNeeded(sb, report, false, true, true);
appendSectionIfNeeded(sb, report, true, true, true);
break;
case ENTITY:
case MODEL:
@@ -170,7 +171,7 @@ public class WeComNotifier {
appendEndpointList(sb, report.getFrontendImpactEndpoints());
sb.append("\n");
}
if (showConversion) {
if (showConversion && report.isConversionCheckEnabled()) {
sb.append("### 类转换影响").append("\n");
appendConversionList(sb, report);
}
@@ -178,10 +179,6 @@ public class WeComNotifier {
/** 渲染关联 Entity每项一行 */
private void appendConversionList(StringBuilder sb, ClassChangeReport report) {
if (!report.isConversionCheckEnabled()) {
sb.append(quoteLine(colorComment("未开启检测"))).append("\n");
return;
}
if (report.getConversionEntities().isEmpty()) {
sb.append(quoteLine(colorComment(""))).append("\n");
return;