import com.puppycrawl.tools.checkstyle.*; import com.puppycrawl.tools.checkstyle.api.*; import java.io.*; import java.nio.file.*; import java.util.*; import net.sourceforge.pmd.*; import net.sourceforge.pmd.cpd.*; import net.sourceforge.pmd.lang.*; import net.sourceforge.pmd.lang.ast.*; import net.sourceforge.pmd.lang.java.ast.*; import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule; import net.sourceforge.pmd.lang.java.symbols.*; import net.sourceforge.pmd.lang.java.types.*; import net.sourceforge.pmd.lang.metrics.MetricOptions; import net.sourceforge.pmd.lang.rule.RuleSet; /** Deliberately independent of the application build and its annotation processors. */ public class Standards { static final List findings = new ArrayList<>(); static final Map> codeLines = new HashMap<>(); static final Set ownedTypes = new HashSet<>(); static final Map types = new HashMap<>(); static final List units = new ArrayList<>(); static Path root; static String module; record Waiver(String metric, String executable, double value, String reason) {} static final List waivers = new ArrayList<>(); /** * Checks configured backend sources and writes standards reports without changing maintained code. * * @param args the consumer repository root as the sole argument * @throws Exception if configuration, resolution or parsing fails, or a blocking violation is found */ public static void main(final String... args) throws Exception { findings.clear(); codeLines.clear(); types.clear(); units.clear(); ownedTypes.clear(); root = Path.of(args[0]).toAbsolutePath().normalize(); final var settings = new Properties(); try (var input = Files.newInputStream(root.resolve("engineering.properties"))) { settings.load(input); } module = settings.getProperty("BACKEND_MODULE"); if (module == null || module.isBlank()) throw new IllegalArgumentException("BACKEND_MODULE is required"); waivers.clear(); final Path waiverFile = root.resolve("tooling/standards/waivers.tsv"); if (Files.exists(waiverFile)) for (String line : Files.readAllLines(waiverFile)) { if (line.isBlank() || line.startsWith("#")) continue; final String[] fields = line.split("\t", -1); if (fields.length != 4 || fields[0].contains("*") || fields[1].contains("*") || fields[3].isBlank()) throw new IllegalArgumentException("Invalid exact waiver in " + waiverFile + ": " + line); waivers.add(new Waiver(fields[0], fields[1], Double.parseDouble(fields[2]), fields[3])); } final Path output = root.resolve(module + "/target/standards"); Files.createDirectories(output); final List files = new ArrayList<>(); for (String scope : List.of("main", "test")) { try (var paths = Files.walk(root.resolve(module + "/src/" + scope + "/java"))) { paths.filter(p -> p.toString().endsWith(".java")).sorted().forEach(files::add); } } for (Path file : files) { final var matcher = java.util.regex.Pattern.compile("package\\s+([\\w.]+)\\s*;").matcher(Files.readString(file)); if (matcher.find()) ownedTypes.add( matcher.group(1) + "." + file.getFileName().toString().replace(".java", "")); } checkstyle(files); final PMDConfiguration config = new PMDConfiguration(); config.setDefaultLanguageVersion( LanguageRegistry.PMD.getLanguageById("java").getVersion("25")); config.setThreads(1); config.prependAuxClasspath( Path.of(System.getProperty("java.home"), "lib/jrt-fs.jar").toString()); config.setIgnoreIncrementalAnalysis(true); // Compiled application/test dependencies permit PMD to resolve external types. final Path cp = root.resolve(module + "/target/standards-classpath.txt"); if (Files.exists(cp)) config.prependAuxClasspath( Files.readString(cp) + File.pathSeparator + root.resolve(module + "/target/classes")); try (var analysis = PmdAnalysis.create(config)) { analysis.addRuleSet(RuleSet.forSingleRule(new Measurements())); for (String rule : List.of( "category/java/codestyle.xml/LocalVariableCouldBeFinal", "category/java/codestyle.xml/MethodArgumentCouldBeFinal", "category/java/design.xml/ImmutableField")) analysis.addRuleSet(analysis.newRuleSetLoader().loadFromResource(rule)); for (Path file : files) analysis.files().addFile(file); final var report = analysis.performAnalysisAndCollectReport(); for (var error : report.getProcessingErrors()) findings.add("BLOCK parser " + error.getFileId() + " " + error.getMsg()); for (var violation : report.getViolations()) findings.add("BLOCK " + violation.getFileId() + ":" + violation.getBeginLine() + " " + violation.getRule().getName() + " " + violation.getDescription()); for (var violation : report.getSuppressedViolations()) findings.add("BLOCK Unapproved suppression: " + violation); for (var error : report.getConfigurationErrors()) findings.add("BLOCK configuration " + error); } duplication(files); Collections.sort(findings); Files.write(output.resolve("report.txt"), findings); findings.stream().filter(f -> !f.startsWith("METRIC ")).forEach(System.out::println); System.out.println( "Backend standards: " + files.size() + " source files; full metrics: " + output.resolve("report.txt")); if (findings.stream().anyMatch(s -> s.startsWith("BLOCK"))) throw new IllegalStateException("Backend standards failed; see " + output.resolve("report.txt")); } static void checkstyle(List files) throws Exception { final Checker checker = new Checker(); checker.setModuleClassLoader(Standards.class.getClassLoader()); checker.configure(ConfigurationLoader.loadConfiguration( root.resolve("generated/tooling/standards/checkstyle.xml").toString(), new PropertiesExpander(new Properties()))); checker.addListener(new AuditListener() { public void auditStarted(AuditEvent e) {} public void auditFinished(AuditEvent e) {} public void fileStarted(AuditEvent e) {} public void fileFinished(AuditEvent e) {} public void addError(AuditEvent e) { findings.add("BLOCK " + e.getFileName() + ":" + e.getLine() + " " + e.getMessage()); } public void addException(AuditEvent e, Throwable t) { findings.add("BLOCK " + e.getFileName() + " parser: " + t); } }); try { checker.process(files.stream().map(Path::toFile).toList()); } finally { checker.destroy(); } } public static class Measurements extends AbstractJavaRule { public Measurements() { setName("BackendMeasurements"); setMessage("Backend standards"); setLanguage(LanguageRegistry.PMD.getLanguageById("java")); } @Override public Object visit(ASTCompilationUnit unit, Object data) { units.add(unit); for (var type : all(unit, ASTTypeDeclaration.class)) types.put(type.getSymbol().getBinaryName(), type); final Set lines = lines(unit); codeLines.put(path(unit), lines); for (var node : all(unit, JavaNode.class)) { if (node instanceof ASTExecutableDeclaration executable) executable(executable); if (node instanceof ASTCompactConstructorDeclaration compact) { final var cyclo = new org.apache.commons.lang3.mutable.MutableInt(1); compact.acceptVisitor(new ScopedCyclo(compact), cyclo); final var cognitive = new net.sourceforge.pmd.lang.java.metrics.internal.CognitiveComplexityVisitor.State( compact); compact.acceptVisitor(new ScopedCognitive(), cognitive); metric(compact, "cyclomatic", cyclo.intValue(), 7, 10); metric(compact, "cognitive", cognitive.getComplexity(), 10, 15); metric(compact, "executableLines", lines(compact).size(), 30, 50); metric(compact, "nesting", nesting(compact.getBody(), 0), 3, 4); metric( compact, "parameters", compact.getEnclosingType().getRecordComponents().size(), 5, 7); visibility(compact); } if (node instanceof ASTVariableId variable) variable(variable); if (node instanceof ASTTypeDeclaration nested && nested.isNested() && !nested.isLocal() && !nested.isAnonymous()) visibility(nested); if (node instanceof ASTImportDeclaration imp && imp.isStatic() && production(unit)) block(imp, "Static imports are test-only"); if (node instanceof ASTExecutableDeclaration || node instanceof ASTFieldDeclaration) visibility((ModifierOwner) node); if (node instanceof ASTMethodDeclaration method && method.isOverride() && !method.isAnnotationPresent("java.lang.Override")) block(method, "Overriding methods require @Override"); } for (var type : all(unit, ASTTypeDeclaration.class)) typeMetrics(type); return data; } } static Path path(Node node) { return Path.of(node.getTextDocument().getFileId().getAbsolutePath()) .toAbsolutePath() .normalize(); } static boolean production(Node node) { return path(node).startsWith(root.resolve(module + "/src/main/java")); } static List all(Node node, Class clazz) { final List result = new ArrayList<>(); if (clazz.isInstance(node)) result.add(clazz.cast(node)); for (int i = 0; i < node.getNumChildren(); i++) result.addAll(all(node.getChild(i), clazz)); return result; } static void block(Node node, String message) { findings.add("BLOCK " + location(node) + " " + message); } static String location(Node node) { return root.relativize(path(node)) + ":" + node.getBeginLine(); } static void metric(Node node, String name, double value, double review, double limit) { if (value > limit) for (Waiver waiver : waivers) { if (waiver.metric().equals(name) && waiver.executable().equals(identity(node)) && waiver.value() == value) { findings.add("WAIVER " + location(node) + " " + name + "=" + value + " " + waiver.reason()); return; } } findings.add((value > limit ? "BLOCK " : value > review ? "REVIEW " : "METRIC ") + location(node) + " " + name + "=" + value); } static String identity(Node node) { if (node instanceof ASTCompactConstructorDeclaration compact) { final List parameters = new ArrayList<>(); for (var component : compact.getEnclosingType().getRecordComponents()) parameters.add(component.getTypeMirror().toString()); return compact.getEnclosingType().getBinaryName() + "#(" + String.join(",", parameters) + ")"; } if (node instanceof ASTExecutableDeclaration executable) { final List parameters = new ArrayList<>(); for (var parameter : executable.getFormalParameters()) parameters.add(parameter.getTypeMirror().toString()); return executable.getEnclosingType().getBinaryName() + "#" + (executable instanceof ASTConstructorDeclaration ? "" : executable.getName()) + "(" + String.join(",", parameters) + ")"; } return node instanceof ASTTypeDeclaration type ? type.getBinaryName() : location(node); } static Set lines(JavaNode node) { final Set result = new HashSet<>(); var token = node.getFirstToken(); while (token != null && !token.isEof()) { final var loc = token.getReportLocation(); for (int line = loc.getStartLine(); line <= loc.getEndLine(); line++) result.add(line); if (token == node.getLastToken()) break; token = token.getNext(); } final String[] sourceLines = node.getTextDocument().getText().toString().split("\\R", -1); result.removeIf(line -> line > sourceLines.length || sourceLines[line - 1].isBlank()); return result; } static void executable(ASTExecutableDeclaration method) { { metric( method, "parameters", method.getArity(), method instanceof ASTMethodDeclaration ? 4 : 5, method instanceof ASTMethodDeclaration ? 5 : 7); } if (method.getBody() == null) return; metric(method, "cyclomatic", cyclomatic(method), 7, 10); metric(method, "cognitive", cognitive(method), 10, 15); metric(method, "executableLines", lines(method).size(), 30, 50); metric(method, "nesting", nesting(method.getBody(), 0), 3, 4); } static final class ScopedCyclo extends net.sourceforge.pmd.lang.java.metrics.internal.CycloVisitor { ScopedCyclo(JavaNode node) { super(MetricOptions.emptyOptions(), node); } @Override public Void visit(ASTLambdaExpression lambda, org.apache.commons.lang3.mutable.MutableInt value) { for (int i = 0; i < lambda.getNumChildren(); i++) lambda.getChild(i).acceptVisitor(this, value); return null; } } static final class ScopedCognitive extends net.sourceforge.pmd.lang.java.metrics.internal.CognitiveComplexityVisitor { @Override public Void visit(ASTClassBody body, State state) { return null; } @Override public Void visitJavaNode(JavaNode node, State state) { return node instanceof ASTTypeDeclaration || node instanceof ASTTypeBody ? null : super.visitJavaNode(node, state); } } static int cyclomatic(ASTExecutableDeclaration method) { final var value = new org.apache.commons.lang3.mutable.MutableInt(); method.acceptVisitor(new ScopedCyclo(method), value); return value.intValue(); } static int cognitive(ASTExecutableDeclaration method) { final var state = new net.sourceforge.pmd.lang.java.metrics.internal.CognitiveComplexityVisitor.State(method); method.acceptVisitor(new ScopedCognitive(), state); return state.getComplexity(); } static int nesting(Node node, int depth) { if (node instanceof ASTTypeDeclaration) return depth; final String kind = node.getXPathNodeName(); final boolean control = Set.of( "IfStatement", "ForStatement", "ForeachStatement", "WhileStatement", "DoStatement", "SwitchStatement", "SwitchExpression", "CatchClause") .contains(kind); final boolean elseIf = node instanceof ASTIfStatement && node.getParent() instanceof ASTIfStatement p && p.getElseBranch() == node; final int current = depth + (control && !elseIf ? 1 : 0); int maximum = current; for (int i = 0; i < node.getNumChildren(); i++) maximum = Math.max(maximum, nesting(node.getChild(i), current)); return maximum; } static void visibility(ModifierOwner member) { final ASTTypeDeclaration owner = ((JavaNode) member).getEnclosingType(); if (owner == null || owner.isAnnotation()) return; if (!member.getModifiers().hasAnyExplicitly(JModifier.PUBLIC, JModifier.PROTECTED, JModifier.PRIVATE)) block(member, "Declare explicit member visibility"); } static void variable(ASTVariableId id) { if (id.isEnumConstant() && !id.getName().matches("[A-Z][A-Z0-9]*(_[A-Z0-9]+)*")) block(id, "Enum constants require UPPER_SNAKE_CASE"); if (id.isRecordComponent() || id.isEnumConstant() || id.isUnnamed()) return; if (id.isTypeInferred() && !id.isLambdaParameter() && !id.isPatternBinding()) { final ASTExpression initializer = id.getInitializer(); if (!(initializer instanceof ASTConstructorCall) && !(initializer instanceof ASTCastExpression) && !(initializer instanceof ASTArrayAllocation)) { block(id, "var requires a visible constructor or cast type"); } } if (id.isLambdaParameter() && id.getParent() instanceof ASTLambdaParameter parameter && parameter.hasVarKeyword()) block(id, "Use an explicit lambda parameter type with final"); if (id.isFinal()) return; final boolean written = id.getLocalUsages().stream().anyMatch(u -> u.getAccessType() != ASTAssignableExpr.AccessType.READ); if (written) return; if (id.isLambdaParameter()) { block(id, "Assigned-once lambda parameter requires final: " + id.getName()); return; } if (id.isField()) { final ASTFieldDeclaration field = id.ancestors(ASTFieldDeclaration.class).first(); if (field == null || id.getInitializer() == null || field.isAnnotationPresent("jakarta.inject.Inject") || field.isAnnotationPresent("lombok.Setter") || id.getEnclosingType().isAnnotationPresent("lombok.Setter") || id.getEnclosingType().isAnnotationPresent("lombok.Data")) return; } block(id, "Assigned-once variable requires final: " + id.getName()); } static boolean owned(String name) { return types.containsKey(name) || ownedTypes.stream().anyMatch(n -> name.equals(n) || name.startsWith(n + "$")); } static void typeMetrics(ASTTypeDeclaration type) { final Set ownLines = new HashSet<>(); final Set scopeLines = codeLines.getOrDefault(path(type), lines(type)); final var nestedTypes = all(type, ASTTypeDeclaration.class).stream() .filter(t -> t != type) .toList(); var token = type.getFirstToken(); while (token != null && !token.isEof()) { final var region = token.getRegion(); if (nestedTypes.stream().noneMatch(t -> t.getTextRegion().contains(region))) { final var loc = token.getReportLocation(); for (int line = loc.getStartLine(); line <= loc.getEndLine(); line++) if (scopeLines.contains(line)) ownLines.add(line); } if (token == type.getLastToken()) break; token = token.getNext(); } metric(type, "classLines", ownLines.size(), 300, 500); if (type.isRecord()) findings.add("DESIGN " + location(type) + " recordComponents=" + type.getRecordComponents().size() + " (manual design review)"); int depth = 0; var superclass = type.getSymbol().getSuperclass(); while (superclass != null && owned(superclass.getBinaryName())) { depth++; superclass = superclass.getSuperclass(); } metric(type, "ownSuperclassDepth", depth, 2, 3); final Set fanout = new HashSet<>(); for (var ref : all(type, ASTClassType.class)) { if (ref.getEnclosingType() != type) continue; final var symbol = ref.getTypeMirror().getSymbol(); if (symbol != null && symbol.isUnresolved()) block( ref, "Cannot resolve type " + ref.getSimpleName() + "; compile the backend and regenerate target/standards-classpath.txt before" + " checking"); if (symbol instanceof JClassSymbol resolved && !resolved.equals(type.getSymbol())) fanout.add(resolved.getBinaryName()); } metric(type, "fanOut", fanout.size(), 15, Double.POSITIVE_INFINITY); int publicMethods = 0; for (var method : type.getOperations()) if (method instanceof ASTMethodDeclaration && method.hasExplicitModifiers(JModifier.PUBLIC) && !trivial(method)) publicMethods++; metric(type, "publicMethods", publicMethods, 15, Double.POSITIVE_INFINITY); final Set dependencies = new HashSet<>(); injected(type, dependencies, new HashSet<>()); metric(type, "injectedDependencies", dependencies.size(), 5, 7); } static boolean trivial(ASTExecutableDeclaration method) { if (method.getBody() == null || method.getBody().getNumChildren() != 1) return false; final JavaNode statement = method.getBody().getChild(0); final String name = method.getName(); String property = name; if (name.startsWith("get") || name.startsWith("set")) property = name.substring(3); else if (name.startsWith("is")) property = name.substring(2); if (property.isEmpty()) return false; property = Character.toLowerCase(property.charAt(0)) + property.substring(1); final String fieldName = property; if (method.getEnclosingType().getSymbol().getDeclaredFields().stream() .noneMatch(f -> f.getSimpleName().equals(fieldName))) return false; if (method.getArity() == 0) return statement instanceof ASTReturnStatement returned && accessorField(returned.getExpr(), method, property); if (method.getArity() != 1 || !name.startsWith("set")) return false; return statement instanceof ASTExpressionStatement expression && expression.getExpr() instanceof ASTAssignmentExpression assignment && !assignment.isCompound() && accessorField(assignment.getLeftOperand(), method, property) && assignment.getRightOperand() instanceof ASTVariableAccess value && method.getFormalParameters().get(0).getVarId().getSymbol().equals(value.getReferencedSym()); } static boolean accessorField(ASTExpression expression, ASTExecutableDeclaration method, String property) { if (!(expression instanceof ASTAssignableExpr.ASTNamedReferenceExpr reference)) return false; if (expression instanceof ASTFieldAccess field && !(field.getQualifier() instanceof ASTThisExpression)) return false; return method.getEnclosingType().getSymbol().getDeclaredFields().stream() .anyMatch( field -> field.getSimpleName().equals(property) && field.equals(reference.getReferencedSym())); } record Dependency(JTypeMirror type, Set qualifiers) {} static boolean annotated(AnnotableSymbol symbol, String annotation) { return symbol.getDeclaredAnnotations().stream() .anyMatch(a -> a.getBinaryName().equals(annotation)); } static Dependency dependency(JVariableSymbol variable) { return dependency(variable, Substitution.EMPTY); } static Dependency dependency(JVariableSymbol variable, Substitution substitution) { final Set qualifiers = new HashSet<>(); for (var annotation : variable.getDeclaredAnnotations()) { if (annotation.getBinaryName().equals("jakarta.inject.Named") || annotated(annotation.getAnnotationSymbol(), "jakarta.inject.Qualifier")) { final Map attributes = new TreeMap<>(); for (String name : annotation.getAttributeNames()) attributes.put(name, String.valueOf(annotation.getAttribute(name))); qualifiers.add(annotation.getBinaryName() + attributes); } } return new Dependency(variable.getTypeMirror(substitution), qualifiers); } static void injected(ASTTypeDeclaration type, Set dependencies, Set visited) { if (!visited.add(type.getBinaryName())) return; final var constructors = type.getSymbol().getConstructors(); final boolean bean = type.getDeclaredAnnotations().any(a -> { final var symbol = a.getTypeMirror().getSymbol(); return Set.of( "jakarta.inject.Singleton", "jakarta.enterprise.context.ApplicationScoped", "jakarta.enterprise.context.RequestScoped", "jakarta.enterprise.context.Dependent", "jakarta.enterprise.context.SessionScoped") .contains(symbol.getCanonicalName()) || annotated(symbol, "jakarta.inject.Scope") || annotated(symbol, "jakarta.enterprise.context.NormalScope") || annotated(symbol, "jakarta.enterprise.inject.Stereotype"); }); for (var constructor : constructors) { if (annotated(constructor, "jakarta.inject.Inject") || bean && constructors.size() == 1) { for (var parameter : constructor.getFormalParameters()) dependencies.add(dependency(parameter)); } } // PMD deliberately sees handwritten source, so account for Lombok's generated constructor. if (bean && type.getOperations() .filterIs(ASTConstructorDeclaration.class) .isEmpty()) { final boolean allArgs = type.isAnnotationPresent("lombok.AllArgsConstructor"); final boolean requiredArgs = type.isAnnotationPresent("lombok.RequiredArgsConstructor"); if (allArgs || requiredArgs) for (var field : type.getDeclarations(ASTFieldDeclaration.class)) { if (!field.isStatic()) for (var id : field.getVarIds()) if (allArgs || id.getInitializer() == null && (id.isFinal() || field.isAnnotationPresent("lombok.NonNull"))) dependencies.add(dependency(id.getSymbol())); } } inheritedInjection(type.getTypeMirror(), dependencies, new HashSet<>()); } static void inheritedInjection(JClassType resolvedType, Set dependencies, Set visited) { if (resolvedType == null) return; final JClassSymbol symbol = resolvedType.getSymbol(); final Substitution substitution = resolvedType.getTypeParamSubst(); if (!visited.add(symbol.getBinaryName())) return; for (var field : symbol.getDeclaredFields()) if (annotated(field, "jakarta.inject.Inject")) dependencies.add(dependency(field, substitution)); for (var method : symbol.getDeclaredMethods()) if (annotated(method, "jakarta.inject.Inject")) for (var parameter : method.getFormalParameters()) dependencies.add(dependency(parameter, substitution)); inheritedInjection(resolvedType.getSuperClass(), dependencies, visited); } static void duplication(List files) throws Exception { final var config = new CPDConfiguration(); config.setMinimumTileSize(100); config.setIgnoreIdentifiers(false); config.setIgnoreLiterals(false); config.setSkipLexicalErrors(false); config.setNoSkipBlocks(true); final Set duplicated = new HashSet<>(); try (var cpd = CpdAnalysis.create(config)) { for (var file : files) if (file.startsWith(root.resolve(module + "/src/main/java"))) cpd.files().addFile(file); cpd.performAnalysis(report -> { for (var error : report.getProcessingErrors()) findings.add("BLOCK CPD parser " + error); for (var match : report.getMatches()) for (var mark : match) { final var loc = mark.getLocation(); final Path path = Path.of(loc.getFileId().getAbsolutePath()) .toAbsolutePath() .normalize(); for (int line = loc.getStartLine(); line <= loc.getEndLine(); line++) if (codeLines.getOrDefault(path, Set.of()).contains(line)) duplicated.add(path + ":" + line); findings.add("CLONE " + path + ":" + loc.getStartLine() + "-" + loc.getEndLine()); } }); } final long total = codeLines.entrySet().stream() .filter(e -> e.getKey().startsWith(root.resolve(module + "/src/main/java"))) .mapToLong(e -> e.getValue().size()) .sum(); final double density = total == 0 ? 0 : 100.0 * duplicated.size() / total; findings.add((density > 3 ? "BLOCK " : density > 1 ? "REVIEW " : "METRIC ") + "productionDuplication=" + density + "% (" + duplicated.size() + "/" + total + ")"); } }