S19: consume versioned engineering resources and move landscape documentation
All checks were successful
Build, test and publish / verify (push) Successful in 8m54s

This commit is contained in:
2026-11-08 15:23:10 +01:00
parent bd898e92d2
commit c6a495a93c
100 changed files with 1869 additions and 4274 deletions

View File

@@ -0,0 +1,30 @@
import java.nio.file.Path;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Element;
/** Exact module-wide line gate; consumes JaCoCo XML without rounding percentages. */
public class Coverage {
public static void main(String[] args) throws Exception {
final var factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
final Element report =
factory.newDocumentBuilder().parse(Path.of(args[0]).toFile()).getDocumentElement();
for (var child = report.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child instanceof Element counter
&& counter.getTagName().equals("counter")
&& counter.getAttribute("type").equals("LINE")) {
final long covered = Long.parseLong(counter.getAttribute("covered"));
final long missed = Long.parseLong(counter.getAttribute("missed"));
final long total = covered + missed;
System.out.println(
"Backend line coverage: " + covered + "/" + total + " (" + 100.0 * covered / total + "%)");
if (total == 0 || 5 * covered < 4 * total)
throw new IllegalStateException("Backend line coverage must reach 80%: " + args[0]);
return;
}
}
throw new IllegalStateException("Missing module LINE counter: " + args[0]);
}
}

View File

@@ -0,0 +1,136 @@
import com.palantir.javaformat.java.Formatter;
import com.palantir.javaformat.java.JavaFormatterOptions;
import java.nio.file.*;
import java.security.MessageDigest;
import java.util.*;
import org.openrewrite.*;
import org.openrewrite.internal.InMemoryLargeSourceSet;
import org.openrewrite.text.*;
/** Source-launcher bootstrap: no project-specific artifact is resolved or installed. */
public class Format {
public static void main(String[] args) throws Exception {
Path root = Path.of(args[0]).toAbsolutePath().normalize();
boolean check = switch (args[1]) {
case "check" -> true;
case "apply" -> false;
default -> throw new IllegalArgumentException("format.mode must be apply or check");
};
Path cacheFile = root.resolve("target/palantir-cache.properties");
var cache = new Properties();
if (Files.exists(cacheFile)) {
try (var input = Files.newInputStream(cacheFile)) {
cache.load(input);
}
}
String configuration = digest(Files.readString(root.resolve("pom.xml"))
+ Files.readString(root.resolve("generated/tooling/format/Format.java"))
+ System.getProperty("java.version")
+ System.getProperty("java.class.path"));
if (!configuration.equals(cache.getProperty("configuration"))) cache.clear();
cache.setProperty("configuration", configuration);
List<SourceFile> sources = new ArrayList<>();
var settings = new Properties();
try (var input = Files.newInputStream(root.resolve("engineering.properties"))) {
settings.load(input);
}
for (String directory :
settings.getProperty("FORMAT_DIRECTORIES", "backend/src").split(",")) {
if (!Files.isDirectory(root.resolve(directory))) continue;
try (var paths = Files.walk(root.resolve(directory))) {
for (Path path : paths.filter(p -> p.toString().endsWith(".java"))
.sorted()
.toList()) {
try {
String content = Files.readString(path);
String key = root.relativize(path).toString();
if (!digest(content).equals(cache.getProperty(key))) {
sources.add(PlainText.builder()
.sourcePath(root.relativize(path))
.text(content)
.build());
}
} catch (Exception error) {
throw new IllegalStateException(path + ": " + error.getMessage(), error);
}
}
}
}
var context = new InMemoryExecutionContext(error -> {
throw new IllegalStateException("Formatting failed: " + error.getMessage(), error);
});
var results = new PalantirRecipe()
.run(new InMemoryLargeSourceSet(sources), context)
.getChangeset()
.getAllResults();
for (Result result : results) {
System.out.println((check ? "Needs formatting: " : "Formatted: ")
+ result.getAfter().getSourcePath());
}
if (check && !results.isEmpty()) {
throw new IllegalStateException("Run mvn -N exec:exec@format to apply Palantir formatting.");
}
// Complete parsing/formatting before writing any maintained file. Each replacement is atomic.
if (!check) {
for (Result result : results) {
Path destination = root.resolve(result.getAfter().getSourcePath());
Path temporary = Files.createTempFile(destination.getParent(), ".palantir-", ".tmp");
try {
Files.writeString(temporary, result.getAfter().printAll());
Files.setPosixFilePermissions(temporary, Files.getPosixFilePermissions(destination));
Files.move(
temporary,
destination,
StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
} finally {
Files.deleteIfExists(temporary);
}
}
}
for (SourceFile source : sources) {
cache.setProperty(
source.getSourcePath().toString(), digest(Files.readString(root.resolve(source.getSourcePath()))));
}
Files.createDirectories(cacheFile.getParent());
try (var output = Files.newOutputStream(cacheFile)) {
cache.store(output, "Successfully formatted content only");
}
System.out.println(
"Palantir " + args[1] + ": " + sources.size() + " Java files, " + results.size() + " differences");
}
static String digest(String value) throws Exception {
return HexFormat.of()
.formatHex(MessageDigest.getInstance("SHA-256")
.digest(value.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
}
public static final class PalantirRecipe extends Recipe {
public String getDisplayName() {
return "Palantir Java Format";
}
public String getDescription() {
return "Apply the pinned Palantir engine and its native import handling.";
}
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new PlainTextVisitor<ExecutionContext>() {
final Formatter formatter = Formatter.createFormatter(JavaFormatterOptions.builder()
.style(JavaFormatterOptions.Style.PALANTIR)
.formatJavadoc(false)
.build());
@Override
public PlainText visitText(PlainText text, ExecutionContext context) {
try {
return text.withText(formatter.formatSourceAndFixImports(text.getText()));
} catch (Exception | LinkageError error) {
throw new IllegalStateException(text.getSourcePath() + ": " + error, error);
}
}
};
}
}
}

View File

@@ -0,0 +1,21 @@
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<module name="TreeWalker">
<module name="TypeName"/>
<module name="MethodName"/>
<module name="MemberName"/>
<module name="ParameterName"/>
<module name="LocalVariableName"/>
<module name="LocalFinalVariableName"/>
<module name="StaticVariableName"/>
<module name="LambdaParameterName"/>
<module name="RecordComponentName"/>
<module name="LeftCurly"/>
<module name="CatchParameterName"/>
<module name="ConstantName"/>
<module name="PackageName"><property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/></module>
<module name="OneTopLevelClass"/>
<module name="AvoidStarImport"/>
<module name="NeedBraces"/>
</module>
</module>

View File

@@ -0,0 +1,16 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>nl.agilecc</groupId><artifactId>backend-standards</artifactId><version>1</version>
<properties><maven.compiler.release>25</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
<dependencies>
<dependency><groupId>jakarta.inject</groupId><artifactId>jakarta.inject-api</artifactId><version>2.0.1</version><scope>test</scope></dependency>
<dependency><groupId>net.sourceforge.pmd</groupId><artifactId>pmd-java</artifactId><version>7.28.0</version></dependency>
<dependency><groupId>com.puppycrawl.tools</groupId><artifactId>checkstyle</artifactId><version>12.3.1</version></dependency>
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.14.1</version><scope>test</scope></dependency>
</dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.16.0</version></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.6.1</version></plugin>
<plugin><groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId><version>3.6.3</version><configuration><mainClass>Standards</mainClass><arguments><argument>${project.basedir}/../../..</argument></arguments></configuration></plugin>
</plugins></build>
</project>

View File

@@ -0,0 +1,590 @@
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<String> findings = new ArrayList<>();
static final Map<Path, Set<Integer>> codeLines = new HashMap<>();
static final Set<String> ownedTypes = new HashSet<>();
static final Map<String, ASTTypeDeclaration> types = new HashMap<>();
static final List<ASTCompilationUnit> units = new ArrayList<>();
static Path root;
static String module;
record Waiver(String metric, String executable, double value, String reason) {}
static final List<Waiver> waivers = new ArrayList<>();
public static void main(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<Path> 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<Path> 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<Integer> 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 <T> List<T> all(Node node, Class<T> clazz) {
final List<T> 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<String> parameters = new ArrayList<>();
for (var component : compact.getEnclosingType().getRecordComponents())
parameters.add(component.getTypeMirror().toString());
return compact.getEnclosingType().getBinaryName() + "#<init>(" + String.join(",", parameters) + ")";
}
if (node instanceof ASTExecutableDeclaration executable) {
final List<String> parameters = new ArrayList<>();
for (var parameter : executable.getFormalParameters())
parameters.add(parameter.getTypeMirror().toString());
return executable.getEnclosingType().getBinaryName() + "#"
+ (executable instanceof ASTConstructorDeclaration ? "<init>" : executable.getName())
+ "(" + String.join(",", parameters) + ")";
}
return node instanceof ASTTypeDeclaration type ? type.getBinaryName() : location(node);
}
static Set<Integer> lines(JavaNode node) {
final Set<Integer> 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<Integer> ownLines = new HashSet<>();
final Set<Integer> 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<String> 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<Dependency> 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<String> 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<String> qualifiers = new HashSet<>();
for (var annotation : variable.getDeclaredAnnotations()) {
if (annotation.getBinaryName().equals("jakarta.inject.Named")
|| annotated(annotation.getAnnotationSymbol(), "jakarta.inject.Qualifier")) {
final Map<String, String> 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<Dependency> dependencies, Set<String> 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<Dependency> dependencies, Set<String> 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<Path> 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<String> 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 + ")");
}
}

View File

@@ -0,0 +1,372 @@
import static org.junit.jupiter.api.Assertions.*;
import java.nio.file.*;
import java.util.*;
import net.sourceforge.pmd.*;
import net.sourceforge.pmd.lang.*;
import net.sourceforge.pmd.lang.document.FileId;
import net.sourceforge.pmd.lang.java.ast.*;
import net.sourceforge.pmd.lang.rule.RuleSet;
import org.junit.jupiter.api.*;
class StandardsTest {
private List<String> analyze(String text) throws Exception {
Standards.root = Path.of("").toAbsolutePath();
Standards.module = "backend";
Standards.ownedTypes.clear();
Standards.findings.clear();
Standards.units.clear();
Standards.types.clear();
Standards.codeLines.clear();
final var config = new PMDConfiguration();
config.setThreads(1);
config.prependAuxClasspath(
Path.of(System.getProperty("java.home"), "lib/jrt-fs.jar").toString());
config.prependAuxClasspath(
System.getProperty("surefire.test.class.path", System.getProperty("java.class.path")));
config.setIgnoreIncrementalAnalysis(true);
config.setDefaultLanguageVersion(
LanguageRegistry.PMD.getLanguageById("java").getVersion("25"));
try (var analysis = PmdAnalysis.create(config)) {
analysis.addRuleSet(RuleSet.forSingleRule(new Standards.Measurements()));
analysis.files().addSourceFile(FileId.fromPath(Standards.root.resolve("Fixture.java")), text);
var report = analysis.performAnalysisAndCollectReport();
assertTrue(
report.getProcessingErrors().isEmpty(),
report.getProcessingErrors().toString());
}
return List.copyOf(Standards.findings);
}
private boolean contains(List<String> findings, String prefix, String metric) {
return findings.stream().anyMatch(s -> s.startsWith(prefix) && s.endsWith(metric));
}
@Test
void cyclomaticAndCognitiveBoundaries() throws Exception {
var at = analyze("class Fixture { public void run(final boolean b) { " + "if (b) {}".repeat(9) + "} }");
assertTrue(contains(at, "REVIEW", "cyclomatic=10.0"));
var over = analyze("class Fixture { public void run(final boolean b) { " + "if (b) {}".repeat(10) + "} }");
assertTrue(contains(over, "BLOCK", "cyclomatic=11.0"));
assertTrue(contains(
analyze("class Fixture { public void run(final boolean b) {" + "if (b) {}".repeat(15) + "} }"),
"REVIEW",
"cognitive=15.0"));
assertTrue(contains(
analyze("class Fixture { public void run(final boolean b) {" + "if (b) {}".repeat(16) + "} }"),
"BLOCK",
"cognitive=16.0"));
}
@Test
void methodAndConstructorParameterBoundariesAndImplicitRecords() throws Exception {
assertTrue(contains(
analyze("class Fixture { public void run(final int a, final int b, final int c, final int d,"
+ " final int e) {} }"),
"REVIEW",
"parameters=5.0"));
assertTrue(contains(
analyze("class Fixture { public void run(final int a, final int b, final int c, final int d,"
+ " final int e, final int f) {} }"),
"BLOCK",
"parameters=6.0"));
var implicit = analyze("record Fixture(int a, int b, int c, int d, int e, int f, int g, int h) {}");
assertFalse(implicit.stream().anyMatch(s -> s.contains("parameters=")));
var compact = analyze(
"record Fixture(int a, int b, int c, int d, int e, int f, int g, int h) { public Fixture" + " {} }");
assertTrue(contains(compact, "BLOCK", "parameters=8.0"));
}
@Test
void physicalLinesExcludeCommentsAndCountBraces() throws Exception {
var findings = analyze("class Fixture {\n"
+ "public void run() {\n"
+ "// comment\n"
+ "/* multi\n"
+ " comment */\n\n"
+ "System.out.println(1); // code\n"
+ "}\n"
+ "}");
assertTrue(contains(findings, "METRIC", "executableLines=3.0"));
assertTrue(contains(findings, "METRIC", "classLines=5.0"));
assertTrue(contains(
analyze("class Fixture {\npublic void run() {\n" + "System.out.println(1);\n".repeat(48) + "}\n}"),
"REVIEW",
"executableLines=50.0"));
assertTrue(contains(
analyze("class Fixture {\npublic void run() {\n" + "System.out.println(1);\n".repeat(49) + "}\n}"),
"BLOCK",
"executableLines=51.0"));
}
@Test
void nestingElseIfCatchSwitchLambdaAndNestedMethods() throws Exception {
var chain = analyze(
"class Fixture { public void run(final boolean b) { if (b) {} else if (b) {} else if (b)" + " {} } }");
assertTrue(contains(chain, "METRIC", "nesting=1.0"));
var nested = analyze("class Fixture { public void run(final boolean b) { if (b) { while (b) { switch (1) {"
+ " default -> { try {} catch (Exception e) { Runnable r = () -> { if (b) {} }; } } }"
+ " } } } }");
assertTrue(contains(nested, "BLOCK", "nesting=5.0"));
var separate = analyze("class Fixture { public void run(final boolean b) { if (b) { class Inner { public void"
+ " go() { if (b) {} } } } } }");
assertFalse(separate.stream().anyMatch(s -> s.endsWith("nesting=2.0")));
}
@Test
void lambdaComplexityContributesAndNestedClassDoesNot() throws Exception {
var findings = analyze("class Fixture { public void run(final boolean b) { Runnable r = () -> { if (b) {} };"
+ " class Inner { public void nested() { if (b) {} } } } }");
assertEquals(
2, findings.stream().filter(s -> s.endsWith("cyclomatic=2.0")).count(), findings.toString());
}
@Test
void varAndFinalRules() throws Exception {
var findings =
analyze("class Fixture { public void run(int p) { var a = new Object(); var b = (Object) a; var c"
+ " = name(); int count = 0; count++; } private String name() { return \"x\"; } }");
assertEquals(
1, findings.stream().filter(s -> s.contains("var requires")).count());
assertTrue(findings.stream().anyMatch(s -> s.contains("requires final: p")));
assertFalse(findings.stream().anyMatch(s -> s.contains("requires final: count")));
}
@Test
void patternBindingsRequireFinalOnlyWhenAssignedOnce() throws Exception {
var findings = analyze("""
record Pair(Object key, Object value) {}
class Fixture {
public void run(final Object input) {
if (input instanceof String unchanged) { System.out.println(unchanged); }
if (input instanceof final String protectedValue) { System.out.println(protectedValue); }
if (input instanceof String reassigned) { reassigned = reassigned.trim(); System.out.println(reassigned); }
switch (input) {
case String switched -> System.out.println(switched);
default -> {}
}
if (input instanceof Pair(var key, var value)) {
System.out.println(key);
System.out.println(value);
}
}
}
""");
for (String binding : List.of("unchanged", "switched", "key", "value"))
assertTrue(findings.stream().anyMatch(s -> s.contains("requires final: " + binding)), findings.toString());
for (String binding : List.of("protectedValue", "reassigned"))
assertFalse(findings.stream().anyMatch(s -> s.contains("requires final: " + binding)), findings.toString());
assertFalse(findings.stream().anyMatch(s -> s.contains("var requires")), findings.toString());
}
@Test
void inheritanceCountsOnlyProjectSuperclasses() throws Exception {
var findings = analyze("class A {} class B extends A {} class C extends B {} class D extends C {} class Fixture"
+ " extends D {} class External extends java.util.ArrayList<String> {}");
assertTrue(contains(findings, "REVIEW", "ownSuperclassDepth=3.0"));
assertTrue(contains(findings, "BLOCK", "ownSuperclassDepth=4.0"));
assertTrue(contains(findings, "METRIC", "ownSuperclassDepth=0.0"));
}
@Test
void injectionUsesResolvedTypesQualifiersInheritedMembersAndImplicitConstructors() throws Exception {
var findings = analyze("""
class Parent {
@jakarta.inject.Inject private String inherited;
@jakarta.inject.Inject public void setValue(final Integer value) {}
}
@jakarta.inject.Singleton class Fixture extends Parent {
private final String value;
public Fixture(@jakarta.inject.Named("a") final String first,
@jakarta.inject.Named("a") final String repeated,
@jakarta.inject.Named("b") final String second) { value = first; }
@jakarta.inject.Inject private String repeatedInherited;
}
""");
assertTrue(contains(findings, "METRIC", "injectedDependencies=4.0"), findings.toString());
String fields = "";
for (int i = 0; i < 7; i++)
fields += "@jakarta.inject.Inject @jakarta.inject.Named(\"" + i + "\") private String d" + i + ";";
assertTrue(contains(analyze("class Fixture {" + fields + "}"), "REVIEW", "injectedDependencies=7.0"));
assertTrue(contains(
analyze("class Fixture {" + fields + "@jakarta.inject.Inject private Integer eighth;}"),
"BLOCK",
"injectedDependencies=8.0"));
}
@Test
void fanoutCountsDistinctJdkTypesAndPublicMethodsExcludeOnlyTrueAccessors() throws Exception {
var findings = analyze("""
class Fixture {
private String name;
private java.util.List<String> values;
private java.util.List<String> repeated;
public String getName() { return name; }
public void setName(final String value) { this.name = value; }
public String getOther() { return name; }
public String getValidatedName() { if (name == null) { throw new IllegalStateException(); } return name; }
public static int calculate() { return 1; }
}
""");
assertTrue(contains(findings, "METRIC", "fanOut=3.0"), findings.toString());
assertTrue(contains(findings, "METRIC", "publicMethods=3.0"), findings.toString());
}
@Test
void commentedAccessorsRemainTrivialWhileAdditionalWorkStillCounts() throws Exception {
var findings = analyze("""
class Fixture {
private String name;
private String trimmed;
private String logged;
private String calculated;
private String appended;
public String getName() { /* Intent comment. */ return name; }
public void setName(final String value) { this./* Field comment. */name = value; }
public String name() { // Read the stored value.
return this.name;
}
public void setName(final String value, final boolean validate) { this.name = value; }
public void setTrimmed(final String value) { this.trimmed = value.trim(); }
public String getLogged() { System.out.println(logged); return logged; }
public String getCalculated() { return calculated.trim(); }
public void setAppended(final String value) { this.appended += value; }
}
""");
assertTrue(contains(findings, "METRIC", "publicMethods=5.0"), findings.toString());
}
@Test
void classLengthBoundaryAndNestedTypeLines() throws Exception {
assertTrue(contains(
analyze("class Fixture {\n" + "private int field;\n".repeat(498) + "}"), "REVIEW", "classLines=500.0"));
assertTrue(contains(
analyze("class Fixture {\n" + "private int field;\n".repeat(499) + "}"), "BLOCK", "classLines=501.0"));
var separate = analyze("class Fixture {\nprivate class Inner {\nprivate int value;\n}\nprivate int other;\n}");
assertEquals(
2, separate.stream().filter(s -> s.endsWith("classLines=3.0")).count());
assertTrue(contains(analyze("class Fixture { private class Inner {} }"), "METRIC", "classLines=1.0"));
}
@Test
void switchBooleanAndThrowTreatmentIsPinned() throws Exception {
var findings = analyze("class Fixture { public int run(final boolean a, final boolean b) { if (a && b) { throw"
+ " new IllegalStateException(); } return switch (1) { case 0, 1 -> 1; default -> 2;"
+ " }; } }");
assertTrue(contains(findings, "METRIC", "cyclomatic=6.0"), findings.toString());
}
private Path project(String production, String tests) throws Exception {
Path directory = Files.createTempDirectory("s7-fixture-");
Files.writeString(directory.resolve("engineering.properties"), "BACKEND_MODULE=backend\n");
Path prod = directory.resolve("backend/src/main/java");
Path test = directory.resolve("backend/src/test/java");
Files.createDirectories(prod);
Files.createDirectories(test);
Path config = directory.resolve("generated/tooling/standards/checkstyle.xml");
Files.createDirectories(config.getParent());
Files.copy(Path.of(System.getProperty("basedir"), "checkstyle.xml"), config);
if (production != null) Files.writeString(prod.resolve("Fixture.java"), production);
if (tests != null) Files.writeString(test.resolve("ExampleTest.java"), tests);
return directory;
}
@Test
void checksFailWithoutRewritingSourcesAndExposeReviewSignals() throws Exception {
String source = "package example;\n"
+ "import java.util.*;\n"
+ "class Fixture { public void run(int x) { if (x == 0) System.out.println(x); } }";
Path directory = project(source, null);
assertThrows(IllegalStateException.class, () -> Standards.main(new String[] {directory.toString()}));
assertEquals(source, Files.readString(directory.resolve("backend/src/main/java/Fixture.java")));
String report = Files.readString(directory.resolve("backend/target/standards/report.txt"));
assertTrue(report.contains("form of import"), report);
assertTrue(report.contains("must use '{}'s"), report);
assertTrue(report.contains("requires final: x"), report);
}
@Test
void productionCpdCountsEveryOccurrenceAndTestClonesDoNotGate() throws Exception {
String method = "public void first() {\n" + "System.out.println(123);\n".repeat(30) + "}\n";
String source = "package example;\nclass Fixture {\n" + method + method.replace("first", "second") + "}";
Path directory = project(source, null);
assertThrows(IllegalStateException.class, () -> Standards.main(new String[] {directory.toString()}));
String report = Files.readString(directory.resolve("backend/target/standards/report.txt"));
assertTrue(report.contains("BLOCK productionDuplication=") && report.contains("(64/67)"), report);
assertTrue(report.lines().filter(l -> l.startsWith("CLONE")).count() >= 2, report);
Path testProject =
project("package example; class Fixture {}", source.replace("class Fixture", "class ExampleTest"));
Standards.main(new String[] {testProject.toString()});
String testReport = Files.readString(testProject.resolve("backend/target/standards/report.txt"));
assertTrue(testReport.contains("productionDuplication=0.0%"), testReport);
}
@Test
void parserFailureNamesTheAffectedFileAndFails() throws Exception {
Path directory = project("package example; class Fixture { public void broken( }", null);
Exception failure = assertThrows(Exception.class, () -> Standards.main(new String[] {directory.toString()}));
assertTrue(
failure.toString().contains("Fixture.java")
|| Standards.findings.stream().anyMatch(f -> f.contains("Fixture.java")),
failure.toString());
}
@Test
void exactCoverageGateAcceptsEightyPercentAndRejectsRoundedNearMiss() throws Exception {
Path directory = Files.createTempDirectory("s7-coverage-");
Path xml = directory.resolve("report.xml");
Path helper = Path.of(System.getProperty("basedir"), "../coverage/Coverage.java")
.normalize();
for (long[] counts : List.of(new long[] {4, 1, 0}, new long[] {799999, 200001, 1}, new long[] {0, 0, 1})) {
Files.writeString(
xml,
"<report><counter type=\"LINE\" covered=\"" + counts[0] + "\" missed=\"" + counts[1]
+ "\"/></report>");
Process process = new ProcessBuilder(
Path.of(System.getProperty("java.home"), "bin/java").toString(),
helper.toString(),
xml.toString())
.redirectErrorStream(true)
.redirectOutput(directory.resolve("output.log").toFile())
.start();
assertEquals(counts[2], process.waitFor(), Files.readString(directory.resolve("output.log")));
}
}
@Test
void overridesAndImplicitRecordInjectionAreMeasured() throws Exception {
var override = analyze("class Fixture extends java.util.ArrayList<String> { public void clear() {} }");
assertTrue(override.stream().anyMatch(f -> f.contains("require @Override")), override.toString());
var record = analyze("@jakarta.inject.Singleton record Fixture(String one, String repeated, Integer two) {}");
assertTrue(contains(record, "METRIC", "injectedDependencies=2.0"), record.toString());
var inherited = analyze("class Parent<T> { @jakarta.inject.Inject private T dependency; } class Fixture extends"
+ " Parent<String> { @jakarta.inject.Inject private String repeated; }");
assertEquals(
2,
inherited.stream()
.filter(f -> f.endsWith("injectedDependencies=1.0"))
.count(),
inherited.toString());
}
@Test
void waiverMatchesOnlyTheExactMetricSignatureAndRetainedValue() throws Exception {
Standards.waivers.add(new Standards.Waiver(
"parameters",
"Fixture#run(int,int,int,int,int,int)",
6,
"Test fixture agreement: preserve an example signature"));
try {
var waived = analyze("class Fixture { public void run(final int a, final int b, final int c, final int d,"
+ " final int e, final int f) {} }");
assertTrue(
waived.stream().anyMatch(f -> f.startsWith("WAIVER") && f.contains("parameters=6.0")),
waived.toString());
var changed = analyze("class Fixture { public void run(final int a, final int b, final int c, final int d,"
+ " final int e, final int f, final int g) {} }");
assertTrue(contains(changed, "BLOCK", "parameters=7.0"));
} finally {
Standards.waivers.clear();
}
}
}