S19: consume versioned engineering resources and move landscape documentation
All checks were successful
Build, test and publish / verify (push) Successful in 8m54s
All checks were successful
Build, test and publish / verify (push) Successful in 8m54s
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,598 +0,0 @@
|
||||
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;
|
||||
|
||||
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();
|
||||
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("agiledancers-be/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("agiledancers-be/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("agiledancers-be/target/standards-classpath.txt");
|
||||
if (Files.exists(cp))
|
||||
config.prependAuxClasspath(Files.readString(cp)
|
||||
+ File.pathSeparator
|
||||
+ root.resolve("agiledancers-be/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("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("agiledancers-be/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("agiledancers-be/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("agiledancers-be/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 + ")");
|
||||
}
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
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.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-");
|
||||
Path prod = directory.resolve("agiledancers-be/src/main/java");
|
||||
Path test = directory.resolve("agiledancers-be/src/test/java");
|
||||
Files.createDirectories(prod);
|
||||
Files.createDirectories(test);
|
||||
Path config = directory.resolve("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("agiledancers-be/src/main/java/Fixture.java")));
|
||||
String report =
|
||||
Files.readString(directory.resolve("agiledancers-be/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("agiledancers-be/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("agiledancers-be/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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user