Files
agiledancers/generated/tooling/standards/src/main/java/Standards.java

597 lines
30 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;
static String module;
record Waiver(String metric, String executable, double value, String reason) {}
static final List<Waiver> waivers = new ArrayList<>();
/**
* Checks configured backend sources and writes standards reports without changing maintained code.
*
* @param args the consumer repository root as the sole argument
* @throws Exception if configuration, resolution or parsing fails, or a blocking violation is found
*/
public static void main(final String... args) throws Exception {
findings.clear();
codeLines.clear();
types.clear();
units.clear();
ownedTypes.clear();
root = Path.of(args[0]).toAbsolutePath().normalize();
final var settings = new Properties();
try (var input = Files.newInputStream(root.resolve("engineering.properties"))) {
settings.load(input);
}
module = settings.getProperty("BACKEND_MODULE");
if (module == null || module.isBlank()) throw new IllegalArgumentException("BACKEND_MODULE is required");
waivers.clear();
final Path waiverFile = root.resolve("tooling/standards/waivers.tsv");
if (Files.exists(waiverFile))
for (String line : Files.readAllLines(waiverFile)) {
if (line.isBlank() || line.startsWith("#")) continue;
final String[] fields = line.split("\t", -1);
if (fields.length != 4 || fields[0].contains("*") || fields[1].contains("*") || fields[3].isBlank())
throw new IllegalArgumentException("Invalid exact waiver in " + waiverFile + ": " + line);
waivers.add(new Waiver(fields[0], fields[1], Double.parseDouble(fields[2]), fields[3]));
}
final Path output = root.resolve(module + "/target/standards");
Files.createDirectories(output);
final List<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 + ")");
}
}