This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
package com.aicheck.parser;
|
||||
|
||||
import com.github.javaparser.StaticJavaParser;
|
||||
import com.github.javaparser.ast.CompilationUnit;
|
||||
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
|
||||
import com.github.javaparser.ast.body.TypeDeclaration;
|
||||
|
||||
/**
|
||||
* 从 Java 源文件路径或 AST 解析类名(简单名 / 全限定名)。
|
||||
*/
|
||||
public class ClassDeclParser {
|
||||
|
||||
/**
|
||||
* 从源码 AST 提取主类名;解析失败或未找到时回退为路径推导的类名。
|
||||
*/
|
||||
public String resolveClassName(String source, String fallbackFromPath) {
|
||||
if (source == null || source.isBlank()) {
|
||||
return fallbackFromPath;
|
||||
}
|
||||
try {
|
||||
CompilationUnit cu = StaticJavaParser.parse(source);
|
||||
for (TypeDeclaration<?> type : cu.getTypes()) {
|
||||
if (type instanceof ClassOrInterfaceDeclaration) {
|
||||
return type.getNameAsString();
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 回退路径类名
|
||||
}
|
||||
return fallbackFromPath;
|
||||
}
|
||||
|
||||
/** 从 .java 路径提取文件名(无扩展名)作为类名 */
|
||||
public static String classNameFromPath(String path) {
|
||||
String fileName = path.substring(path.lastIndexOf('/') + 1);
|
||||
if (!fileName.endsWith(".java")) {
|
||||
return fileName;
|
||||
}
|
||||
return fileName.substring(0, fileName.length() - 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全限定类名:package + 类名;源码无 package 时从文件路径推断。
|
||||
*/
|
||||
public String resolveQualifiedClassName(String source, String relativePath, String fallbackClassName) {
|
||||
String simpleName = resolveClassName(source, fallbackClassName);
|
||||
if (source != null && !source.isBlank()) {
|
||||
try {
|
||||
CompilationUnit cu = StaticJavaParser.parse(source);
|
||||
String packageName = cu.getPackageDeclaration()
|
||||
.map(p -> p.getNameAsString())
|
||||
.orElse("");
|
||||
if (!packageName.isBlank()) {
|
||||
return packageName + "." + simpleName;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 回退路径推断
|
||||
}
|
||||
}
|
||||
return inferQualifiedFromPath(relativePath, simpleName);
|
||||
}
|
||||
|
||||
/** 从 src/main/java/ 后的路径推断 package.className */
|
||||
public static String inferQualifiedFromPath(String relativePath, String className) {
|
||||
if (relativePath == null || relativePath.isBlank()) {
|
||||
return className;
|
||||
}
|
||||
String normalized = relativePath.replace('\\', '/');
|
||||
String marker = "src/main/java/";
|
||||
int idx = normalized.indexOf(marker);
|
||||
if (idx < 0) {
|
||||
return className;
|
||||
}
|
||||
String subPath = normalized.substring(idx + marker.length());
|
||||
int lastSlash = subPath.lastIndexOf('/');
|
||||
if (lastSlash <= 0) {
|
||||
return className;
|
||||
}
|
||||
String packageName = subPath.substring(0, lastSlash).replace('/', '.');
|
||||
return packageName + "." + className;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.aicheck.parser;
|
||||
|
||||
import com.aicheck.model.FieldInfo;
|
||||
import com.github.javaparser.StaticJavaParser;
|
||||
import com.github.javaparser.ast.CompilationUnit;
|
||||
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
|
||||
import com.github.javaparser.ast.body.FieldDeclaration;
|
||||
import com.github.javaparser.ast.body.TypeDeclaration;
|
||||
import com.github.javaparser.ast.body.VariableDeclarator;
|
||||
import com.github.javaparser.ast.comments.JavadocComment;
|
||||
import com.github.javaparser.ast.expr.AnnotationExpr;
|
||||
import com.github.javaparser.ast.expr.Expression;
|
||||
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
|
||||
import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 解析模型类字段:名称、类型、业务说明(注解或 Javadoc)。
|
||||
*/
|
||||
public class ClassFieldParser {
|
||||
|
||||
/** 解析指定类的实例字段列表 */
|
||||
public List<FieldInfo> parseFields(String source, String expectedClassName) {
|
||||
if (source == null || source.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
CompilationUnit cu = StaticJavaParser.parse(source);
|
||||
ClassOrInterfaceDeclaration classDecl = findClass(cu, expectedClassName);
|
||||
if (classDecl == null) {
|
||||
return List.of();
|
||||
}
|
||||
return parseClassFields(classDecl);
|
||||
}
|
||||
|
||||
/** 按类名查找类声明,找不到则取第一个类 */
|
||||
private ClassOrInterfaceDeclaration findClass(CompilationUnit cu, String expectedClassName) {
|
||||
if (expectedClassName != null && !expectedClassName.isBlank()) {
|
||||
for (TypeDeclaration<?> type : cu.getTypes()) {
|
||||
if (type instanceof ClassOrInterfaceDeclaration) {
|
||||
ClassOrInterfaceDeclaration classDecl = (ClassOrInterfaceDeclaration) type;
|
||||
if (classDecl.getNameAsString().equals(expectedClassName)) {
|
||||
return classDecl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (TypeDeclaration<?> type : cu.getTypes()) {
|
||||
if (type instanceof ClassOrInterfaceDeclaration) {
|
||||
return (ClassOrInterfaceDeclaration) type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 提取非 static final 字段,跳过常量 */
|
||||
private List<FieldInfo> parseClassFields(ClassOrInterfaceDeclaration classDecl) {
|
||||
Map<String, FieldInfo> fields = new LinkedHashMap<>();
|
||||
for (FieldDeclaration fieldDecl : classDecl.getFields()) {
|
||||
if (fieldDecl.isStatic() && fieldDecl.isFinal()) {
|
||||
continue;
|
||||
}
|
||||
String type = TypeNameUtils.typeToString(fieldDecl.getElementType());
|
||||
String description = extractFieldLabel(fieldDecl);
|
||||
for (VariableDeclarator variable : fieldDecl.getVariables()) {
|
||||
fields.put(variable.getNameAsString(), new FieldInfo(variable.getNameAsString(), type, description));
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(fields.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段说明:@Schema(description) > @ApiModelProperty > Javadoc,均无则空串。
|
||||
*/
|
||||
String extractFieldLabel(FieldDeclaration fieldDecl) {
|
||||
for (AnnotationExpr annotation : fieldDecl.getAnnotations()) {
|
||||
String annName = annotation.getNameAsString();
|
||||
if ("Schema".equals(annName)) {
|
||||
String description = readAnnotationStringValue(annotation, "description");
|
||||
if (!description.isEmpty()) {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
if ("ApiModelProperty".equals(annName)) {
|
||||
String value = readAnnotationStringValue(annotation, "value");
|
||||
if (!value.isEmpty()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return extractJavadoc(fieldDecl);
|
||||
}
|
||||
|
||||
/** 读取注解中的字符串属性值 */
|
||||
private String readAnnotationStringValue(AnnotationExpr annotation, String attributeName) {
|
||||
if (annotation.isNormalAnnotationExpr()) {
|
||||
NormalAnnotationExpr normal = annotation.asNormalAnnotationExpr();
|
||||
for (var pair : normal.getPairs()) {
|
||||
if (pair.getNameAsString().equals(attributeName)) {
|
||||
return literalString(pair.getValue());
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
if (annotation.isSingleMemberAnnotationExpr()) {
|
||||
SingleMemberAnnotationExpr single = annotation.asSingleMemberAnnotationExpr();
|
||||
if ("value".equals(attributeName) || "description".equals(attributeName)) {
|
||||
return literalString(single.getMemberValue());
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** 提取字符串字面量值 */
|
||||
private String literalString(Expression expression) {
|
||||
if (expression.isStringLiteralExpr()) {
|
||||
return expression.asStringLiteralExpr().getValue().trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** 从字段 Javadoc 提取首段描述 */
|
||||
private String extractJavadoc(FieldDeclaration fieldDecl) {
|
||||
Optional<JavadocComment> javadoc = fieldDecl.getJavadocComment();
|
||||
if (javadoc.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
String text = javadoc.get().parse().getDescription().toText();
|
||||
return text == null ? "" : text.trim().replaceAll("\\s+", " ");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.aicheck.parser;
|
||||
|
||||
import com.github.javaparser.StaticJavaParser;
|
||||
import com.github.javaparser.ast.CompilationUnit;
|
||||
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
|
||||
import com.github.javaparser.ast.body.MethodDeclaration;
|
||||
import com.github.javaparser.ast.body.TypeDeclaration;
|
||||
import com.github.javaparser.ast.expr.MethodCallExpr;
|
||||
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 扫描 Dto→Entity 转换关系:convert 方法返回值、BeanUtils.copyProperties 调用。
|
||||
*/
|
||||
public class ConversionParser {
|
||||
|
||||
/** 在类内查找 convert 方法,收集返回 Entity 的类型名 */
|
||||
public Set<String> findConvertTargetsInClass(String source, String className) {
|
||||
Set<String> entities = new LinkedHashSet<>();
|
||||
if (source == null || source.isBlank()) {
|
||||
return entities;
|
||||
}
|
||||
CompilationUnit cu = StaticJavaParser.parse(source);
|
||||
for (TypeDeclaration<?> type : cu.getTypes()) {
|
||||
if (type instanceof ClassOrInterfaceDeclaration) {
|
||||
ClassOrInterfaceDeclaration classDecl = (ClassOrInterfaceDeclaration) type;
|
||||
if (!classDecl.getNameAsString().equals(className)) {
|
||||
continue;
|
||||
}
|
||||
for (MethodDeclaration method : classDecl.getMethods()) {
|
||||
if (!"convert".equals(method.getNameAsString())) {
|
||||
continue;
|
||||
}
|
||||
String returnType = TypeNameUtils.simpleName(TypeNameUtils.typeToString(method.getType()));
|
||||
if (returnType.endsWith("Entity")) {
|
||||
entities.add(returnType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
/** 递归扫描目录,查找 BeanUtils.copyProperties(sourceClass, *Entity) */
|
||||
public Set<String> findBeanUtilsTargets(Path rootDir, String sourceClassName) throws IOException {
|
||||
Set<String> entities = new LinkedHashSet<>();
|
||||
if (!Files.exists(rootDir)) {
|
||||
return entities;
|
||||
}
|
||||
try (Stream<Path> paths = Files.walk(rootDir)) {
|
||||
paths.filter(path -> path.toString().endsWith(".java")).forEach(path -> {
|
||||
try {
|
||||
String source = Files.readString(path, StandardCharsets.UTF_8);
|
||||
entities.addAll(scanBeanUtilsInSource(source, sourceClassName));
|
||||
} catch (IOException ignored) {
|
||||
// 跳过
|
||||
}
|
||||
});
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
/** 在单文件源码中扫描 BeanUtils.copyProperties 调用 */
|
||||
private Set<String> scanBeanUtilsInSource(String source, String sourceClassName) {
|
||||
Set<String> entities = new LinkedHashSet<>();
|
||||
CompilationUnit cu = StaticJavaParser.parse(source);
|
||||
cu.accept(new VoidVisitorAdapter<Void>() {
|
||||
@Override
|
||||
public void visit(MethodCallExpr call, Void arg) {
|
||||
super.visit(call, arg);
|
||||
if (!call.getNameAsString().equals("copyProperties")) {
|
||||
return;
|
||||
}
|
||||
if (call.getScope().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String scope = call.getScope().get().toString();
|
||||
if (!scope.endsWith("BeanUtils")) {
|
||||
return;
|
||||
}
|
||||
if (call.getArguments().size() < 2) {
|
||||
return;
|
||||
}
|
||||
String firstArg = TypeNameUtils.simpleName(call.getArguments().get(0).toString());
|
||||
String secondArg = TypeNameUtils.simpleName(call.getArguments().get(1).toString());
|
||||
if (sourceClassName.equals(firstArg) && secondArg.endsWith("Entity")) {
|
||||
entities.add(secondArg);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
return entities;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package com.aicheck.parser;
|
||||
|
||||
import com.aicheck.model.ApiEndpoint;
|
||||
import com.github.javaparser.StaticJavaParser;
|
||||
import com.github.javaparser.ast.CompilationUnit;
|
||||
import com.github.javaparser.ast.NodeList;
|
||||
import com.github.javaparser.ast.expr.Expression;
|
||||
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
|
||||
import com.github.javaparser.ast.body.MethodDeclaration;
|
||||
import com.github.javaparser.ast.body.Parameter;
|
||||
import com.github.javaparser.ast.body.TypeDeclaration;
|
||||
import com.github.javaparser.ast.expr.AnnotationExpr;
|
||||
import com.github.javaparser.ast.type.Type;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 扫描 Controller / Feign 接口,提取 HTTP 方法、URI、入参/返回类型。
|
||||
*/
|
||||
public class EndpointParser {
|
||||
private static final Set<String> MAPPING_ANNOTATIONS = Set.of(
|
||||
"GetMapping", "PostMapping", "PutMapping", "DeleteMapping", "PatchMapping", "RequestMapping"
|
||||
);
|
||||
private static final Map<String, String> MAPPING_DEFAULT_METHOD = Map.of(
|
||||
"GetMapping", "GET",
|
||||
"PostMapping", "POST",
|
||||
"PutMapping", "PUT",
|
||||
"DeleteMapping", "DELETE",
|
||||
"PatchMapping", "PATCH"
|
||||
);
|
||||
|
||||
/** 扫描 @RestController / @Controller 目录 */
|
||||
public List<ApiEndpoint> scanControllerDirectory(Path rootDir, String relativePrefix) throws IOException {
|
||||
return scanDirectory(rootDir, relativePrefix, ScanMode.CONTROLLER);
|
||||
}
|
||||
|
||||
/** 扫描 @FeignClient 接口目录 */
|
||||
public List<ApiEndpoint> scanFeignDirectory(Path rootDir, String relativePrefix) throws IOException {
|
||||
return scanDirectory(rootDir, relativePrefix, ScanMode.FEIGN);
|
||||
}
|
||||
|
||||
/** 递归 walk 目录下 .java 并解析 */
|
||||
private List<ApiEndpoint> scanDirectory(Path rootDir, String relativePrefix, ScanMode mode) throws IOException {
|
||||
if (!Files.exists(rootDir)) {
|
||||
return List.of();
|
||||
}
|
||||
List<ApiEndpoint> endpoints = new ArrayList<>();
|
||||
try (Stream<Path> paths = Files.walk(rootDir)) {
|
||||
paths.filter(path -> path.toString().endsWith(".java")).forEach(path -> {
|
||||
try {
|
||||
String source = Files.readString(path, StandardCharsets.UTF_8);
|
||||
String relativePath = toRelativePath(relativePrefix, rootDir, path);
|
||||
endpoints.addAll(parseCompilationUnit(source, relativePath, mode));
|
||||
} catch (IOException ignored) {
|
||||
// 跳过无法读取的文件
|
||||
}
|
||||
});
|
||||
}
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
/** 解析单个编译单元,过滤 Controller 或 Feign */
|
||||
private List<ApiEndpoint> parseCompilationUnit(String source, String relativePath, ScanMode mode) {
|
||||
CompilationUnit cu = StaticJavaParser.parse(source);
|
||||
List<ApiEndpoint> endpoints = new ArrayList<>();
|
||||
|
||||
for (TypeDeclaration<?> type : cu.getTypes()) {
|
||||
if (!(type instanceof ClassOrInterfaceDeclaration)) {
|
||||
continue;
|
||||
}
|
||||
ClassOrInterfaceDeclaration declaration = (ClassOrInterfaceDeclaration) type;
|
||||
if (mode == ScanMode.CONTROLLER && !isController(declaration)) {
|
||||
continue;
|
||||
}
|
||||
if (mode == ScanMode.FEIGN && !isFeignClient(declaration)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String basePath = mode == ScanMode.FEIGN
|
||||
? joinPaths(extractFeignBasePath(declaration), extractTypeLevelPath(declaration))
|
||||
: extractTypeLevelPath(declaration);
|
||||
for (MethodDeclaration method : declaration.getMethods()) {
|
||||
if (mode == ScanMode.FEIGN && declaration.isInterface()) {
|
||||
endpoints.addAll(parseMethod(method, basePath, relativePath));
|
||||
} else if (mode == ScanMode.CONTROLLER && !declaration.isInterface()) {
|
||||
endpoints.addAll(parseMethod(method, basePath, relativePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
/** 解析方法上的 Mapping 注解,生成 ApiEndpoint */
|
||||
private List<ApiEndpoint> parseMethod(MethodDeclaration method, String basePath, String sourceFile) {
|
||||
List<ApiEndpoint> endpoints = new ArrayList<>();
|
||||
for (AnnotationExpr annotation : method.getAnnotations()) {
|
||||
String annName = annotation.getNameAsString();
|
||||
if (!MAPPING_ANNOTATIONS.contains(annName)) {
|
||||
continue;
|
||||
}
|
||||
List<String> subPaths = extractPaths(annotation);
|
||||
List<String> httpMethods = extractHttpMethods(annotation, annName);
|
||||
for (String httpMethod : httpMethods) {
|
||||
for (String subPath : subPaths) {
|
||||
String uri = joinPaths(basePath, subPath);
|
||||
Set<String> paramTypes = extractParamTypes(method);
|
||||
Set<String> returnTypes = TypeNameUtils.peelDirectTypeNames(method.getType());
|
||||
endpoints.add(new ApiEndpoint(httpMethod, uri, sourceFile, paramTypes, returnTypes));
|
||||
}
|
||||
}
|
||||
}
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
/** 收集方法入参类型简单名 */
|
||||
private Set<String> extractParamTypes(MethodDeclaration method) {
|
||||
Set<String> paramTypes = new LinkedHashSet<>();
|
||||
for (Parameter parameter : method.getParameters()) {
|
||||
Type type = parameter.getType();
|
||||
paramTypes.add(TypeNameUtils.simpleName(TypeNameUtils.typeToString(type)));
|
||||
paramTypes.addAll(TypeNameUtils.peelDirectTypeNames(type));
|
||||
}
|
||||
return paramTypes;
|
||||
}
|
||||
|
||||
/** 是否 Spring Controller */
|
||||
private boolean isController(ClassOrInterfaceDeclaration declaration) {
|
||||
return declaration.getAnnotations().stream()
|
||||
.anyMatch(ann -> {
|
||||
String name = ann.getNameAsString();
|
||||
return "RestController".equals(name) || "Controller".equals(name);
|
||||
});
|
||||
}
|
||||
|
||||
/** 是否 Feign 客户端接口 */
|
||||
private boolean isFeignClient(ClassOrInterfaceDeclaration declaration) {
|
||||
return declaration.isInterface() && declaration.getAnnotations().stream()
|
||||
.anyMatch(ann -> "FeignClient".equals(ann.getNameAsString()));
|
||||
}
|
||||
|
||||
/** 类级 @RequestMapping 路径 */
|
||||
private String extractTypeLevelPath(ClassOrInterfaceDeclaration declaration) {
|
||||
for (AnnotationExpr annotation : declaration.getAnnotations()) {
|
||||
if ("RequestMapping".equals(annotation.getNameAsString())) {
|
||||
List<String> paths = extractPaths(annotation);
|
||||
if (!paths.isEmpty()) {
|
||||
return paths.get(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** @FeignClient(path=...) 基础路径 */
|
||||
private String extractFeignBasePath(ClassOrInterfaceDeclaration declaration) {
|
||||
for (AnnotationExpr annotation : declaration.getAnnotations()) {
|
||||
if ("FeignClient".equals(annotation.getNameAsString())) {
|
||||
List<String> paths = AnnotationValueReader.readStringArray(annotation, "path");
|
||||
if (!paths.isEmpty()) {
|
||||
return paths.get(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** 从 Mapping 注解读取 value/path */
|
||||
private List<String> extractPaths(AnnotationExpr annotation) {
|
||||
return AnnotationValueReader.readStringArray(annotation, "value", "path");
|
||||
}
|
||||
|
||||
/** 推断 HTTP 方法;RequestMapping 无 method 时默认 GET */
|
||||
private List<String> extractHttpMethods(AnnotationExpr annotation, String annName) {
|
||||
if (!"RequestMapping".equals(annName)) {
|
||||
return List.of(MAPPING_DEFAULT_METHOD.getOrDefault(annName, "GET"));
|
||||
}
|
||||
List<String> methods = AnnotationValueReader.readEnumArray(annotation, "method");
|
||||
if (methods.isEmpty()) {
|
||||
return List.of("GET");
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
|
||||
/** 拼接类级与方法级路径 */
|
||||
private String joinPaths(String base, String sub) {
|
||||
String normalizedBase = normalizePath(base);
|
||||
String normalizedSub = normalizePath(sub);
|
||||
if (normalizedBase.isEmpty()) {
|
||||
return normalizedSub.isEmpty() ? "/" : normalizedSub;
|
||||
}
|
||||
if (normalizedSub.isEmpty()) {
|
||||
return normalizedBase;
|
||||
}
|
||||
String joined = normalizedBase + "/" + normalizedSub.substring(1);
|
||||
return joined.replaceAll("/+", "/");
|
||||
}
|
||||
|
||||
/** 规范化 URI 路径 */
|
||||
private String normalizePath(String path) {
|
||||
if (path == null || path.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
String trimmed = path.trim();
|
||||
if (!trimmed.startsWith("/")) {
|
||||
trimmed = "/" + trimmed;
|
||||
}
|
||||
return trimmed.replaceAll("/+", "/");
|
||||
}
|
||||
|
||||
/** 生成相对仓库根的路径 */
|
||||
private String toRelativePath(String relativePrefix, Path rootDir, Path file) {
|
||||
String relative = rootDir.relativize(file).toString().replace("\\", "/");
|
||||
if (relativePrefix == null || relativePrefix.isBlank()) {
|
||||
return relative;
|
||||
}
|
||||
String prefix = relativePrefix.endsWith("/")
|
||||
? relativePrefix.substring(0, relativePrefix.length() - 1)
|
||||
: relativePrefix;
|
||||
return prefix + "/" + relative;
|
||||
}
|
||||
|
||||
private enum ScanMode {
|
||||
CONTROLLER, FEIGN
|
||||
}
|
||||
|
||||
/** 从注解 AST 读取字符串或枚举数组 */
|
||||
static final class AnnotationValueReader {
|
||||
private AnnotationValueReader() {
|
||||
}
|
||||
|
||||
static List<String> readStringArray(AnnotationExpr annotation, String... keys) {
|
||||
NodeList<?> values = readArrayValues(annotation, keys);
|
||||
List<String> result = new ArrayList<>();
|
||||
for (Object value : values) {
|
||||
String text = value.toString().replace("\"", "").trim();
|
||||
if (!text.isBlank()) {
|
||||
result.add(text);
|
||||
}
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
result.add("");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static List<String> readEnumArray(AnnotationExpr annotation, String key) {
|
||||
NodeList<?> values = readArrayValues(annotation, key);
|
||||
List<String> result = new ArrayList<>();
|
||||
for (Object value : values) {
|
||||
String text = value.toString().trim();
|
||||
if (text.contains(".")) {
|
||||
text = text.substring(text.lastIndexOf('.') + 1);
|
||||
}
|
||||
result.add(text.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static NodeList<?> readArrayValues(AnnotationExpr annotation, String... keys) {
|
||||
if (annotation.isSingleMemberAnnotationExpr()) {
|
||||
Expression value = annotation.asSingleMemberAnnotationExpr().getMemberValue();
|
||||
if (value.isArrayInitializerExpr()) {
|
||||
return value.asArrayInitializerExpr().getValues();
|
||||
}
|
||||
return new NodeList<>(value);
|
||||
}
|
||||
if (annotation.isNormalAnnotationExpr()) {
|
||||
var pairs = annotation.asNormalAnnotationExpr().getPairs();
|
||||
for (var pair : pairs) {
|
||||
for (String key : keys) {
|
||||
if (pair.getNameAsString().equals(key)) {
|
||||
if (pair.getValue().isArrayInitializerExpr()) {
|
||||
return pair.getValue().asArrayInitializerExpr().getValues();
|
||||
}
|
||||
return new NodeList<>(pair.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var pair : pairs) {
|
||||
if ("value".equals(pair.getNameAsString())) {
|
||||
if (pair.getValue().isArrayInitializerExpr()) {
|
||||
return pair.getValue().asArrayInitializerExpr().getValues();
|
||||
}
|
||||
return new NodeList<>(pair.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
return new NodeList<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.aicheck.parser;
|
||||
|
||||
import com.github.javaparser.ast.type.ClassOrInterfaceType;
|
||||
import com.github.javaparser.ast.type.Type;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Java 类型名工具:转字符串、取简单名、剥离 ActionResult/List 等泛型包装。
|
||||
*/
|
||||
public final class TypeNameUtils {
|
||||
/** 需要向内层继续剥离的包装类型 */
|
||||
private static final Set<String> WRAPPER_TYPES = Set.of(
|
||||
"ActionResult", "List", "PageListVO", "Set", "Collection", "Iterable", "Optional"
|
||||
);
|
||||
|
||||
private TypeNameUtils() {
|
||||
}
|
||||
|
||||
/** Type 转无空白字符串 */
|
||||
public static String typeToString(Type type) {
|
||||
if (type == null) {
|
||||
return "Object";
|
||||
}
|
||||
return type.toString().replaceAll("\\s+", "");
|
||||
}
|
||||
|
||||
/** 取类型简单名,去掉包名与泛型 */
|
||||
public static String simpleName(String typeName) {
|
||||
if (typeName == null || typeName.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
String cleaned = typeName.replaceAll("\\s+", "");
|
||||
int genericStart = cleaned.indexOf('<');
|
||||
String base = genericStart >= 0 ? cleaned.substring(0, genericStart) : cleaned;
|
||||
int dot = base.lastIndexOf('.');
|
||||
return dot >= 0 ? base.substring(dot + 1) : base;
|
||||
}
|
||||
|
||||
/** 从 Type AST 收集实际业务类型简单名(穿透包装泛型) */
|
||||
public static Set<String> peelDirectTypeNames(Type type) {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
collectPeelTargets(type, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 从类型字符串收集实际业务类型简单名 */
|
||||
public static Set<String> peelDirectTypeNames(String typeName) {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
collectPeelTargets(typeName, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 递归收集:包装类型则进入泛型参数,否则记录简单名 */
|
||||
private static void collectPeelTargets(Type type, Set<String> result) {
|
||||
if (type == null) {
|
||||
return;
|
||||
}
|
||||
if (type.isClassOrInterfaceType()) {
|
||||
ClassOrInterfaceType classType = type.asClassOrInterfaceType();
|
||||
String name = simpleName(classType.getNameAsString());
|
||||
if (WRAPPER_TYPES.contains(name) && classType.getTypeArguments().isPresent()) {
|
||||
for (Type arg : classType.getTypeArguments().get()) {
|
||||
collectPeelTargets(arg, result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
result.add(name);
|
||||
return;
|
||||
}
|
||||
result.add(simpleName(typeToString(type)));
|
||||
}
|
||||
|
||||
/** 字符串版递归收集 */
|
||||
private static void collectPeelTargets(String typeName, Set<String> result) {
|
||||
String cleaned = typeName.replaceAll("\\s+", "");
|
||||
int genericStart = cleaned.indexOf('<');
|
||||
if (genericStart < 0) {
|
||||
result.add(simpleName(cleaned));
|
||||
return;
|
||||
}
|
||||
String outer = simpleName(cleaned.substring(0, genericStart));
|
||||
String inner = cleaned.substring(genericStart + 1, cleaned.lastIndexOf('>'));
|
||||
if (WRAPPER_TYPES.contains(outer)) {
|
||||
for (String part : splitGenericArgs(inner)) {
|
||||
collectPeelTargets(part, result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
result.add(outer);
|
||||
}
|
||||
|
||||
/** 按逗号分割泛型参数,支持嵌套 <> */
|
||||
private static List<String> splitGenericArgs(String inner) {
|
||||
List<String> parts = new java.util.ArrayList<>();
|
||||
int depth = 0;
|
||||
StringBuilder current = new StringBuilder();
|
||||
for (char ch : inner.toCharArray()) {
|
||||
if (ch == '<') {
|
||||
depth++;
|
||||
} else if (ch == '>') {
|
||||
depth--;
|
||||
} else if (ch == ',' && depth == 0) {
|
||||
parts.add(current.toString().trim());
|
||||
current.setLength(0);
|
||||
continue;
|
||||
}
|
||||
current.append(ch);
|
||||
}
|
||||
if (current.length() > 0) {
|
||||
parts.add(current.toString().trim());
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user