599 lines
26 KiB
Java
599 lines
26 KiB
Java
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 + ")");
|
|
}
|
|
}
|