package com.codechecker.api.parser; import com.codechecker.api.model.EndpointSnapshot; import com.codechecker.api.model.MethodParameterSnapshot; import com.codechecker.parser.TypeNameUtils; import com.github.javaparser.StaticJavaParser; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.NodeList; 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.expr.Expression; import com.github.javaparser.ast.expr.NormalAnnotationExpr; import com.github.javaparser.ast.type.Type; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; /** * 解析 Controller / Feign 接口完整快照(含入参明细)。 */ public class EndpointSnapshotParser { private static final Set MAPPING_ANNOTATIONS = Set.of( "GetMapping", "PostMapping", "PutMapping", "DeleteMapping", "PatchMapping", "RequestMapping" ); private static final Map MAPPING_DEFAULT_METHOD = Map.of( "GetMapping", "GET", "PostMapping", "POST", "PutMapping", "PUT", "DeleteMapping", "DELETE", "PatchMapping", "PATCH" ); private static final Set FRAMEWORK_PARAM_TYPES = Set.of( "HttpServletRequest", "HttpServletResponse", "BindingResult", "Principal", "Authentication", "Model", "ModelMap", "UriComponentsBuilder", "WebRequest", "NativeWebRequest", "Errors", "Locale" ); private final boolean excludeFrameworkParams; public EndpointSnapshotParser(boolean excludeFrameworkParams) { this.excludeFrameworkParams = excludeFrameworkParams; } public List parseSource(String source, String sourceFile, boolean feignMode) { if (source == null || source.isBlank()) { return List.of(); } CompilationUnit cu = StaticJavaParser.parse(source); List snapshots = new ArrayList<>(); for (TypeDeclaration type : cu.getTypes()) { if (!(type instanceof ClassOrInterfaceDeclaration)) { continue; } ClassOrInterfaceDeclaration decl = (ClassOrInterfaceDeclaration) type; if (feignMode && !isFeignClient(decl)) { continue; } if (!feignMode && !isController(decl)) { continue; } String basePath = feignMode ? joinPaths(extractFeignBasePath(decl), extractTypeLevelPath(decl)) : extractTypeLevelPath(decl); String className = decl.getNameAsString(); for (MethodDeclaration method : decl.getMethods()) { if (feignMode && !decl.isInterface()) { continue; } if (!feignMode && decl.isInterface()) { continue; } snapshots.addAll(parseMethod(method, basePath, sourceFile, className)); } } return snapshots; } private List parseMethod(MethodDeclaration method, String basePath, String sourceFile, String className) { List result = new ArrayList<>(); for (AnnotationExpr annotation : method.getAnnotations()) { String annName = annotation.getNameAsString(); if (!MAPPING_ANNOTATIONS.contains(annName)) { continue; } List subPaths = readStringArray(annotation, "value", "path"); List httpMethods = extractHttpMethods(annotation, annName); List params = extractParameters(method); String methodDescription = MethodDescriptionExtractor.extract(method); String fingerprint = EndpointSnapshot.buildFingerprint(sourceFile, method.getNameAsString()); for (String httpMethod : httpMethods) { for (String subPath : subPaths) { String uri = joinPaths(basePath, subPath); result.add(new EndpointSnapshot(fingerprint, httpMethod, uri, sourceFile, className, method.getNameAsString(), methodDescription, params)); } } } return result; } private List extractParameters(MethodDeclaration method) { Map paramDescriptions = MethodParamJavadocExtractor.extract(method); List params = new ArrayList<>(); for (Parameter parameter : method.getParameters()) { String typeName = TypeNameUtils.typeToString(parameter.getType()); String simple = TypeNameUtils.simpleName(typeName); if (excludeFrameworkParams && FRAMEWORK_PARAM_TYPES.contains(simple)) { continue; } String source = resolveParamSource(parameter); String paramName = parameter.getNameAsString(); String bindingName = resolveBindingName(parameter, source, paramName); boolean required = resolveRequired(parameter, source); String dtoName = "body".equals(source) ? simple : ""; String description = paramDescriptions.getOrDefault(paramName, ""); params.add(new MethodParameterSnapshot( paramName, bindingName, typeName, source, required, description, dtoName )); } return params; } private String resolveBindingName(Parameter parameter, String source, String paramName) { if (!"path".equals(source) && !"query".equals(source)) { return paramName; } String annName = "path".equals(source) ? "PathVariable" : "RequestParam"; for (AnnotationExpr ann : parameter.getAnnotations()) { if (!annName.equals(ann.getNameAsString())) { continue; } List bindings = readStringArray(ann, "value", "name"); for (String binding : bindings) { if (binding != null && !binding.isBlank()) { return binding; } } } return paramName; } private String resolveParamSource(Parameter parameter) { for (AnnotationExpr ann : parameter.getAnnotations()) { String name = ann.getNameAsString(); if ("RequestBody".equals(name)) { return "body"; } if ("PathVariable".equals(name)) { return "path"; } if ("RequestParam".equals(name)) { return "query"; } } return "simple"; } private boolean resolveRequired(Parameter parameter, String source) { if ("query".equals(source)) { for (AnnotationExpr ann : parameter.getAnnotations()) { if ("RequestParam".equals(ann.getNameAsString()) && ann.isNormalAnnotationExpr()) { for (var pair : ann.asNormalAnnotationExpr().getPairs()) { if ("required".equals(pair.getNameAsString())) { return !"false".equalsIgnoreCase(pair.getValue().toString().trim()); } } } } } return !"query".equals(source); } private boolean isController(ClassOrInterfaceDeclaration decl) { return decl.getAnnotations().stream() .anyMatch(ann -> { String n = ann.getNameAsString(); return "RestController".equals(n) || "Controller".equals(n); }); } private boolean isFeignClient(ClassOrInterfaceDeclaration decl) { return decl.isInterface() && decl.getAnnotations().stream() .anyMatch(ann -> "FeignClient".equals(ann.getNameAsString())); } private String extractTypeLevelPath(ClassOrInterfaceDeclaration decl) { for (AnnotationExpr annotation : decl.getAnnotations()) { if ("RequestMapping".equals(annotation.getNameAsString())) { List paths = readStringArray(annotation, "value", "path"); if (!paths.isEmpty()) { return paths.get(0); } } } return ""; } private String extractFeignBasePath(ClassOrInterfaceDeclaration decl) { for (AnnotationExpr annotation : decl.getAnnotations()) { if ("FeignClient".equals(annotation.getNameAsString())) { List paths = readStringArray(annotation, "path"); if (!paths.isEmpty()) { return paths.get(0); } } } return ""; } private List extractHttpMethods(AnnotationExpr annotation, String annName) { if (!"RequestMapping".equals(annName)) { return List.of(MAPPING_DEFAULT_METHOD.getOrDefault(annName, "GET")); } List methods = readEnumArray(annotation, "method"); return methods.isEmpty() ? List.of("GET") : 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; } return (normalizedBase + "/" + normalizedSub.substring(1)).replaceAll("/+", "/"); } private String normalizePath(String path) { if (path == null || path.isBlank()) { return ""; } String trimmed = path.trim(); if (!trimmed.startsWith("/")) { trimmed = "/" + trimmed; } return trimmed.replaceAll("/+", "/"); } private List readStringArray(AnnotationExpr annotation, String... keys) { NodeList values = readArrayValues(annotation, keys); List 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; } private List readEnumArray(AnnotationExpr annotation, String key) { NodeList values = readArrayValues(annotation, key); List 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 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<>(); } }