diff --git a/README.md b/README.md index 5ab686ff..c7efb6f2 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,11 @@ The graphs generated in the report will look similar to this one: ![image info](./RefactorFirst_Sample_Report.png) ## Please Note: Java 11 (or newer) required to run RefactorFirst -The change to require Java 11 is needed to address vulnerability CVE-2023-4759 in JGit **Java 21 codebase analysis is supported!** +The change to require Java 11 is needed to address vulnerability CVE-2023-4759 in JGit +Please use a recent JDK release of the Java version you are using. +If you use an old JDK release of your chosen Java version, you may encounter issues during analysis. + ## There are several ways to run the analysis on your codebase: diff --git a/change-proneness-ranker/pom.xml b/change-proneness-ranker/pom.xml index c2e2fc29..47d1b835 100644 --- a/change-proneness-ranker/pom.xml +++ b/change-proneness-ranker/pom.xml @@ -5,7 +5,7 @@ org.hjug.refactorfirst refactor-first - 0.6.3-SNAPSHOT + 0.7.0-SNAPSHOT org.hjug.refactorfirst.changepronenessranker diff --git a/circular-reference-detector/LICENSE b/circular-reference-detector/LICENSE deleted file mode 100644 index 0df1eda7..00000000 --- a/circular-reference-detector/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2023 Nikhil Pereira - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/circular-reference-detector/README.md b/circular-reference-detector/README.md deleted file mode 100644 index a9fbfba4..00000000 --- a/circular-reference-detector/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Java Circular Reference Detector - -Tool to create graph images of cyclic references in a java project. - -Usage : -- Pass two command line arguments: - - File path of source directory of the java project. - - Output directory path to store images of the circular reference graphs. - -By [Ideacrest Solutions](https://www.ideacrestsolutions.com/) diff --git a/circular-reference-detector/pom.xml b/circular-reference-detector/pom.xml deleted file mode 100644 index 00a8634f..00000000 --- a/circular-reference-detector/pom.xml +++ /dev/null @@ -1,63 +0,0 @@ - - 4.0.0 - - - org.hjug.refactorfirst - refactor-first - 0.6.3-SNAPSHOT - - - org.hjug.refactorfirst.circularreferencedetector - circular-reference-detector - - Tool to help detecting circular references by parsing a java project. - - - - org.projectlombok - lombok - ${lombok.version} - - - org.slf4j - slf4j-api - 1.7.26 - - - org.jgrapht - jgrapht-core - 1.5.2 - - - com.github.javaparser - javaparser-symbol-solver-core - 3.26.1 - - - org.junit.jupiter - junit-jupiter-api - 5.9.0 - test - - - org.junit.jupiter - junit-jupiter-engine - 5.9.0 - test - - - - - - - - maven-surefire-plugin - 2.22.2 - - - maven-failsafe-plugin - 2.22.2 - - - - \ No newline at end of file diff --git a/circular-reference-detector/src/main/java/org/hjug/app/CircularReferenceDetectorApp.java b/circular-reference-detector/src/main/java/org/hjug/app/CircularReferenceDetectorApp.java deleted file mode 100644 index 71497697..00000000 --- a/circular-reference-detector/src/main/java/org/hjug/app/CircularReferenceDetectorApp.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.hjug.app; - -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import org.hjug.cycledetector.CircularReferenceChecker; -import org.hjug.parser.JavaProjectParser; -import org.jgrapht.Graph; -import org.jgrapht.alg.flow.GusfieldGomoryHuCutTree; -import org.jgrapht.graph.AsSubgraph; -import org.jgrapht.graph.AsUndirectedGraph; -import org.jgrapht.graph.DefaultWeightedEdge; - -/** - * Command line application to detect circular references in a java project. - * Takes two arguments : source folder of java project, directory to store images of the circular reference graphs. - * - * @author nikhil_pereira - */ -public class CircularReferenceDetectorApp { - - private Map renderedSubGraphs = new HashMap<>(); - - // public static void main(String[] args) { - // CircularReferenceDetectorApp circularReferenceDetectorApp = new CircularReferenceDetectorApp(); - // circularReferenceDetectorApp.launchApp(args); - // } - - /** - * Parses source project files and creates a graph of class references of the java project. - * Detects cycles in the class references graph and stores the cycle graphs in the given output directory - * - * @param args - */ - public void launchApp(String[] args) { - if (!validateArgs(args)) { - printCommandUsage(); - } else { - String srcDirectoryPath = args[0]; - JavaProjectParser javaProjectParser = new JavaProjectParser(); - try { - Graph classReferencesGraph = - javaProjectParser.getClassReferences(srcDirectoryPath); - detectAndStoreCyclesInDirectory(classReferencesGraph); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - private void detectAndStoreCyclesInDirectory(Graph classReferencesGraph) { - CircularReferenceChecker circularReferenceChecker = new CircularReferenceChecker(); - Map> cyclesForEveryVertexMap = - circularReferenceChecker.detectCycles(classReferencesGraph); - cyclesForEveryVertexMap.forEach((vertex, subGraph) -> { - int vertexCount = subGraph.vertexSet().size(); - int edgeCount = subGraph.edgeSet().size(); - if (vertexCount > 1 && edgeCount > 1 && !isDuplicateSubGraph(subGraph, vertex)) { - renderedSubGraphs.put(vertex, subGraph); - System.out.println("Vertex: " + vertex + " vertex count: " + vertexCount + " edge count: " + edgeCount); - GusfieldGomoryHuCutTree gusfieldGomoryHuCutTree = - new GusfieldGomoryHuCutTree<>(new AsUndirectedGraph<>(subGraph)); - double minCut = gusfieldGomoryHuCutTree.calculateMinCut(); - System.out.println("Min cut weight: " + minCut); - Set minCutEdges = gusfieldGomoryHuCutTree.getCutEdges(); - System.out.println("Minimum Cut Edges:"); - for (DefaultWeightedEdge minCutEdge : minCutEdges) { - System.out.println(minCutEdge); - } - } - }); - } - - private boolean isDuplicateSubGraph(AsSubgraph subGraph, String vertex) { - if (!renderedSubGraphs.isEmpty()) { - for (AsSubgraph renderedSubGraph : renderedSubGraphs.values()) { - if (renderedSubGraph.vertexSet().size() == subGraph.vertexSet().size() - && renderedSubGraph.edgeSet().size() - == subGraph.edgeSet().size() - && renderedSubGraph.vertexSet().contains(vertex)) { - return true; - } - } - } - - return false; - } - - private boolean validateArgs(String[] args) { - return args.length == 2; - } - - private void printCommandUsage() { - System.out.println("Usage:\n" - + "argument 1 : file path of source directory of the java project." - + "argument 2 : output directory path to store images of the circular reference graphs."); - } -} diff --git a/circular-reference-detector/src/main/java/org/hjug/cycledetector/CircularReferenceChecker.java b/circular-reference-detector/src/main/java/org/hjug/cycledetector/CircularReferenceChecker.java deleted file mode 100644 index 19f2b49d..00000000 --- a/circular-reference-detector/src/main/java/org/hjug/cycledetector/CircularReferenceChecker.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.hjug.cycledetector; - -import java.util.HashMap; -import java.util.Map; -import org.jgrapht.Graph; -import org.jgrapht.alg.cycle.CycleDetector; -import org.jgrapht.graph.AsSubgraph; -import org.jgrapht.graph.DefaultWeightedEdge; - -public class CircularReferenceChecker { - - /** - * Detects cycles in the classReferencesGraph parameter - * and stores the cycles of a class as a subgraph in a Map - * - * @param classReferencesGraph - * @return a Map of Class and its Cycle Graph - */ - public Map> detectCycles( - Graph classReferencesGraph) { - Map> cyclesForEveryVertexMap = new HashMap<>(); - CycleDetector cycleDetector = new CycleDetector<>(classReferencesGraph); - cycleDetector.findCycles().forEach(v -> { - AsSubgraph subGraph = - new AsSubgraph<>(classReferencesGraph, cycleDetector.findCyclesContainingVertex(v)); - cyclesForEveryVertexMap.put(v, subGraph); - }); - return cyclesForEveryVertexMap; - } -} diff --git a/circular-reference-detector/src/main/java/org/hjug/parser/JavaProjectParser.java b/circular-reference-detector/src/main/java/org/hjug/parser/JavaProjectParser.java deleted file mode 100644 index f450d77d..00000000 --- a/circular-reference-detector/src/main/java/org/hjug/parser/JavaProjectParser.java +++ /dev/null @@ -1,143 +0,0 @@ -package org.hjug.parser; - -import com.github.javaparser.StaticJavaParser; -import com.github.javaparser.ast.CompilationUnit; -import com.github.javaparser.ast.body.FieldDeclaration; -import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.body.Parameter; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import lombok.extern.slf4j.Slf4j; -import org.jgrapht.Graph; -import org.jgrapht.graph.DefaultDirectedWeightedGraph; -import org.jgrapht.graph.DefaultWeightedEdge; - -@Slf4j -public class JavaProjectParser { - - /** - * Given a java source directory return a graph of class references - * @param srcDirectory - * @return - * @throws IOException - */ - public Graph getClassReferences(String srcDirectory) throws IOException { - Graph classReferencesGraph = - new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); - if (srcDirectory == null || srcDirectory.isEmpty()) { - throw new IllegalArgumentException(); - } else { - List classNames = getClassNames(srcDirectory); - try (Stream filesStream = Files.walk(Paths.get(srcDirectory))) { - filesStream - .filter(path -> path.getFileName().toString().endsWith(".java")) - .forEach(path -> { - log.info("Parsing {}", path); - List types = getInstanceVarTypes(classNames, path.toFile()); - types.addAll(getMethodArgumentTypes(classNames, path.toFile())); - if (!types.isEmpty()) { - String className = - getClassName(path.getFileName().toString()); - classReferencesGraph.addVertex(className); - types.forEach(classReferencesGraph::addVertex); - types.forEach(type -> { - if (!classReferencesGraph.containsEdge(className, type)) { - classReferencesGraph.addEdge(className, type); - } else { - DefaultWeightedEdge edge = classReferencesGraph.getEdge(className, type); - classReferencesGraph.setEdgeWeight( - edge, classReferencesGraph.getEdgeWeight(edge) + 1); - } - }); - } - }); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - } - - return classReferencesGraph; - } - - /** - * Get instance variables types of a java source file using java parser - * @param classNamesToFilterBy - only add instance variable types which have these class names as type - * @param file - * @return - */ - private List getInstanceVarTypes(List classNamesToFilterBy, File javaSrcFile) { - CompilationUnit compilationUnit; - try { - compilationUnit = StaticJavaParser.parse(javaSrcFile); - return compilationUnit.findAll(FieldDeclaration.class).stream() - .map(f -> f.getVariables().get(0).getType()) - .filter(v -> !v.isPrimitiveType()) - .map(Object::toString) - .filter(classNamesToFilterBy::contains) - .collect(Collectors.toList()); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - return new ArrayList<>(); - } - - /** - * Get parameter types of methods declared in a java source file using java parser - * @param classNamesToFilterBy - only add types which have these class names as type - * @param file - * @return - */ - private List getMethodArgumentTypes(List classNamesToFilterBy, File javaSrcFile) { - CompilationUnit compilationUnit; - try { - compilationUnit = StaticJavaParser.parse(javaSrcFile); - return compilationUnit.findAll(MethodDeclaration.class).stream() - .flatMap(f -> f.getParameters().stream() - .map(Parameter::getType) - .filter(type -> !type.isPrimitiveType()) - .collect(Collectors.toList()) - .stream()) - .map(Object::toString) - .filter(classNamesToFilterBy::contains) - .collect(Collectors.toList()); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - return new ArrayList<>(); - } - - /** - * Get all java classes in a source directory - * - * @param srcDirectory - * @return - * @throws IOException - */ - private List getClassNames(String srcDirectory) throws IOException { - try (Stream filesStream = Files.walk(Paths.get(srcDirectory))) { - return filesStream - .map(path -> path.getFileName().toString()) - .filter(fileName -> fileName.endsWith(".java")) - .map(this::getClassName) - .collect(Collectors.toList()); - } - } - - /** - * Extract class name from java file name - * Example : MyJavaClass.java becomes MyJavaClass - * - * @param javaFileName - * @return - */ - private String getClassName(String javaFileName) { - return javaFileName.substring(0, javaFileName.indexOf('.')); - } -} diff --git a/circular-reference-detector/src/test/java/org/hjug/parser/JavaProjectParserTests.java b/circular-reference-detector/src/test/java/org/hjug/parser/JavaProjectParserTests.java deleted file mode 100644 index c09f47e1..00000000 --- a/circular-reference-detector/src/test/java/org/hjug/parser/JavaProjectParserTests.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.hjug.parser; - -import static org.junit.jupiter.api.Assertions.*; - -import java.io.File; -import java.io.IOException; -import org.jgrapht.Graph; -import org.jgrapht.graph.DefaultWeightedEdge; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -class JavaProjectParserTests { - - JavaProjectParser sutJavaProjectParser = new JavaProjectParser(); - - @DisplayName("When source directory input param is empty or null throw IllegalArgumentException.") - @Test - void parseSourceDirectoryEmptyTest() { - Assertions.assertThrows(IllegalArgumentException.class, () -> sutJavaProjectParser.getClassReferences("")); - Assertions.assertThrows(IllegalArgumentException.class, () -> sutJavaProjectParser.getClassReferences(null)); - } - - @DisplayName("Given a valid source directory input parameter return a valid graph.") - @Test - void parseSourceDirectoryTest() throws IOException { - File srcDirectory = new File("src/test/resources/javaSrcDirectory"); - Graph classReferencesGraph = - sutJavaProjectParser.getClassReferences(srcDirectory.getAbsolutePath()); - assertNotNull(classReferencesGraph); - assertEquals(5, classReferencesGraph.vertexSet().size()); - assertEquals(7, classReferencesGraph.edgeSet().size()); - assertTrue(classReferencesGraph.containsVertex("A")); - assertTrue(classReferencesGraph.containsVertex("B")); - assertTrue(classReferencesGraph.containsVertex("C")); - assertTrue(classReferencesGraph.containsVertex("D")); - assertTrue(classReferencesGraph.containsVertex("E")); - assertTrue(classReferencesGraph.containsEdge("A", "B")); - assertTrue(classReferencesGraph.containsEdge("B", "C")); - assertTrue(classReferencesGraph.containsEdge("C", "A")); - assertTrue(classReferencesGraph.containsEdge("C", "E")); - assertTrue(classReferencesGraph.containsEdge("D", "A")); - assertTrue(classReferencesGraph.containsEdge("D", "C")); - assertTrue(classReferencesGraph.containsEdge("E", "D")); - - // confirm edge weight calculations - assertEquals(1, getEdgeWeight(classReferencesGraph, "A", "B")); - assertEquals(2, getEdgeWeight(classReferencesGraph, "E", "D")); - } - - private static double getEdgeWeight( - Graph classReferencesGraph, String sourceVertex, String targetVertex) { - return classReferencesGraph.getEdgeWeight(classReferencesGraph.getEdge(sourceVertex, targetVertex)); - } -} diff --git a/circular-reference-detector/src/test/resources/testOutputDirectory/graphtestGraph.png b/circular-reference-detector/src/test/resources/testOutputDirectory/graphtestGraph.png deleted file mode 100644 index 811aea4c..00000000 Binary files a/circular-reference-detector/src/test/resources/testOutputDirectory/graphtestGraph.png and /dev/null differ diff --git a/cli/pom.xml b/cli/pom.xml index f9967c6f..53fd7e2a 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -4,7 +4,7 @@ org.hjug.refactorfirst refactor-first - 0.6.3-SNAPSHOT + 0.7.0-SNAPSHOT jar @@ -31,13 +31,15 @@ org.slf4j slf4j-simple - 2.0.7 + + + + com.google.guava + guava org.apache.maven maven-core - 3.9.2 - compile diff --git a/cli/src/main/java/org/hjug/refactorfirst/ReportCommand.java b/cli/src/main/java/org/hjug/refactorfirst/ReportCommand.java index ec448110..71a7cc4d 100644 --- a/cli/src/main/java/org/hjug/refactorfirst/ReportCommand.java +++ b/cli/src/main/java/org/hjug/refactorfirst/ReportCommand.java @@ -21,9 +21,43 @@ public class ReportCommand implements Callable { @Option( names = {"-d", "--details"}, + defaultValue = "false", description = "Show detailed report") private boolean showDetails; + @Option( + names = {"-eac", "--edge-analysis-count"}, + defaultValue = "50", + description = "Back Edge Analysis Count") + protected int backEdgeAnalysisCount; + + @Option( + names = {"-c", "--analyze-cycles"}, + defaultValue = "true", + description = "Analyze Cycles") + private boolean analyzeCycles; + + @Option( + names = {"-m", "--minify-html"}, + defaultValue = "false", + description = "Minify HTML output") + private boolean minifiyHtml; + + @Option( + names = {"-xt", "--exclude-tests"}, + defaultValue = "true", + description = "Exclude tests from analysis") + private boolean excludeTests; + + /** + * The test source directory containing test class sources. + */ + @Option( + names = {"-tsd", "--output"}, + description = + "Test source directory. Defaults to test/src or test\\src based on your OS. Default is intentionally generic.") + private String testSourceDirectory; + @Option( names = {"-p", "--project"}, description = "Project name") @@ -61,11 +95,31 @@ public Integer call() { switch (reportType) { case SIMPLE_HTML: SimpleHtmlReport simpleHtmlReport = new SimpleHtmlReport(); - simpleHtmlReport.execute(showDetails, projectName, projectVersion, outputDirectory, baseDir); + simpleHtmlReport.execute( + backEdgeAnalysisCount, + analyzeCycles, + showDetails, + minifiyHtml, + excludeTests, + testSourceDirectory, + projectName, + projectVersion, + baseDir, + outputDirectory); return 0; case HTML: HtmlReport htmlReport = new HtmlReport(); - htmlReport.execute(showDetails, projectName, projectVersion, outputDirectory, baseDir); + htmlReport.execute( + backEdgeAnalysisCount, + analyzeCycles, + showDetails, + minifiyHtml, + excludeTests, + testSourceDirectory, + projectName, + projectVersion, + baseDir, + outputDirectory); return 0; case JSON: JsonReportExecutor jsonReportExecutor = new JsonReportExecutor(); diff --git a/codebase-graph-builder/pom.xml b/codebase-graph-builder/pom.xml new file mode 100644 index 00000000..e37089fe --- /dev/null +++ b/codebase-graph-builder/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + + org.hjug.refactorfirst + refactor-first + 0.7.0-SNAPSHOT + + + org.hjug.refactorfirst.codebasegraphbuilder + codebase-graph-builder + + + + + org.openrewrite.recipe + rewrite-recipe-bom + 3.4.0 + pom + import + + + + + + + org.slf4j + slf4j-api + + + org.jgrapht + jgrapht-core + + + org.openrewrite + rewrite-java-21 + + + org.openrewrite + rewrite-java-17 + + + org.openrewrite + rewrite-java-11 + + + org.openrewrite + rewrite-java + + + + + io.quarkus.gizmo + gizmo + 1.9.0 + + + org.openrewrite + rewrite-core + + + \ No newline at end of file diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/CodebaseGraphDTO.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/CodebaseGraphDTO.java new file mode 100644 index 00000000..4933eb05 --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/CodebaseGraphDTO.java @@ -0,0 +1,15 @@ +package org.hjug.graphbuilder; + +import java.util.Map; +import lombok.Data; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultWeightedEdge; + +@Data +public class CodebaseGraphDTO { + + private final Graph classReferencesGraph; + private final Graph packageReferencesGraph; + // used for looking up files where classes reside + private final Map classToSourceFilePathMapping; +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/JavaGraphBuilder.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/JavaGraphBuilder.java new file mode 100644 index 00000000..102ae245 --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/JavaGraphBuilder.java @@ -0,0 +1,114 @@ +package org.hjug.graphbuilder; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.extern.slf4j.Slf4j; +import org.hjug.graphbuilder.visitor.JavaMethodDeclarationVisitor; +import org.hjug.graphbuilder.visitor.JavaVariableTypeVisitor; +import org.hjug.graphbuilder.visitor.JavaVisitor; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultDirectedWeightedGraph; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.openrewrite.ExecutionContext; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; + +@Slf4j +public class JavaGraphBuilder { + + /** + * Given a java source directory return a graph of class references + * + * @param srcDirectory + * @return + * @throws IOException + */ + public Graph getClassReferences( + String srcDirectory, boolean excludeTests, String testSourceDirectory) throws IOException { + Graph classReferencesGraph; + if (srcDirectory == null || srcDirectory.isEmpty()) { + throw new IllegalArgumentException(); + } else { + classReferencesGraph = processWithOpenRewrite(srcDirectory, excludeTests, testSourceDirectory) + .getClassReferencesGraph(); + } + + return classReferencesGraph; + } + + private CodebaseGraphDTO processWithOpenRewrite(String srcDir, boolean excludeTests, String testSourceDirectory) + throws IOException { + File srcDirectory = new File(srcDir); + + JavaParser javaParser = JavaParser.fromJavaVersion().build(); + ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); + + final Graph classReferencesGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + + final Graph packageReferencesGraph = + new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); + + final JavaVisitor javaVisitor = + new JavaVisitor<>(classReferencesGraph, packageReferencesGraph); + final JavaVariableTypeVisitor javaVariableTypeVisitor = + new JavaVariableTypeVisitor<>(classReferencesGraph, packageReferencesGraph); + final JavaMethodDeclarationVisitor javaMethodDeclarationVisitor = + new JavaMethodDeclarationVisitor<>(classReferencesGraph, packageReferencesGraph); + + try (Stream pathStream = Files.walk(Paths.get(srcDirectory.getAbsolutePath()))) { + List list; + if (excludeTests) { + list = pathStream + .filter(file -> !file.toString().contains(testSourceDirectory)) + .collect(Collectors.toList()); + } else { + list = pathStream.collect(Collectors.toList()); + } + + javaParser + .parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx) + .forEach(cu -> { + javaVisitor.visit(cu, ctx); + javaVariableTypeVisitor.visit(cu, ctx); + javaMethodDeclarationVisitor.visit(cu, ctx); + }); + } + + removeClassesNotInCodebase(javaVisitor.getPackagesInCodebase(), classReferencesGraph); + + return new CodebaseGraphDTO( + classReferencesGraph, packageReferencesGraph, javaVisitor.getClassToSourceFilePathMapping()); + } + + // remove node if package not in codebase + void removeClassesNotInCodebase( + Set packagesInCodebase, Graph classReferencesGraph) { + + // collect nodes to remove + Set classesToRemove = new HashSet<>(); + for (String classFqn : classReferencesGraph.vertexSet()) { + if (!packagesInCodebase.contains(getPackage(classFqn))) { + classesToRemove.add(classFqn); + } + } + + classReferencesGraph.removeAllVertices(classesToRemove); + } + + String getPackage(String fqn) { + // handle no package + if (!fqn.contains(".")) { + return ""; + } + + int lastIndex = fqn.lastIndexOf("."); + return fqn.substring(0, lastIndex); + } +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/FqnCapturingProcessor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/FqnCapturingProcessor.java new file mode 100644 index 00000000..475b03e4 --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/FqnCapturingProcessor.java @@ -0,0 +1,59 @@ +package org.hjug.graphbuilder.visitor; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.openrewrite.java.tree.J; + +public interface FqnCapturingProcessor { + + default J.ClassDeclaration captureClassDeclarations( + J.ClassDeclaration classDecl, Map> fqns) { + // get class fqn (including "$") + String fqn = classDecl.getType().getFullyQualifiedName(); + + String currentPackage = getPackage(fqn); + String className = getClassName(fqn); + Map classesInPackage = fqns.getOrDefault(currentPackage, new HashMap<>()); + + if (className.contains("$")) { + String normalizedClassName = className.replace('$', '.'); + List parts = Arrays.asList(normalizedClassName.split("\\.")); + for (int i = 0; i < parts.size(); i++) { + String key = String.join(".", parts.subList(i, parts.size())); + classesInPackage.put(key, currentPackage + "." + normalizedClassName); + } + } else { + classesInPackage.put(className, fqn); + } + + fqns.put(currentPackage, classesInPackage); + return classDecl; + } + + default String getPackage(String fqn) { + // handle no package + if (!fqn.contains(".")) { + return ""; + } + + int lastIndex = fqn.lastIndexOf("."); + return fqn.substring(0, lastIndex); + } + + /** + * + * @param fqn + * @return Class name (including "$") after last period in FQN + */ + default String getClassName(String fqn) { + // handle no package + if (!fqn.contains(".")) { + return fqn; + } + + int lastIndex = fqn.lastIndexOf("."); + return fqn.substring(lastIndex + 1); + } +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitor.java new file mode 100644 index 00000000..a87c393d --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitor.java @@ -0,0 +1,118 @@ +package org.hjug.graphbuilder.visitor; + +import java.util.List; +import lombok.Getter; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.SimpleDirectedWeightedGraph; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.tree.*; + +public class JavaClassDeclarationVisitor

extends JavaIsoVisitor

implements TypeProcessor { + + private final JavaMethodInvocationVisitor methodInvocationVisitor; + private final JavaNewClassVisitor newClassVisitor; + + @Getter + private Graph classReferencesGraph = + new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + + public JavaClassDeclarationVisitor() { + methodInvocationVisitor = new JavaMethodInvocationVisitor(classReferencesGraph); + newClassVisitor = new JavaNewClassVisitor(classReferencesGraph); + } + + public JavaClassDeclarationVisitor(Graph classReferencesGraph) { + this.classReferencesGraph = classReferencesGraph; + methodInvocationVisitor = new JavaMethodInvocationVisitor(classReferencesGraph); + newClassVisitor = new JavaNewClassVisitor(classReferencesGraph); + } + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, P p) { + J.ClassDeclaration classDeclaration = super.visitClassDeclaration(classDecl, p); + + JavaType.FullyQualified type = classDeclaration.getType(); + String owningFqn = type.getFullyQualifiedName(); + + processType(owningFqn, type); + + TypeTree extendsTypeTree = classDeclaration.getExtends(); + if (null != extendsTypeTree) { + processType(owningFqn, extendsTypeTree.getType()); + } + + List implementsTypeTree = classDeclaration.getImplements(); + if (null != implementsTypeTree) { + for (TypeTree typeTree : implementsTypeTree) { + processType(owningFqn, typeTree.getType()); + } + } + + for (J.Annotation leadingAnnotation : classDeclaration.getLeadingAnnotations()) { + processAnnotation(owningFqn, leadingAnnotation); + } + + if (null != classDeclaration.getTypeParameters()) { + for (J.TypeParameter typeParameter : classDeclaration.getTypeParameters()) { + processTypeParameter(owningFqn, typeParameter); + } + } + + // process method invocations and lambda invocations + processInvocations(classDeclaration); + + return classDeclaration; + } + + private void processInvocations(J.ClassDeclaration classDeclaration) { + JavaType.FullyQualified type = classDeclaration.getType(); + String owningFqn = type.getFullyQualifiedName(); + + for (Statement statement : classDeclaration.getBody().getStatements()) { + if (statement instanceof J.Block) { + processBlock((J.Block) statement, owningFqn); + } + if (statement instanceof J.MethodDeclaration) { + J.MethodDeclaration methodDeclaration = (J.MethodDeclaration) statement; + processBlock(methodDeclaration.getBody(), owningFqn); + } + } + } + + private void processBlock(J.Block block, String owningFqn) { + if (null != block && null != block.getStatements()) { + for (Statement statementInBlock : block.getStatements()) { + if (statementInBlock instanceof J.MethodInvocation) { + J.MethodInvocation methodInvocation = (J.MethodInvocation) statementInBlock; + methodInvocationVisitor.visitMethodInvocation(owningFqn, methodInvocation); + } else if (statementInBlock instanceof J.Lambda) { + J.Lambda lambda = (J.Lambda) statementInBlock; + processType(owningFqn, lambda.getType()); + } else if (statementInBlock instanceof J.NewClass) { + J.NewClass newClass = (J.NewClass) statementInBlock; + newClassVisitor.visitNewClass(owningFqn, newClass); + } else if (statementInBlock instanceof J.Return) { + J.Return returnStmt = (J.Return) statementInBlock; + visitReturn(owningFqn, returnStmt); + } + } + } + } + + public J.Return visitReturn(String owningFqn, J.Return visitedReturn) { + Expression expression = visitedReturn.getExpression(); + if (expression instanceof J.MethodInvocation) { + J.MethodInvocation methodInvocation = (J.MethodInvocation) expression; + methodInvocationVisitor.visitMethodInvocation(owningFqn, methodInvocation); + } else if (expression instanceof J.NewClass) { + J.NewClass newClass = (J.NewClass) expression; + newClassVisitor.visitNewClass(owningFqn, newClass); + } else if (expression instanceof J.Lambda) { + J.Lambda lambda = (J.Lambda) expression; + processType(owningFqn, lambda.getType()); + } + + return visitedReturn; + } +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaFqnCapturingVisitor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaFqnCapturingVisitor.java new file mode 100644 index 00000000..7bb536fe --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaFqnCapturingVisitor.java @@ -0,0 +1,76 @@ +package org.hjug.graphbuilder.visitor; + +import java.util.*; +import lombok.Getter; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.tree.J; + +/** + * Captures Fully Qualified Names (FQN) of classes as they will be imported in import statements. + * fqns map that is populated by this visitor is used to resolve Generic types. + * + * @param

+ */ +@Getter +public class JavaFqnCapturingVisitor

extends JavaIsoVisitor

{ + + // consider using ConcurrentHashMap to scale performance + // package -> name, FQN + private final Map> fqnMap = new HashMap<>(); + private final Set fqns = new HashSet<>(); + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, P p) { + captureClassDeclarations(classDecl, fqnMap); + return classDecl; + } + + J.ClassDeclaration captureClassDeclarations(J.ClassDeclaration classDecl, Map> fqnMap) { + // get class fqn (including "$") + String fqn = classDecl.getType().getFullyQualifiedName(); + fqns.add(fqn); + + /* String currentPackage = getPackage(fqn); + String className = getClassName(fqn); + Map classesInPackage = fqnMap.getOrDefault(currentPackage, new HashMap<>()); + + if (className.contains("$")) { + String normalizedClassName = className.replace('$', '.'); + List parts = Arrays.asList(normalizedClassName.split("\\.")); + for (int i = 0; i < parts.size(); i++) { + String key = String.join(".", parts.subList(i, parts.size())); + classesInPackage.put(key, currentPackage + "." + normalizedClassName); + } + } else { + classesInPackage.put(className, fqn); + } + + fqnMap.put(currentPackage, classesInPackage);*/ + return classDecl; + } + + String getPackage(String fqn) { + // handle no package + if (!fqn.contains(".")) { + return ""; + } + + int lastIndex = fqn.lastIndexOf("."); + return fqn.substring(0, lastIndex); + } + + /** + * + * @param fqn + * @return Class name (including "$") after last period in FQN + */ + String getClassName(String fqn) { + // handle no package + if (!fqn.contains(".")) { + return fqn; + } + + int lastIndex = fqn.lastIndexOf("."); + return fqn.substring(lastIndex + 1); + } +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitor.java new file mode 100644 index 00000000..1775473b --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitor.java @@ -0,0 +1,78 @@ +package org.hjug.graphbuilder.visitor; + +import java.util.List; +import lombok.Getter; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.SimpleDirectedWeightedGraph; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.NameTree; +import org.openrewrite.java.tree.TypeTree; + +public class JavaMethodDeclarationVisitor

extends JavaIsoVisitor

implements TypeProcessor { + + @Getter + private Graph classReferencesGraph = + new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + + @Getter + private Graph packageReferencesGraph = + new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + + public JavaMethodDeclarationVisitor() {} + + public JavaMethodDeclarationVisitor( + Graph classReferencesGraph, + Graph packageReferencesGraph) { + this.classReferencesGraph = classReferencesGraph; + this.packageReferencesGraph = packageReferencesGraph; + } + + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, P p) { + J.MethodDeclaration methodDeclaration = super.visitMethodDeclaration(method, p); + + JavaType.Method methodType = methodDeclaration.getMethodType(); + if (null == methodType) { + // sometimes methodType is null, not sure why... + return methodDeclaration; + } + + String owner = methodType.getDeclaringType().getFullyQualifiedName(); + + // if returnTypeExpression is null, a constructor declaration is being processed + TypeTree returnTypeExpression = methodDeclaration.getReturnTypeExpression(); + if (returnTypeExpression != null) { + JavaType returnType = returnTypeExpression.getType(); + + // skip primitive variable declarations + if (!(returnType instanceof JavaType.Primitive)) { + processType(owner, returnType); + } + } + + for (J.Annotation leadingAnnotation : methodDeclaration.getLeadingAnnotations()) { + processType(owner, leadingAnnotation.getType()); + } + + if (null != methodDeclaration.getTypeParameters()) { + for (J.TypeParameter typeParameter : methodDeclaration.getTypeParameters()) { + processTypeParameter(owner, typeParameter); + } + } + + // don't need to capture parameter declarations + // they are captured in JavaVariableTypeVisitor + + List throwz = methodDeclaration.getThrows(); + if (null != throwz && !throwz.isEmpty()) { + for (NameTree thrown : throwz) { + processType(owner, thrown.getType()); + } + } + + return methodDeclaration; + } +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaMethodInvocationVisitor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaMethodInvocationVisitor.java new file mode 100644 index 00000000..70f81833 --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaMethodInvocationVisitor.java @@ -0,0 +1,37 @@ +package org.hjug.graphbuilder.visitor; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; + +// See RemoveMethodInvocationsVisitor for other visitor methods to override +// Custom visitor - not extending IsoVisitor on purpose since it does not provide caller information + +@RequiredArgsConstructor +@Getter +public class JavaMethodInvocationVisitor implements TypeProcessor { + + private final Graph classReferencesGraph; + + public J.MethodInvocation visitMethodInvocation(String invokingFqn, J.MethodInvocation methodInvocation) { + // getDeclaringType() returns the type that declared the method being invoked + JavaType.Method methodType = methodInvocation.getMethodType(); + // sometimes methodType is null - not sure why + if (null != methodType && null != methodType.getDeclaringType()) { + processType(invokingFqn, methodType.getDeclaringType()); + } + + if (null != methodInvocation.getTypeParameters() + && !methodInvocation.getTypeParameters().isEmpty()) { + for (Expression typeParameter : methodInvocation.getTypeParameters()) { + processType(invokingFqn, typeParameter.getType()); + } + } + + return methodInvocation; + } +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitor.java new file mode 100644 index 00000000..1be95649 --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitor.java @@ -0,0 +1,20 @@ +package org.hjug.graphbuilder.visitor; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.openrewrite.java.tree.J; + +@RequiredArgsConstructor +@Getter +public class JavaNewClassVisitor implements TypeProcessor { + + private final Graph classReferencesGraph; + + public J.NewClass visitNewClass(String invokingFqn, J.NewClass newClass) { + processType(invokingFqn, newClass.getType()); + // TASK: process initializer block??? + return newClass; + } +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitor.java new file mode 100644 index 00000000..117681cb --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitor.java @@ -0,0 +1,110 @@ +package org.hjug.graphbuilder.visitor; + +import java.util.List; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.SimpleDirectedWeightedGraph; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.tree.*; + +@Slf4j +public class JavaVariableTypeVisitor

extends JavaIsoVisitor

implements TypeProcessor { + + @Getter + private Graph classReferencesGraph = + new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + + @Getter + private Graph packageReferencesGraph = + new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + + private final JavaNewClassVisitor newClassVisitor; + private final JavaMethodInvocationVisitor methodInvocationVisitor; + + public JavaVariableTypeVisitor() { + newClassVisitor = new JavaNewClassVisitor(classReferencesGraph); + methodInvocationVisitor = new JavaMethodInvocationVisitor(classReferencesGraph); + } + + public JavaVariableTypeVisitor( + Graph classReferencesGraph, + Graph packageReferencesGraph) { + this.classReferencesGraph = classReferencesGraph; + this.packageReferencesGraph = packageReferencesGraph; + newClassVisitor = new JavaNewClassVisitor(classReferencesGraph); + methodInvocationVisitor = new JavaMethodInvocationVisitor(classReferencesGraph); + } + + @Override + public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, P p) { + J.VariableDeclarations variableDeclarations = super.visitVariableDeclarations(multiVariable, p); + + /* + * Handles + * java.lang.NullPointerException: Cannot invoke "org.openrewrite.java.tree.JavaType$Variable.getOwner()" + * because the return value of + * "org.openrewrite.java.tree.J$VariableDeclarations$NamedVariable.getVariableType()" is null + */ + List variables = variableDeclarations.getVariables(); + if (null == variables || variables.isEmpty() || null == variables.get(0).getVariableType()) { + return variableDeclarations; + } + + JavaType owner = variables.get(0).getVariableType().getOwner(); + String ownerFqn = ""; + + if (owner instanceof JavaType.Method) { + JavaType.Method m = (JavaType.Method) owner; + // log.debug("Method owner: " + m.getDeclaringType().getFullyQualifiedName()); + ownerFqn = m.getDeclaringType().getFullyQualifiedName(); + } else if (owner instanceof JavaType.Class) { + JavaType.Class c = (JavaType.Class) owner; + // log.debug("Method owner: " + c.getFullyQualifiedName()); + ownerFqn = c.getFullyQualifiedName(); + } + + log.debug("*************************"); + log.debug("Processing " + ownerFqn + ":" + variableDeclarations); + log.debug("*************************"); + + TypeTree typeTree = variableDeclarations.getTypeExpression(); + + JavaType javaType; + if (null != typeTree) { + javaType = typeTree.getType(); + } else { + return variableDeclarations; + } + + // TODO: getAllAnnotations() is deprecated - need to call + // AnnotationService.getAllAnnotations() but not sure which one yet + // but I'm not sure how to get a cursor + // All types, including primitives can be annotated + for (J.Annotation annotation : variableDeclarations.getAllAnnotations()) { + processAnnotation(ownerFqn, annotation); + } + + // skip primitive variable declarations + if (javaType instanceof JavaType.Primitive) { + return variableDeclarations; + } + + processType(ownerFqn, javaType); + + // process variable instantiation if present + for (J.VariableDeclarations.NamedVariable variable : variables) { + Expression initializer = variable.getInitializer(); + if (null != initializer && null != initializer.getType() && initializer instanceof J.MethodInvocation) { + J.MethodInvocation methodInvocation = (J.MethodInvocation) initializer; + methodInvocationVisitor.visitMethodInvocation(ownerFqn, methodInvocation); + } else if (null != initializer && null != initializer.getType() && initializer instanceof J.NewClass) { + J.NewClass newClassType = (J.NewClass) initializer; + newClassVisitor.visitNewClass(ownerFqn, newClassType); + } + } + + return variableDeclarations; + } +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVisitor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVisitor.java new file mode 100644 index 00000000..a37ce8c4 --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVisitor.java @@ -0,0 +1,64 @@ +package org.hjug.graphbuilder.visitor; + +import java.util.*; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.tree.*; + +@Slf4j +public class JavaVisitor

extends JavaIsoVisitor

implements TypeProcessor { + + // used to keep track of what packages are in the codebase + // used to remove the nodes that are not in the codebase + @Getter + private final Set packagesInCodebase = new HashSet<>(); + + // used for looking up files where classes reside + @Getter + private final Map classToSourceFilePathMapping = new HashMap<>(); + + @Getter + private final Graph classReferencesGraph; + + @Getter + private final Graph packageReferencesGraph; + + private final JavaClassDeclarationVisitor

javaClassDeclarationVisitor; + + public JavaVisitor( + Graph classReferencesGraph, + Graph packageReferencesGraph) { + this.classReferencesGraph = classReferencesGraph; + this.packageReferencesGraph = packageReferencesGraph; + javaClassDeclarationVisitor = new JavaClassDeclarationVisitor<>(classReferencesGraph); + } + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, P p) { + return javaClassDeclarationVisitor.visitClassDeclaration(classDecl, p); + } + + // Map each class to its source file + @Override + public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, P p) { + J.CompilationUnit compilationUnit = super.visitCompilationUnit(cu, p); + + J.Package packageDeclaration = compilationUnit.getPackageDeclaration(); + if (null == packageDeclaration) { + return compilationUnit; + } + + packagesInCodebase.add(packageDeclaration.getPackageName()); + + for (J.ClassDeclaration aClass : compilationUnit.getClasses()) { + classToSourceFilePathMapping.put( + aClass.getType().getFullyQualifiedName(), + compilationUnit.getSourcePath().toUri().toString()); + } + + return compilationUnit; + } +} diff --git a/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/TypeProcessor.java b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/TypeProcessor.java new file mode 100644 index 00000000..5f2e83da --- /dev/null +++ b/codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/TypeProcessor.java @@ -0,0 +1,142 @@ +package org.hjug.graphbuilder.visitor; + +import lombok.extern.slf4j.Slf4j; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.TypeTree; + +public interface TypeProcessor { + + @Slf4j + final class LogHolder {} + + /** + * @param ownerFqn The FQN that is the source of the relationship + * @param javaType The type that is used/referenced by the source of the relationship + */ + default void processType(String ownerFqn, JavaType javaType) { + if (javaType instanceof JavaType.Class) { + processType(ownerFqn, (JavaType.Class) javaType); + } else if (javaType instanceof JavaType.Parameterized) { + // A --> A + processType(ownerFqn, (JavaType.Parameterized) javaType); + } else if (javaType instanceof JavaType.GenericTypeVariable) { + // T t; + processType(ownerFqn, (JavaType.GenericTypeVariable) javaType); + } else if (javaType instanceof JavaType.Array) { + processType(ownerFqn, (JavaType.Array) javaType); + } + } + + private void processType(String ownerFqn, JavaType.Parameterized parameterized) { + // List> --> A + processAnnotations(ownerFqn, parameterized); + LogHolder.log.debug("Parameterized type FQN : " + parameterized.getFullyQualifiedName()); + addType(ownerFqn, parameterized.getFullyQualifiedName()); + + LogHolder.log.debug("Nested Parameterized type parameters: " + parameterized.getTypeParameters()); + for (JavaType parameter : parameterized.getTypeParameters()) { + processType(ownerFqn, parameter); + } + } + + private void processType(String ownerFqn, JavaType.Array arrayType) { + // D[] --> D + LogHolder.log.debug("Array Element type: " + arrayType.getElemType()); + processType(ownerFqn, arrayType.getElemType()); + } + + private void processType(String ownerFqn, JavaType.GenericTypeVariable typeVariable) { + LogHolder.log.debug("Type parameter type name: " + typeVariable.getName()); + + for (JavaType bound : typeVariable.getBounds()) { + if (bound instanceof JavaType.Class) { + addType(((JavaType.Class) bound).getFullyQualifiedName(), ownerFqn); + } else if (bound instanceof JavaType.Parameterized) { + addType(((JavaType.Parameterized) bound).getFullyQualifiedName(), ownerFqn); + } + } + } + + private void processType(String ownerFqn, JavaType.Class classType) { + processAnnotations(ownerFqn, classType); + LogHolder.log.debug("Class type FQN: " + classType.getFullyQualifiedName()); + addType(ownerFqn, classType.getFullyQualifiedName()); + } + + private void processAnnotations(String ownerFqn, JavaType.FullyQualified fullyQualified) { + if (!fullyQualified.getAnnotations().isEmpty()) { + for (JavaType.FullyQualified annotation : fullyQualified.getAnnotations()) { + String annotationFqn = annotation.getFullyQualifiedName(); + LogHolder.log.debug("Extra Annotation FQN: " + annotationFqn); + addType(ownerFqn, annotationFqn); + } + } + } + + default void processAnnotation(String ownerFqn, J.Annotation annotation) { + if (annotation.getType() instanceof JavaType.Unknown) { + return; + } + + JavaType.Class type = (JavaType.Class) annotation.getType(); + if (null != type) { + String annotationFqn = type.getFullyQualifiedName(); + LogHolder.log.debug("Variable Annotation FQN: " + annotationFqn); + addType(ownerFqn, annotationFqn); + + if (null != annotation.getArguments()) { + for (Expression argument : annotation.getArguments()) { + processType(ownerFqn, argument.getType()); + } + } + } + } + + default void processTypeParameter(String ownerFqn, J.TypeParameter typeParameter) { + + if (null != typeParameter.getBounds()) { + for (TypeTree bound : typeParameter.getBounds()) { + processType(ownerFqn, bound.getType()); + } + } + + if (!typeParameter.getAnnotations().isEmpty()) { + for (J.Annotation annotation : typeParameter.getAnnotations()) { + processAnnotation(ownerFqn, annotation); + } + } + } + + default Graph getPackageReferencesGraph() { + return null; + } + + Graph getClassReferencesGraph(); + + /** + * + * @param ownerFqn The FQN that is the source of the relationship + * @param typeFqn The FQN of the type that is being used by the source + */ + default void addType(String ownerFqn, String typeFqn) { + if (ownerFqn.equals(typeFqn)) return; + + Graph classReferencesGraph = getClassReferencesGraph(); + + classReferencesGraph.addVertex(ownerFqn); + classReferencesGraph.addVertex(typeFqn); + + if (!classReferencesGraph.containsEdge(ownerFqn, typeFqn)) { + classReferencesGraph.addEdge(ownerFqn, typeFqn); + } else { + DefaultWeightedEdge edge = classReferencesGraph.getEdge(ownerFqn, typeFqn); + classReferencesGraph.setEdgeWeight(edge, classReferencesGraph.getEdgeWeight(edge) + 1); + } + } + + // TODO: process packages +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/JavaGraphBuilderTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/JavaGraphBuilderTest.java new file mode 100644 index 00000000..324c855a --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/JavaGraphBuilderTest.java @@ -0,0 +1,104 @@ +package org.hjug.graphbuilder; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class JavaGraphBuilderTest { + + JavaGraphBuilder javaGraphBuilder = new JavaGraphBuilder(); + + @DisplayName("When source directory input param is empty or null throw IllegalArgumentException.") + @Test + void parseSourceDirectoryEmptyTest() { + Assertions.assertThrows( + IllegalArgumentException.class, () -> javaGraphBuilder.getClassReferences("", false, "")); + Assertions.assertThrows( + IllegalArgumentException.class, () -> javaGraphBuilder.getClassReferences(null, false, "")); + } + + @DisplayName("Given a valid source directory input parameter return a valid graph.") + @Test + void parseSourceDirectoryTest() throws IOException { + File srcDirectory = new File("src/test/resources/javaSrcDirectory"); + Graph classReferencesGraph = + javaGraphBuilder.getClassReferences(srcDirectory.getAbsolutePath(), false, ""); + assertNotNull(classReferencesGraph); + assertEquals(5, classReferencesGraph.vertexSet().size()); + assertEquals(7, classReferencesGraph.edgeSet().size()); + assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.A")); + assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.B")); + assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.C")); + assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.D")); + assertTrue(classReferencesGraph.containsVertex("com.ideacrest.parser.testclasses.E")); + assertTrue(classReferencesGraph.containsEdge( + "com.ideacrest.parser.testclasses.A", "com.ideacrest.parser.testclasses.B")); + assertTrue(classReferencesGraph.containsEdge( + "com.ideacrest.parser.testclasses.B", "com.ideacrest.parser.testclasses.C")); + assertTrue(classReferencesGraph.containsEdge( + "com.ideacrest.parser.testclasses.C", "com.ideacrest.parser.testclasses.A")); + assertTrue(classReferencesGraph.containsEdge( + "com.ideacrest.parser.testclasses.C", "com.ideacrest.parser.testclasses.E")); + assertTrue(classReferencesGraph.containsEdge( + "com.ideacrest.parser.testclasses.D", "com.ideacrest.parser.testclasses.A")); + assertTrue(classReferencesGraph.containsEdge( + "com.ideacrest.parser.testclasses.D", "com.ideacrest.parser.testclasses.C")); + assertTrue(classReferencesGraph.containsEdge( + "com.ideacrest.parser.testclasses.E", "com.ideacrest.parser.testclasses.D")); + + // confirm edge weight calculations + assertEquals( + 1, + getEdgeWeight( + classReferencesGraph, + "com.ideacrest.parser.testclasses.A", + "com.ideacrest.parser.testclasses.B")); + assertEquals( + 2, + getEdgeWeight( + classReferencesGraph, + "com.ideacrest.parser.testclasses.E", + "com.ideacrest.parser.testclasses.D")); + } + + private static double getEdgeWeight( + Graph classReferencesGraph, String sourceVertex, String targetVertex) { + return classReferencesGraph.getEdgeWeight(classReferencesGraph.getEdge(sourceVertex, targetVertex)); + } + + @Test + void removeClassesNotInCodebase() throws IOException { + File srcDirectory = new File("src/test/resources/javaSrcDirectory"); + Graph classReferencesGraph = + javaGraphBuilder.getClassReferences(srcDirectory.getAbsolutePath(), false, ""); + + classReferencesGraph.addVertex("org.favioriteoss.FunClass"); + classReferencesGraph.addVertex("org.favioriteoss.AnotherFunClass"); + + DefaultWeightedEdge edge1 = + classReferencesGraph.addEdge("com.ideacrest.parser.testclasses.A", "org.favioriteoss.FunClass"); + DefaultWeightedEdge edge2 = + classReferencesGraph.addEdge("com.ideacrest.parser.testclasses.A", "org.favioriteoss.AnotherFunClass"); + + assertTrue(classReferencesGraph.containsVertex("org.favioriteoss.FunClass")); + assertTrue(classReferencesGraph.containsVertex("org.favioriteoss.AnotherFunClass")); + + Set packagesInCodebase = new HashSet<>(); + packagesInCodebase.add("com.ideacrest.parser.testclasses"); + + javaGraphBuilder.removeClassesNotInCodebase(packagesInCodebase, classReferencesGraph); + + assertFalse(classReferencesGraph.containsVertex("org.favioriteoss.FunClass")); + assertFalse(classReferencesGraph.containsVertex("org.favioriteoss.AnotherFunClass")); + assertFalse(classReferencesGraph.containsEdge(edge1)); + assertFalse(classReferencesGraph.containsEdge(edge2)); + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitorTest.java new file mode 100644 index 00000000..1cf523a1 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitorTest.java @@ -0,0 +1,62 @@ +package org.hjug.graphbuilder.visitor; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.openrewrite.ExecutionContext; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; + +class JavaClassDeclarationVisitorTest { + + @Test + void visitClasses() throws IOException { + + File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses"); + + org.openrewrite.java.JavaParser javaParser = + JavaParser.fromJavaVersion().build(); + ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); + + JavaClassDeclarationVisitor javaVariableCapturingVisitor = + new JavaClassDeclarationVisitor<>(); + + List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList()); + javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> { + javaVariableCapturingVisitor.visit(cu, ctx); + }); + + Assertions.assertTrue(javaVariableCapturingVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.A")); + Assertions.assertTrue(javaVariableCapturingVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.B")); + Assertions.assertTrue(javaVariableCapturingVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.C")); + // false because it doesn't reference any other classes + Assertions.assertTrue(javaVariableCapturingVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.D")); + Assertions.assertTrue(javaVariableCapturingVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.MyAnnotation")); + // false because the class declaration doesn't reference any other classes + Assertions.assertFalse(javaVariableCapturingVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.E")); + Assertions.assertTrue(javaVariableCapturingVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.F")); + Assertions.assertTrue(javaVariableCapturingVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.G")); + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaFqnCapturingVisitorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaFqnCapturingVisitorTest.java new file mode 100644 index 00000000..5ceb8534 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaFqnCapturingVisitorTest.java @@ -0,0 +1,69 @@ +package org.hjug.graphbuilder.visitor; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.openrewrite.ExecutionContext; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; + +@Disabled +class JavaFqnCapturingVisitorTest { + + @Test + void visitClasses() throws IOException { + + File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses"); + + org.openrewrite.java.JavaParser javaParser = + JavaParser.fromJavaVersion().build(); + ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); + + JavaFqnCapturingVisitor javaFqnCapturingVisitor = new JavaFqnCapturingVisitor(); + + List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList()); + javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> { + javaFqnCapturingVisitor.visit(cu, ctx); + }); + + Map> fqns = javaFqnCapturingVisitor.getFqnMap(); + Map processed = fqns.get("org.hjug.graphbuilder.visitor.testclasses"); + Assertions.assertEquals("org.hjug.graphbuilder.visitor.testclasses.A", processed.get("A")); + Assertions.assertEquals( + "org.hjug.graphbuilder.visitor.testclasses.A.InnerClass", processed.get("A.InnerClass")); + Assertions.assertEquals("org.hjug.graphbuilder.visitor.testclasses.A.InnerClass", processed.get("InnerClass")); + Assertions.assertEquals( + "org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner", + processed.get("A.InnerClass.InnerInner")); + Assertions.assertEquals( + "org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner", + processed.get("InnerClass.InnerInner")); + Assertions.assertEquals( + "org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner", processed.get("InnerInner")); + Assertions.assertEquals( + "org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner.MegaInner", + processed.get("A.InnerClass.InnerInner.MegaInner")); + Assertions.assertEquals( + "org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner.MegaInner", + processed.get("InnerClass.InnerInner.MegaInner")); + Assertions.assertEquals( + "org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner.MegaInner", + processed.get("InnerInner.MegaInner")); + Assertions.assertEquals( + "org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner.MegaInner", + processed.get("MegaInner")); + Assertions.assertEquals( + "org.hjug.graphbuilder.visitor.testclasses.A.StaticInnerClass", processed.get("A.StaticInnerClass")); + Assertions.assertEquals( + "org.hjug.graphbuilder.visitor.testclasses.A.StaticInnerClass", processed.get("StaticInnerClass")); + Assertions.assertEquals("org.hjug.graphbuilder.visitor.testclasses.NonPublic", processed.get("NonPublic")); + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitorTest.java new file mode 100644 index 00000000..3e20d16f --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitorTest.java @@ -0,0 +1,50 @@ +package org.hjug.graphbuilder.visitor; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.openrewrite.ExecutionContext; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; + +class JavaMethodDeclarationVisitorTest { + + @Test + void visitMethodDeclarations() throws IOException { + + File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses"); + + org.openrewrite.java.JavaParser javaParser = + JavaParser.fromJavaVersion().build(); + ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); + + JavaMethodDeclarationVisitor methodDeclarationVisitor = new JavaMethodDeclarationVisitor<>(); + + List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList()); + javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> { + methodDeclarationVisitor.visit(cu, ctx); + }); + + methodDeclarationVisitor.getClassReferencesGraph(); + + Assertions.assertTrue(methodDeclarationVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.A")); + + // TODO: Assert stuff + /* Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.A")); + Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.B")); + Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.C")); + Assertions.assertFalse(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.D")); + Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.MyAnnotation")); + Assertions.assertFalse(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.E")); + Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.F")); + Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex("org.hjug.javaVariableVisitorTestClasses.G"));*/ + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodInvocationVisitorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodInvocationVisitorTest.java new file mode 100644 index 00000000..1f92a05b --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodInvocationVisitorTest.java @@ -0,0 +1,62 @@ +package org.hjug.graphbuilder.visitor; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.SimpleDirectedWeightedGraph; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.openrewrite.ExecutionContext; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; + +class JavaMethodInvocationVisitorTest { + + @Test + void visitMethodInvocations() throws IOException { + + File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation"); + + JavaParser javaParser = JavaParser.fromJavaVersion().build(); + ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); + + Graph classReferencesGraph = + new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + + Graph packageReferencesGraph = + new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + + JavaClassDeclarationVisitor classDeclarationVisitor = + new JavaClassDeclarationVisitor<>(classReferencesGraph); + JavaVariableTypeVisitor variableTypeVisitor = + new JavaVariableTypeVisitor<>(classReferencesGraph, packageReferencesGraph); + + List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList()); + javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> { + classDeclarationVisitor.visit(cu, ctx); + variableTypeVisitor.visit(cu, ctx); + }); + + Graph graph = classDeclarationVisitor.getClassReferencesGraph(); + Assertions.assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.methodInvocation.A")); + Assertions.assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.methodInvocation.B")); + Assertions.assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.methodInvocation.C")); + + Assertions.assertEquals( + 3, + graph.getEdgeWeight(graph.getEdge( + "org.hjug.graphbuilder.visitor.testclasses.methodInvocation.A", + "org.hjug.graphbuilder.visitor.testclasses.methodInvocation.B"))); + Assertions.assertEquals( + 3, + graph.getEdgeWeight(graph.getEdge( + "org.hjug.graphbuilder.visitor.testclasses.methodInvocation.A", + "org.hjug.graphbuilder.visitor.testclasses.methodInvocation.C"))); + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitorTest.java new file mode 100644 index 00000000..8612b12b --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitorTest.java @@ -0,0 +1,58 @@ +package org.hjug.graphbuilder.visitor; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.SimpleDirectedWeightedGraph; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.openrewrite.ExecutionContext; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; + +public class JavaNewClassVisitorTest { + + @Test + void visitNewClass() throws IOException { + + File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass"); + + JavaParser javaParser = JavaParser.fromJavaVersion().build(); + ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); + + Graph classReferencesGraph = + new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + + Graph packageReferencesGraph = + new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + + JavaClassDeclarationVisitor classDeclarationVisitor = + new JavaClassDeclarationVisitor<>(classReferencesGraph); + JavaVariableTypeVisitor variableTypeVisitor = + new JavaVariableTypeVisitor<>(classReferencesGraph, packageReferencesGraph); + + List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList()); + javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> { + classDeclarationVisitor.visit(cu, ctx); + variableTypeVisitor.visit(cu, ctx); + }); + + Graph graph = variableTypeVisitor.getClassReferencesGraph(); + Assertions.assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.newClass.A")); + Assertions.assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.newClass.B")); + Assertions.assertTrue(graph.containsVertex("org.hjug.graphbuilder.visitor.testclasses.newClass.C")); + + // TODO: Investigate further to confirm correctness + Assertions.assertEquals( + 3, + graph.getEdgeWeight(graph.getEdge( + "org.hjug.graphbuilder.visitor.testclasses.newClass.A", + "org.hjug.graphbuilder.visitor.testclasses.newClass.C"))); + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitorTest.java new file mode 100644 index 00000000..fbaf7523 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitorTest.java @@ -0,0 +1,59 @@ +package org.hjug.graphbuilder.visitor; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.openrewrite.ExecutionContext; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; + +class JavaVariableTypeVisitorTest { + + @Test + void visitClasses() throws IOException { + + File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses"); + + org.openrewrite.java.JavaParser javaParser = + JavaParser.fromJavaVersion().build(); + ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); + + JavaVariableTypeVisitor javaVariableCapturingVisitor = new JavaVariableTypeVisitor<>(); + + List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList()); + javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> { + javaVariableCapturingVisitor.visit(cu, ctx); + }); + + Assertions.assertTrue(javaVariableCapturingVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.A")); + Assertions.assertTrue(javaVariableCapturingVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.B")); + Assertions.assertTrue(javaVariableCapturingVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.C")); + Assertions.assertTrue(javaVariableCapturingVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.D")); + Assertions.assertTrue(javaVariableCapturingVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.E")); + Assertions.assertTrue(javaVariableCapturingVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.MyAnnotation")); + Assertions.assertFalse(javaVariableCapturingVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.F")); + Assertions.assertFalse(javaVariableCapturingVisitor + .getClassReferencesGraph() + .containsVertex("org.hjug.graphbuilder.visitor.testclasses.G")); + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVisitorTest.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVisitorTest.java new file mode 100644 index 00000000..b1260b29 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVisitorTest.java @@ -0,0 +1,48 @@ +package org.hjug.graphbuilder.visitor; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import org.jgrapht.Graph; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.SimpleDirectedWeightedGraph; +import org.junit.jupiter.api.Test; +import org.openrewrite.ExecutionContext; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; + +class JavaVisitorTest { + + @Test + void visitClasses() throws IOException { + + File srcDirectory = new File("src/test/java/org/hjug/graphbuilder/visitor/testclasses"); + + org.openrewrite.java.JavaParser javaParser = + JavaParser.fromJavaVersion().build(); + ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace); + + final Graph classReferencesGraph = + new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + + final Graph packageReferencesGraph = + new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + + final JavaVisitor javaVisitor = + new JavaVisitor<>(classReferencesGraph, packageReferencesGraph); + + List list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList()); + javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> { + System.out.println(cu.getSourcePath()); + javaVisitor.visit(cu, ctx); + }); + + assertEquals(3, javaVisitor.getPackagesInCodebase().size()); + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/A.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/A.java new file mode 100644 index 00000000..9b42e2c4 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/A.java @@ -0,0 +1,61 @@ +package org.hjug.graphbuilder.visitor.testclasses; + +import java.util.List; +import java.util.Map; + +@MyAnnotation +public class A { + + // public A(B cB, C cC){} + + B crazyType; + + @MyAnnotation + @MyOtherAnnotation + int intVar, intVar2; + + @MyAnnotation + @MyOtherAnnotation + C rawC; + + B b, b3; + C c; + D[] ds; + D d; + + @MyAnnotation + B[] arrayOfGenericBsWithCTypeParam; + + @MyAnnotation + B bWithArrayOfCs; + + List> listWithNestedGenric; + Map map; + List> listOfListsOfNumbers; + + @MyAnnotation + F doSomething(B paramB, C genericParam) { + List> list3; + A a2; + B b2; + C c2; + + H h = new H(); + + B.invocationTest(h); + + return new G(); + } + + class InnerClass { + class InnerInner { + class MegaInner { + D d; + } + } + } + + static class StaticInnerClass {} +} + +class NonPublic {} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/B.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/B.java new file mode 100644 index 00000000..991ea9c9 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/B.java @@ -0,0 +1,10 @@ +package org.hjug.graphbuilder.visitor.testclasses; + +public class B { + + static D invocationTest(T type) { + return new D(); + } + + static class InnerB extends A {} +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/C.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/C.java new file mode 100644 index 00000000..11daf502 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/C.java @@ -0,0 +1,3 @@ +package org.hjug.graphbuilder.visitor.testclasses; + +public class C {} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/D.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/D.java new file mode 100644 index 00000000..d12051e0 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/D.java @@ -0,0 +1,3 @@ +package org.hjug.graphbuilder.visitor.testclasses; + +public class D {} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/E.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/E.java new file mode 100644 index 00000000..e715afef --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/E.java @@ -0,0 +1,5 @@ +package org.hjug.graphbuilder.visitor.testclasses; + +public interface E { + void foo(A a); +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/F.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/F.java new file mode 100644 index 00000000..770c8ee1 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/F.java @@ -0,0 +1,3 @@ +package org.hjug.graphbuilder.visitor.testclasses; + +public class F {} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/G.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/G.java new file mode 100644 index 00000000..86c80a3f --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/G.java @@ -0,0 +1,3 @@ +package org.hjug.graphbuilder.visitor.testclasses; + +public class G extends F {} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/H.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/H.java new file mode 100644 index 00000000..1a427e58 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/H.java @@ -0,0 +1,3 @@ +package org.hjug.graphbuilder.visitor.testclasses; + +public class H extends B {} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/MyAnnotation.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/MyAnnotation.java new file mode 100644 index 00000000..71089ab8 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/MyAnnotation.java @@ -0,0 +1,7 @@ +package org.hjug.graphbuilder.visitor.testclasses; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE_USE) +@interface MyAnnotation {} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/MyOtherAnnotation.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/MyOtherAnnotation.java new file mode 100644 index 00000000..3907769f --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/MyOtherAnnotation.java @@ -0,0 +1,7 @@ +package org.hjug.graphbuilder.visitor.testclasses; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE_USE) +@interface MyOtherAnnotation {} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/A.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/A.java new file mode 100644 index 00000000..733c521a --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/A.java @@ -0,0 +1,11 @@ +package org.hjug.graphbuilder.visitor.testclasses.methodInvocation; + +public class A { + + A doSomething() { + B.invocationTest(new D()); + A a = B.invocationTest(new D()); + // TODO: add visitor for J.ReturnType + return B.invocationTest(new D()); + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/B.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/B.java new file mode 100644 index 00000000..0d4cfe91 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/B.java @@ -0,0 +1,8 @@ +package org.hjug.graphbuilder.visitor.testclasses.methodInvocation; + +public class B { + + static A invocationTest(T type) { + return new A(); + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/C.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/C.java new file mode 100644 index 00000000..9b4af8a1 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/C.java @@ -0,0 +1,3 @@ +package org.hjug.graphbuilder.visitor.testclasses.methodInvocation; + +public class C extends B {} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/D.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/D.java new file mode 100644 index 00000000..f4218105 --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/D.java @@ -0,0 +1,3 @@ +package org.hjug.graphbuilder.visitor.testclasses.methodInvocation; + +public class D extends C {} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass/A.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass/A.java new file mode 100644 index 00000000..f99d037e --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass/A.java @@ -0,0 +1,12 @@ +package org.hjug.graphbuilder.visitor.testclasses.newClass; + +public class A { + + B newClassMethod() { + new C(); + C c = new C(); + + // TODO: add visitor for J.ReturnType + return new B(c); + } +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass/B.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass/B.java new file mode 100644 index 00000000..2c48703a --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass/B.java @@ -0,0 +1,6 @@ +package org.hjug.graphbuilder.visitor.testclasses.newClass; + +public class B { + + public B(C c) {} +} diff --git a/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass/C.java b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass/C.java new file mode 100644 index 00000000..7eb36fad --- /dev/null +++ b/codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass/C.java @@ -0,0 +1,3 @@ +package org.hjug.graphbuilder.visitor.testclasses.newClass; + +public class C {} diff --git a/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/A.java b/codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/A.java similarity index 100% rename from circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/A.java rename to codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/A.java diff --git a/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/B.java b/codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/B.java similarity index 100% rename from circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/B.java rename to codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/B.java diff --git a/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/C.java b/codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/C.java similarity index 100% rename from circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/C.java rename to codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/C.java diff --git a/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/D.java b/codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/D.java similarity index 100% rename from circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/D.java rename to codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/D.java diff --git a/circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/E.java b/codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/E.java similarity index 100% rename from circular-reference-detector/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/E.java rename to codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/E.java diff --git a/cost-benefit-calculator/pom.xml b/cost-benefit-calculator/pom.xml index fe6490dc..f2c567a4 100644 --- a/cost-benefit-calculator/pom.xml +++ b/cost-benefit-calculator/pom.xml @@ -5,7 +5,7 @@ org.hjug.refactorfirst refactor-first - 0.6.3-SNAPSHOT + 0.7.0-SNAPSHOT org.hjug.refactorfirst.costbenefitcalculator @@ -16,7 +16,11 @@ org.slf4j slf4j-api - 2.0.7 + + + + org.hjug.refactorfirst.codebasegraphbuilder + codebase-graph-builder @@ -30,8 +34,8 @@ - org.hjug.refactorfirst.circularreferencedetector - circular-reference-detector + org.hjug.refactorfirst.dsm + dsm diff --git a/cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java b/cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java index fa098123..66b29002 100644 --- a/cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java +++ b/cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java @@ -3,8 +3,6 @@ import static net.sourceforge.pmd.RuleViolation.CLASS_NAME; import static net.sourceforge.pmd.RuleViolation.PACKAGE_NAME; -import com.github.javaparser.ParserConfiguration; -import com.github.javaparser.StaticJavaParser; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -13,38 +11,24 @@ import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; import net.sourceforge.pmd.*; import net.sourceforge.pmd.lang.LanguageRegistry; import org.eclipse.jgit.api.errors.GitAPIException; -import org.hjug.cycledetector.CircularReferenceChecker; import org.hjug.git.ChangePronenessRanker; import org.hjug.git.GitLogReader; import org.hjug.git.ScmLogInfo; import org.hjug.metrics.*; import org.hjug.metrics.rules.CBORule; -import org.hjug.parser.JavaProjectParser; -import org.jgrapht.Graph; -import org.jgrapht.alg.flow.GusfieldGomoryHuCutTree; -import org.jgrapht.graph.AsSubgraph; -import org.jgrapht.graph.AsUndirectedGraph; -import org.jgrapht.graph.DefaultWeightedEdge; @Slf4j public class CostBenefitCalculator implements AutoCloseable { - private final Map> renderedSubGraphs = new HashMap<>(); - private Report report; - private String repositoryPath; + private final String repositoryPath; private GitLogReader gitLogReader; private final ChangePronenessRanker changePronenessRanker; - private final JavaProjectParser javaProjectParser = new JavaProjectParser(); - - @Getter - private Graph classReferencesGraph; public CostBenefitCalculator(String repositoryPath) { this.repositoryPath = repositoryPath; @@ -52,11 +36,6 @@ public CostBenefitCalculator(String repositoryPath) { log.info("Initiating Cost Benefit calculation"); try { gitLogReader = new GitLogReader(new File(repositoryPath)); - // repository = gitLogReader.gitRepository(new File(repositoryPath)); - // for (String file : - // gitLogReader.listRepositoryContentsAtHEAD(repository).keySet()) { - // log.info("Files at HEAD: {}", file); - // } } catch (IOException e) { log.error("Failure to access Git repository", e); } @@ -69,166 +48,52 @@ public void close() throws Exception { gitLogReader.close(); } - public List runCycleAnalysis() { - StaticJavaParser.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.BLEEDING_EDGE); - List rankedCycles = new ArrayList<>(); - try { - boolean calculateCycleChurn = false; - idenfifyRankedCycles(rankedCycles, calculateCycleChurn); - sortRankedCycles(rankedCycles, calculateCycleChurn); - setPriorities(rankedCycles); - } catch (IOException e) { - throw new RuntimeException(e); - } - - return rankedCycles; - } - - public List runCycleAnalysisAndCalculateCycleChurn() { - StaticJavaParser.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.BLEEDING_EDGE); - List rankedCycles = new ArrayList<>(); - try { - boolean calculateCycleChurn = true; - idenfifyRankedCycles(rankedCycles, calculateCycleChurn); - sortRankedCycles(rankedCycles, calculateCycleChurn); - setPriorities(rankedCycles); - } catch (IOException e) { - throw new RuntimeException(e); - } + // copied from PMD's PmdTaskImpl.java and modified + public void runPmdAnalysis() throws IOException { + PMDConfiguration configuration = new PMDConfiguration(); - return rankedCycles; - } + try (PmdAnalysis pmd = PmdAnalysis.create(configuration)) { + loadRules(pmd); - private void idenfifyRankedCycles(List rankedCycles, boolean calculateChurnForCycles) - throws IOException { - Map> cycles = getCycles(); - Map classNamesAndPaths = getClassNamesAndPaths(); - cycles.forEach((vertex, subGraph) -> { - int vertexCount = subGraph.vertexSet().size(); - int edgeCount = subGraph.edgeSet().size(); - - if (vertexCount > 1 && edgeCount > 1 && !isDuplicateSubGraph(subGraph, vertex)) { - renderedSubGraphs.put(vertex, subGraph); - log.info("Vertex: " + vertex + " vertex count: " + vertexCount + " edge count: " + edgeCount); - GusfieldGomoryHuCutTree gusfieldGomoryHuCutTree = - new GusfieldGomoryHuCutTree<>(new AsUndirectedGraph<>(subGraph)); - double minCut = gusfieldGomoryHuCutTree.calculateMinCut(); - Set minCutEdges = gusfieldGomoryHuCutTree.getCutEdges(); - - List cycleNodes = subGraph.vertexSet().stream() - .map(classInCycle -> new CycleNode(classInCycle, classNamesAndPaths.get(classInCycle))) - .collect(Collectors.toList()); - - rankedCycles.add( - createRankedCycle(calculateChurnForCycles, vertex, subGraph, cycleNodes, minCut, minCutEdges)); + try (Stream files = Files.walk(Paths.get(repositoryPath))) { + files.filter(Files::isRegularFile).forEach(file -> pmd.files().addFile(file)); } - }); - } - private static void setPriorities(List rankedCycles) { - int priority = 1; - for (RankedCycle rankedCycle : rankedCycles) { - rankedCycle.setPriority(priority++); + report = pmd.performAnalysisAndCollectReport(); } } - private Map> getCycles() throws IOException { - classReferencesGraph = javaProjectParser.getClassReferences(repositoryPath); - CircularReferenceChecker circularReferenceChecker = new CircularReferenceChecker(); - Map> cycles = - circularReferenceChecker.detectCycles(classReferencesGraph); - return cycles; - } - - private static void sortRankedCycles(List rankedCycles, boolean calculateChurnForCycles) { - if (calculateChurnForCycles) { - rankedCycles.sort(Comparator.comparing(RankedCycle::getAverageChangeProneness)); + public void runPmdAnalysis(boolean excludeTests, String testSourceDirectory) throws IOException { + PMDConfiguration configuration = new PMDConfiguration(); - int cpr = 1; - for (RankedCycle rankedCycle : rankedCycles) { - rankedCycle.setChangePronenessRank(cpr++); - } - } else { - rankedCycles.sort(Comparator.comparing(RankedCycle::getRawPriority).reversed()); - } - } + try (PmdAnalysis pmd = PmdAnalysis.create(configuration)) { + loadRules(pmd); - private RankedCycle createRankedCycle( - boolean calculateChurnForCycles, - String vertex, - AsSubgraph subGraph, - List cycleNodes, - double minCut, - Set minCutEdges) { - RankedCycle rankedCycle; - if (calculateChurnForCycles) { - List changeRanks = getRankedChangeProneness(cycleNodes); - - Map cycleNodeMap = new HashMap<>(); - - for (CycleNode cycleNode : cycleNodes) { - cycleNodeMap.put(cycleNode.getFileName(), cycleNode); - } + try (Stream files = Files.walk(Paths.get(repositoryPath))) { + Stream pathStream; + if (excludeTests) { + pathStream = files.filter(Files::isRegularFile) + .filter(file -> !file.toString().contains(testSourceDirectory)); + } else { + pathStream = files.filter(Files::isRegularFile); + } - for (ScmLogInfo changeRank : changeRanks) { - CycleNode cycleNode = cycleNodeMap.get(changeRank.getPath()); - cycleNode.setScmLogInfo(changeRank); + pathStream.forEach(file -> pmd.files().addFile(file)); } - // sum change proneness ranks - int changePronenessRankSum = changeRanks.stream() - .mapToInt(ScmLogInfo::getChangePronenessRank) - .sum(); - rankedCycle = new RankedCycle( - vertex, - changePronenessRankSum, - subGraph.vertexSet(), - subGraph.edgeSet(), - minCut, - minCutEdges, - cycleNodes); - } else { - rankedCycle = - new RankedCycle(vertex, subGraph.vertexSet(), subGraph.edgeSet(), minCut, minCutEdges, cycleNodes); + report = pmd.performAnalysisAndCollectReport(); } - return rankedCycle; } - private boolean isDuplicateSubGraph(AsSubgraph subGraph, String vertex) { - if (!renderedSubGraphs.isEmpty()) { - for (AsSubgraph renderedSubGraph : renderedSubGraphs.values()) { - if (renderedSubGraph.vertexSet().size() == subGraph.vertexSet().size() - && renderedSubGraph.edgeSet().size() - == subGraph.edgeSet().size() - && renderedSubGraph.vertexSet().contains(vertex)) { - return true; - } - } - } + private void loadRules(PmdAnalysis pmd) { + RuleSetLoader rulesetLoader = pmd.newRuleSetLoader(); + pmd.addRuleSets(rulesetLoader.loadRuleSetsWithoutException(List.of("category/java/design.xml"))); - return false; - } + Rule cboClassRule = new CBORule(); + cboClassRule.setLanguage(LanguageRegistry.PMD.getLanguageByFullName("Java")); + pmd.addRuleSet(RuleSet.forSingleRule(cboClassRule)); - // copied from PMD's PmdTaskImpl.java and modified - public void runPmdAnalysis() throws IOException { - PMDConfiguration configuration = new PMDConfiguration(); - - try (PmdAnalysis pmd = PmdAnalysis.create(configuration)) { - RuleSetLoader rulesetLoader = pmd.newRuleSetLoader(); - pmd.addRuleSets(rulesetLoader.loadRuleSetsWithoutException(List.of("category/java/design.xml"))); - - Rule cboClassRule = new CBORule(); - cboClassRule.setLanguage(LanguageRegistry.PMD.getLanguageByFullName("Java")); - pmd.addRuleSet(RuleSet.forSingleRule(cboClassRule)); - - log.info("files to be scanned: " + Paths.get(repositoryPath)); - - try (Stream files = Files.walk(Paths.get(repositoryPath))) { - files.filter(Files::isRegularFile).forEach(file -> pmd.files().addFile(file)); - } - - report = pmd.performAnalysisAndCollectReport(); - } + log.info("files to be scanned: " + Paths.get(repositoryPath)); } public List calculateGodClassCostBenefitValues() { @@ -287,7 +152,7 @@ List getRankedChangeProneness(List disharm ScmLogInfo scmLogInfo = null; try { scmLogInfo = gitLogReader.fileLog(path); - log.info("Successfully fetched scmLogInfo for {}", scmLogInfo.getPath()); + log.debug("Successfully fetched scmLogInfo for {}", scmLogInfo.getPath()); } catch (GitAPIException | IOException e) { log.error("Error reading Git repository contents.", e); } catch (NullPointerException e) { @@ -354,32 +219,4 @@ private String canonicaliseURIStringForRepoLookup(String uriString) { } return uriString.replace("file:///" + repositoryPath.replace("\\", "/") + "/", ""); } - - public Map getClassNamesAndPaths() throws IOException { - - Map fileNamePaths = new HashMap<>(); - - try (Stream walk = Files.walk(Paths.get(repositoryPath))) { - walk.forEach(path -> { - String filename = path.getFileName().toString(); - if (filename.endsWith(".java")) { - String uriString = path.toUri().toString(); - fileNamePaths.put(getClassName(filename), canonicaliseURIStringForRepoLookup(uriString)); - } - }); - } - - return fileNamePaths; - } - - /** - * Extract class name from java file name - * Example : MyJavaClass.java becomes MyJavaClass - * - * @param javaFileName - * @return - */ - private String getClassName(String javaFileName) { - return javaFileName.substring(0, javaFileName.indexOf('.')); - } } diff --git a/cost-benefit-calculator/src/main/java/org/hjug/cbc/CycleRanker.java b/cost-benefit-calculator/src/main/java/org/hjug/cbc/CycleRanker.java new file mode 100644 index 00000000..af6a0394 --- /dev/null +++ b/cost-benefit-calculator/src/main/java/org/hjug/cbc/CycleRanker.java @@ -0,0 +1,137 @@ +package org.hjug.cbc; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.hjug.dsm.CircularReferenceChecker; +import org.hjug.graphbuilder.JavaGraphBuilder; +import org.jgrapht.Graph; +import org.jgrapht.graph.AsSubgraph; +import org.jgrapht.graph.DefaultWeightedEdge; + +@RequiredArgsConstructor +public class CycleRanker { + + private final String repositoryPath; + private final JavaGraphBuilder javaGraphBuilder = new JavaGraphBuilder(); + + @Getter + private Graph classReferencesGraph; + + public void generateClassReferencesGraph(boolean excludeTests, String testSourceDirectory) { + try { + classReferencesGraph = + javaGraphBuilder.getClassReferences(repositoryPath, excludeTests, testSourceDirectory); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public List performCycleAnalysis(boolean excludeTests, String testSourceDirectory) { + List rankedCycles = new ArrayList<>(); + try { + boolean calculateCycleChurn = false; + generateClassReferencesGraph(excludeTests, testSourceDirectory); + identifyRankedCycles(rankedCycles); + sortRankedCycles(rankedCycles, calculateCycleChurn); + setPriorities(rankedCycles); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return rankedCycles; + } + + private void identifyRankedCycles(List rankedCycles) throws IOException { + CircularReferenceChecker circularReferenceChecker = new CircularReferenceChecker(); + Map> cycles = + circularReferenceChecker.getCycles(classReferencesGraph); + Map classNamesAndPaths = getClassNamesAndPaths(); + cycles.forEach((vertex, subGraph) -> { + // TODO: Calculate min cuts for smaller graphs - has a runtime of O(V^4) for a graph + /*Set minCutEdges; + GusfieldGomoryHuCutTree gusfieldGomoryHuCutTree = + new GusfieldGomoryHuCutTree<>(new AsUndirectedGraph<>(subGraph)); + double minCut = gusfieldGomoryHuCutTree.calculateMinCut(); + minCutEdges = gusfieldGomoryHuCutTree.getCutEdges();*/ + + List cycleNodes = subGraph.vertexSet().stream() + .map(classInCycle -> new CycleNode(classInCycle, classNamesAndPaths.get(classInCycle))) + // .peek(cycleNode -> log.info(cycleNode.toString())) + .collect(Collectors.toList()); + + rankedCycles.add(createRankedCycle(vertex, subGraph, cycleNodes, 0.0, new HashSet<>())); + }); + } + + private RankedCycle createRankedCycle( + String vertex, + AsSubgraph subGraph, + List cycleNodes, + double minCut, + Set minCutEdges) { + + return new RankedCycle(vertex, subGraph.vertexSet(), subGraph.edgeSet(), minCut, minCutEdges, cycleNodes); + } + + private static void sortRankedCycles(List rankedCycles, boolean calculateChurnForCycles) { + if (calculateChurnForCycles) { + rankedCycles.sort(Comparator.comparing(RankedCycle::getAverageChangeProneness)); + + int cpr = 1; + for (RankedCycle rankedCycle : rankedCycles) { + rankedCycle.setChangePronenessRank(cpr++); + } + } else { + rankedCycles.sort(Comparator.comparing(RankedCycle::getRawPriority).reversed()); + } + } + + private static void setPriorities(List rankedCycles) { + int priority = 1; + for (RankedCycle rankedCycle : rankedCycles) { + rankedCycle.setPriority(priority++); + } + } + + public Map getClassNamesAndPaths() throws IOException { + + Map fileNamePaths = new HashMap<>(); + + try (Stream walk = Files.walk(Paths.get(repositoryPath))) { + walk.forEach(path -> { + String filename = path.getFileName().toString(); + if (filename.endsWith(".java")) { + String uriString = path.toUri().toString(); + fileNamePaths.put(getClassName(filename), canonicaliseURIStringForRepoLookup(uriString)); + } + }); + } + + return fileNamePaths; + } + + private String canonicaliseURIStringForRepoLookup(String uriString) { + if (repositoryPath.startsWith("/") || repositoryPath.startsWith("\\")) { + return uriString.replace("file://" + repositoryPath.replace("\\", "/") + "/", ""); + } + return uriString.replace("file:///" + repositoryPath.replace("\\", "/") + "/", ""); + } + + /** + * Extract class name from java file name + * Example : MyJavaClass.java becomes MyJavaClass + * + * @param javaFileName + * @return + */ + private String getClassName(String javaFileName) { + return javaFileName.substring(0, javaFileName.indexOf('.')); + } +} diff --git a/coverage/pom.xml b/coverage/pom.xml index 09a54ca1..666238d8 100644 --- a/coverage/pom.xml +++ b/coverage/pom.xml @@ -7,7 +7,7 @@ org.hjug.refactorfirst refactor-first - 0.6.3-SNAPSHOT + 0.7.0-SNAPSHOT coverage @@ -29,6 +29,11 @@ effort-ranker + + org.hjug.refactorfirst.dsm + dsm + + org.hjug.refactorfirst.costbenefitcalculator cost-benefit-calculator @@ -43,6 +48,11 @@ org.hjug.refactorfirst.plugin refactor-first-maven-plugin + + + org.hjug.refactorfirst.report + report + diff --git a/dsm/pom.xml b/dsm/pom.xml new file mode 100644 index 00000000..2aa3db70 --- /dev/null +++ b/dsm/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + + org.hjug.refactorfirst + refactor-first + 0.7.0-SNAPSHOT + + + org.hjug.refactorfirst.dsm + dsm + + + Implementation of a DSM that only has JGraphT-Core as a dependency. + Can be used by other projects. + + + + + org.jgrapht + jgrapht-core + + + org.jgrapht + jgrapht-opt + + + org.slf4j + slf4j-api + + + + \ No newline at end of file diff --git a/dsm/src/main/java/org/hjug/dsm/CircularReferenceChecker.java b/dsm/src/main/java/org/hjug/dsm/CircularReferenceChecker.java new file mode 100644 index 00000000..54700e0e --- /dev/null +++ b/dsm/src/main/java/org/hjug/dsm/CircularReferenceChecker.java @@ -0,0 +1,71 @@ +package org.hjug.dsm; + +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.jgrapht.Graph; +import org.jgrapht.alg.cycle.CycleDetector; +import org.jgrapht.graph.AsSubgraph; +import org.jgrapht.graph.DefaultWeightedEdge; + +@Slf4j +public class CircularReferenceChecker { + + private final Map> uniqueSubGraphs = new HashMap<>(); + + /** + * Detects cycles in the graph that is passed in + * and returns the unique cycles in the graph as a map of subgraphs + * + * @param graph + * @return a Map of unique cycles in the graph + */ + public Map> getCycles(Graph graph) { + + if (!uniqueSubGraphs.isEmpty()) { + return uniqueSubGraphs; + } + + // use CycleDetector.findCycles()? + Map> cycles = detectCycles(graph); + + cycles.forEach((vertex, subGraph) -> { + int vertexCount = subGraph.vertexSet().size(); + int edgeCount = subGraph.edgeSet().size(); + + if (vertexCount > 1 && edgeCount > 1 && !isDuplicateSubGraph(subGraph, vertex)) { + uniqueSubGraphs.put(vertex, subGraph); + log.debug("Vertex: {} vertex count: {} edge count: {}", vertex, vertexCount, edgeCount); + } + }); + + return uniqueSubGraphs; + } + + private boolean isDuplicateSubGraph(AsSubgraph subGraph, String vertex) { + if (!uniqueSubGraphs.isEmpty()) { + for (AsSubgraph renderedSubGraph : uniqueSubGraphs.values()) { + if (renderedSubGraph.vertexSet().size() == subGraph.vertexSet().size() + && renderedSubGraph.edgeSet().size() + == subGraph.edgeSet().size() + && renderedSubGraph.vertexSet().contains(vertex)) { + return true; + } + } + } + + return false; + } + + private Map> detectCycles( + Graph graph) { + Map> cyclesForEveryVertexMap = new HashMap<>(); + CycleDetector cycleDetector = new CycleDetector<>(graph); + cycleDetector.findCycles().forEach(v -> { + AsSubgraph subGraph = + new AsSubgraph<>(graph, cycleDetector.findCyclesContainingVertex(v)); + cyclesForEveryVertexMap.put(v, subGraph); + }); + return cyclesForEveryVertexMap; + } +} diff --git a/dsm/src/main/java/org/hjug/dsm/DSM.java b/dsm/src/main/java/org/hjug/dsm/DSM.java new file mode 100644 index 00000000..93190380 --- /dev/null +++ b/dsm/src/main/java/org/hjug/dsm/DSM.java @@ -0,0 +1,334 @@ +package org.hjug.dsm; + +import java.util.*; +import java.util.stream.Collectors; +import lombok.Getter; +import org.jgrapht.Graph; +import org.jgrapht.Graphs; +import org.jgrapht.alg.connectivity.KosarajuStrongConnectivityInspector; +import org.jgrapht.alg.util.Triple; +import org.jgrapht.graph.AsSubgraph; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.SimpleDirectedWeightedGraph; +import org.jgrapht.opt.graph.sparse.SparseIntDirectedWeightedGraph; + +/* +Generated with Generative AI using a prompt similar to the following and iterated on: +Provide a complete implementation of a Numerical DSM with integer weighted edges in Java. +Include as many comments as possible in the implementation to make it easy to understand. +Use JGraphT classes and methods to the greatest extent possible. +Construction of the DSM should take place as follows. +First, Place nodes with empty rows at the top of the DSM. +Second, Place nodes with empty columns on the right of the DSM. +Third, identify strongly connected nodes and treat them as a single node using JGraphT's TarjanSimpleCycles class. +Fourth, order all edges in the DSM with a topological sort that permits cycles in the graph after identifying strongly connected components. +Fifth, Print the DSM in a method that performs no sorting or ordering - it should only print rows and columns. +When the DSM is printed, label the columns and rows. +Place dashes on the diagonal when printing. +include a method tht returns all edges above the diagonal. +include another method that returns the optimal edge above the diagonal to remove, +include a third method that identifies all minimum weight edges to remove above the diagonal. + +Used https://sookocheff.com/post/dsm/improving-software-architecture-using-design-structure-matrix/#optimizing-processes +as a starting point. + */ + +public class DSM { + private Graph graph; + private List sortedActivities; + boolean activitiesSorted = false; + + Map vertexToInt = new HashMap<>(); + Map intToVertex = new HashMap<>(); + List> sparseEdges = new ArrayList<>(); + int vertexCount = 0; + + @Getter + Map> cycles; + + public DSM() { + this(new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class)); + } + + public DSM(Graph graph) { + this.graph = graph; + sortedActivities = new ArrayList<>(); + cycles = new CircularReferenceChecker().getCycles(graph); + } + + public void addActivity(String activity) { + graph.addVertex(activity); + } + + public void addDependency(String from, String to, int weight) { + DefaultWeightedEdge edge = graph.addEdge(from, to); + if (edge != null) { + graph.setEdgeWeight(edge, weight); + } + } + + private void orderVertices() { + SparseIntDirectedWeightedGraph sparseGraph = getSparseIntDirectedWeightedGraph(); + List> sccs = findStronglyConnectedComponents(sparseGraph); + sortedActivities = convertIntToStringVertices(topologicalSort(sccs, sparseGraph)); + // reversing corrects rendering of the DSM + // with sources as rows and targets as columns + // was needed after AI solution was generated and iterated + Collections.reverse(sortedActivities); + activitiesSorted = true; + } + + private SparseIntDirectedWeightedGraph getSparseIntDirectedWeightedGraph() { + for (String vertex : graph.vertexSet()) { + vertexToInt.put(vertex, vertexCount); + intToVertex.put(vertexCount, vertex); + vertexCount++; + } + + // Create the list of sparseEdges for the SparseIntDirectedWeightedGraph + for (DefaultWeightedEdge edge : graph.edgeSet()) { + int source = vertexToInt.get(graph.getEdgeSource(edge)); + int target = vertexToInt.get(graph.getEdgeTarget(edge)); + double weight = graph.getEdgeWeight(edge); + sparseEdges.add(Triple.of(source, target, weight)); + } + + // Create the SparseIntDirectedWeightedGraph + return new SparseIntDirectedWeightedGraph(vertexCount, sparseEdges); + } + + List convertIntToStringVertices(List intVertices) { + return intVertices.stream().map(intToVertex::get).collect(Collectors.toList()); + } + + /** + * Kosaraju SCC detector avoids stack overflow. + * It is used by JGraphT's CycleDetector, and makes sense to use it here as well for consistency + * @param graph + * @return + */ + private List> findStronglyConnectedComponents(Graph graph) { + KosarajuStrongConnectivityInspector kosaraju = + new KosarajuStrongConnectivityInspector<>(graph); + return kosaraju.stronglyConnectedSets(); + } + + private List topologicalSort(List> sccs, Graph graph) { + List sortedActivities = new ArrayList<>(); + Set visited = new HashSet<>(); + + for (Set scc : sccs) { + for (Integer activity : scc) { + if (!visited.contains(activity)) { + topologicalSortUtil(activity, visited, sortedActivities, graph); + } + } + } + + Collections.reverse(sortedActivities); + return sortedActivities; + } + + private void topologicalSortUtil( + Integer activity, Set visited, List sortedActivities, Graph graph) { + visited.add(activity); + + for (Integer neighbor : Graphs.successorListOf(graph, activity)) { + if (!visited.contains(neighbor)) { + topologicalSortUtil(neighbor, visited, sortedActivities, graph); + } + } + + sortedActivities.add(activity); + } + + public List getEdgesAboveDiagonal() { + if (!activitiesSorted) { + orderVertices(); + } + + List edgesAboveDiagonal = new ArrayList<>(); + + for (int i = 0; i < sortedActivities.size(); i++) { + for (int j = i + 1; j < sortedActivities.size(); j++) { + // source / destination vertex was flipped after solution generation + // to correctly identify the vertex above the diagonal to remove + DefaultWeightedEdge edge = graph.getEdge(sortedActivities.get(i), sortedActivities.get(j)); + if (edge != null) { + edgesAboveDiagonal.add(edge); + } + } + } + + return edgesAboveDiagonal; + } + + public DefaultWeightedEdge getFirstLowestWeightEdgeAboveDiagonalToRemove() { + if (!activitiesSorted) { + orderVertices(); + } + + List edgesAboveDiagonal = getEdgesAboveDiagonal(); + DefaultWeightedEdge optimalEdge = null; + int minWeight = Integer.MAX_VALUE; + + for (DefaultWeightedEdge edge : edgesAboveDiagonal) { + int weight = (int) graph.getEdgeWeight(edge); + if (weight < minWeight) { + minWeight = weight; + optimalEdge = edge; + if (minWeight == 1) { + break; + } + } + } + + return optimalEdge; + } + + public List getMinimumWeightEdgesAboveDiagonal() { + if (!activitiesSorted) { + orderVertices(); + } + + List edgesAboveDiagonal = getEdgesAboveDiagonal(); + List minWeightEdges = new ArrayList<>(); + double minWeight = Double.MAX_VALUE; + + for (DefaultWeightedEdge edge : edgesAboveDiagonal) { + double weight = graph.getEdgeWeight(edge); + if (weight < minWeight) { + minWeight = weight; + minWeightEdges.clear(); + minWeightEdges.add(edge); + } else if (weight == minWeight) { + minWeightEdges.add(edge); + } + } + + return minWeightEdges; + } + + public void printDSM() { + if (!activitiesSorted) { + orderVertices(); + } + + printDSM(graph, sortedActivities); + } + + void printDSM(Graph graph, List sortedActivities) { + + System.out.println("Design Structure Matrix:"); + System.out.print(" "); + for (String col : sortedActivities) { + System.out.print(col + " "); + } + System.out.println(); + for (String row : sortedActivities) { + System.out.print(row + " "); + for (String col : sortedActivities) { + if (col.equals(row)) { + System.out.print("- "); + } else { + DefaultWeightedEdge edge = graph.getEdge(row, col); + if (edge != null) { + System.out.print((int) graph.getEdgeWeight(edge) + " "); + } else { + System.out.print("0 "); + } + } + } + System.out.println(); + } + } + + /** + * Captures the impact of the removal of each edge above the diagonal. + * + * @param limit the number of back edges to analyze + * @return List Impact of each edge if removed. + */ + public List getImpactOfEdgesAboveDiagonalIfRemoved(int limit) { + // get edges above diagonal for DSM graph + List edgesAboveDiagonal; + List allEdgesAboveDiagonal = getEdgesAboveDiagonal(); + + if (limit == 0 || allEdgesAboveDiagonal.size() <= limit) { + edgesAboveDiagonal = allEdgesAboveDiagonal; + } else { + // get first 50 values of min weight + List minimumWeightEdgesAboveDiagonal = getMinimumWeightEdgesAboveDiagonal(); + int max = Math.min(minimumWeightEdgesAboveDiagonal.size(), limit); + edgesAboveDiagonal = minimumWeightEdgesAboveDiagonal.subList(0, max); + } + + double avgCycleNodeCount = getAverageCycleNodeCount(); + + // build the cloned graph + Graph clonedGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class); + graph.vertexSet().forEach(clonedGraph::addVertex); + for (DefaultWeightedEdge weightedEdge : graph.edgeSet()) { + clonedGraph.addEdge(graph.getEdgeSource(weightedEdge), graph.getEdgeTarget(weightedEdge), weightedEdge); + } + + List edgesToRemove = new ArrayList<>(); + // capture impact of each edge on graph when removed + for (DefaultWeightedEdge edge : edgesAboveDiagonal) { + int edgeInCyclesCount = 0; + for (AsSubgraph cycle : cycles.values()) { + if (cycle.containsEdge(edge)) { + edgeInCyclesCount++; + } + } + + // remove the edge + clonedGraph.removeEdge(edge); + + // identify updated cycles and calculate updated graph information + edgesToRemove.add(getEdgeToRemoveInfo( + edge, edgeInCyclesCount, avgCycleNodeCount, new CircularReferenceChecker().getCycles(clonedGraph))); + + // add the edge back for next iteration + clonedGraph.addEdge(graph.getEdgeSource(edge), graph.getEdgeTarget(edge), edge); + clonedGraph.setEdgeWeight(edge, graph.getEdgeWeight(edge)); + } + + edgesToRemove.sort(Comparator.comparing(EdgeToRemoveInfo::getPayoff)); + Collections.reverse(edgesToRemove); + return edgesToRemove; + } + + private EdgeToRemoveInfo getEdgeToRemoveInfo( + DefaultWeightedEdge edge, + int edgeInCyclesCount, + double currentAvgCycleNodeCount, + Map> cycles) { + // get the new number of cycles + int newCycleCount = cycles.size(); + + // calculate the average cycle node count + double newAverageCycleNodeCount = getAverageCycleNodeCount(cycles); + + // capture the what-if values + double edgeWeight = graph.getEdgeWeight(edge); + + double impact = (currentAvgCycleNodeCount - newAverageCycleNodeCount) / edgeWeight; + return new EdgeToRemoveInfo( + edge, edgeWeight, edgeInCyclesCount, newCycleCount, newAverageCycleNodeCount, impact); + } + + public static double getAverageCycleNodeCount(Map> cycles) { + return cycles.values().stream() + .mapToInt(cycle -> cycle.vertexSet().size()) + .average() + .orElse(0.0); + } + + public double getAverageCycleNodeCount() { + return cycles.values().stream() + .mapToInt(cycle -> cycle.vertexSet().size()) + .average() + .orElse(0.0); + } +} diff --git a/dsm/src/main/java/org/hjug/dsm/EdgeToRemoveInfo.java b/dsm/src/main/java/org/hjug/dsm/EdgeToRemoveInfo.java new file mode 100644 index 00000000..c16e0967 --- /dev/null +++ b/dsm/src/main/java/org/hjug/dsm/EdgeToRemoveInfo.java @@ -0,0 +1,14 @@ +package org.hjug.dsm; + +import lombok.Data; +import org.jgrapht.graph.DefaultWeightedEdge; + +@Data +public class EdgeToRemoveInfo { + private final DefaultWeightedEdge edge; + private final double edgeWeight; + private final int edgeInCycleCount; + private final int newCycleCount; + private final double averageCycleNodeCount; + private final double payoff; // impact / effort +} diff --git a/circular-reference-detector/src/test/java/org/hjug/cycledetector/CircularReferenceCheckerTests.java b/dsm/src/test/java/org/hjug/dsm/CircularReferenceCheckerTests.java similarity index 50% rename from circular-reference-detector/src/test/java/org/hjug/cycledetector/CircularReferenceCheckerTests.java rename to dsm/src/test/java/org/hjug/dsm/CircularReferenceCheckerTests.java index d6c4accd..fb2e8439 100644 --- a/circular-reference-detector/src/test/java/org/hjug/cycledetector/CircularReferenceCheckerTests.java +++ b/dsm/src/test/java/org/hjug/dsm/CircularReferenceCheckerTests.java @@ -1,9 +1,7 @@ -package org.hjug.cycledetector; +package org.hjug.dsm; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.File; import java.util.Map; import org.jgrapht.Graph; import org.jgrapht.graph.AsSubgraph; @@ -18,7 +16,7 @@ class CircularReferenceCheckerTests { @DisplayName("Detect 3 cycles from given graph.") @Test - public void detectCyclesTest() { + void detectCyclesTest() { Graph classReferencesGraph = new DefaultDirectedGraph<>(DefaultWeightedEdge.class); classReferencesGraph.addVertex("A"); classReferencesGraph.addVertex("B"); @@ -27,21 +25,7 @@ public void detectCyclesTest() { classReferencesGraph.addEdge("B", "C"); classReferencesGraph.addEdge("C", "A"); Map> cyclesForEveryVertexMap = - sutCircularReferenceChecker.detectCycles(classReferencesGraph); - assertEquals(3, cyclesForEveryVertexMap.size()); - } - - @DisplayName("Create graph image in given outputDirectory") - @Test - public void createImageTest() { - Graph classReferencesGraph = new DefaultDirectedGraph<>(DefaultWeightedEdge.class); - classReferencesGraph.addVertex("A"); - classReferencesGraph.addVertex("B"); - classReferencesGraph.addVertex("C"); - classReferencesGraph.addEdge("A", "B"); - classReferencesGraph.addEdge("B", "C"); - classReferencesGraph.addEdge("C", "A"); - File newGraphImage = new File("src/test/resources/testOutputDirectory/graphtestGraph.png"); - assertTrue(newGraphImage.exists() && !newGraphImage.isDirectory()); + sutCircularReferenceChecker.getCycles(classReferencesGraph); + assertEquals(1, cyclesForEveryVertexMap.size()); } } diff --git a/dsm/src/test/java/org/hjug/dsm/DSMTest.java b/dsm/src/test/java/org/hjug/dsm/DSMTest.java new file mode 100644 index 00000000..4e871eb1 --- /dev/null +++ b/dsm/src/test/java/org/hjug/dsm/DSMTest.java @@ -0,0 +1,134 @@ +package org.hjug.dsm; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.SimpleDirectedWeightedGraph; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DSMTest { + + DSM dsm; + + @BeforeEach + void setUp() { + dsm = new DSM(); + dsm.addActivity("A"); + dsm.addActivity("B"); + dsm.addActivity("C"); + dsm.addActivity("D"); + + dsm.addDependency("A", "B", 1); + dsm.addDependency("B", "C", 2); + dsm.addDependency("C", "D", 3); + dsm.addDependency("B", "A", 6); // Adding a cycle + dsm.addDependency("C", "A", 5); // Adding a cycle + dsm.addDependency("D", "A", 4); // Adding a cycle + + /* + D C B A + D - 0 0 4 + C 3 - 0 5 + B 0 2 - 6 + A 0 0 1 - + */ + + dsm.addActivity("E"); + dsm.addActivity("F"); + dsm.addActivity("G"); + dsm.addActivity("H"); + dsm.addDependency("D", "C", 2); + dsm.addDependency("A", "H", 7); + dsm.addDependency("E", "C", 9); + dsm.addDependency("E", "H", 2); + dsm.addDependency("G", "E", 2); + dsm.addDependency("H", "D", 9); + dsm.addDependency("H", "G", 5); + + // dsm.printDSM(); + } + + @Test + void optimalBackwardEdgeToRemove() { + // Identify which edge above the diagonal should be removed first + DefaultWeightedEdge edge = dsm.getFirstLowestWeightEdgeAboveDiagonalToRemove(); + assertEquals("(D : C)", edge.toString()); + } + + @Test + void optimalBackwardEdgeToRemoveWithWeightOfOne() { + DSM dsm2 = new DSM(); + dsm2.addActivity("A"); + dsm2.addActivity("B"); + dsm2.addActivity("C"); + + dsm2.addDependency("A", "B", 1); + dsm2.addDependency("B", "C", 1); + dsm2.addDependency("B", "A", 1); + dsm2.addDependency("C", "A", 1); + + // Identify which edge above the diagonal should be removed first + DefaultWeightedEdge edge = dsm2.getFirstLowestWeightEdgeAboveDiagonalToRemove(); + assertEquals("(C : A)", edge.toString()); + } + + @Test + void minWeightBackwardEdges() { + // Identify which edge above the diagonal in the set of cycles should be removed first + List edges = dsm.getMinimumWeightEdgesAboveDiagonal(); + assertEquals(2, edges.size()); + assertEquals("(D : C)", edges.get(0).toString()); + assertEquals("(E : H)", edges.get(1).toString()); + } + + @Test + void edgesAboveDiagonal() { + // Identify edges above the diagonal + List edges = dsm.getEdgesAboveDiagonal(); + assertEquals(5, edges.size()); + assertEquals("(D : C)", edges.get(0).toString()); + assertEquals("(D : A)", edges.get(1).toString()); + assertEquals("(C : A)", edges.get(2).toString()); + assertEquals("(B : A)", edges.get(3).toString()); + assertEquals("(E : H)", edges.get(4).toString()); + } + + @Test + void getImpactOfEdgesAboveDiagonalIfRemoved() { + dsm = new DSM(new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class)); + dsm.addActivity("A"); + dsm.addActivity("B"); + dsm.addActivity("C"); + dsm.addActivity("D"); + + // Cycle 1 + dsm.addDependency("A", "B", 1); + dsm.addDependency("B", "C", 2); + dsm.addDependency("C", "D", 3); + dsm.addDependency("B", "A", 6); // Adding a cycle + dsm.addDependency("C", "A", 5); // Adding a cycle + dsm.addDependency("D", "A", 4); // Adding a cycle + + // Cycle 2 + dsm.addActivity("E"); + dsm.addActivity("F"); + dsm.addActivity("G"); + dsm.addActivity("H"); + dsm.addDependency("E", "F", 2); + dsm.addDependency("F", "G", 7); + dsm.addDependency("G", "H", 9); + dsm.addDependency("H", "E", 9); // create cycle + + dsm.addDependency("A", "E", 9); + dsm.addDependency("E", "A", 3); // create cycle between cycles + + List infos = dsm.getImpactOfEdgesAboveDiagonalIfRemoved(50); + assertEquals(5, infos.size()); + + assertEquals("(H : E)", infos.get(0).getEdge().toString()); + assertEquals(2, infos.get(0).getNewCycleCount()); + assertEquals(4.5, infos.get(0).getAverageCycleNodeCount()); + } +} diff --git a/effort-ranker/pom.xml b/effort-ranker/pom.xml index cd42fa18..0b70f025 100644 --- a/effort-ranker/pom.xml +++ b/effort-ranker/pom.xml @@ -5,7 +5,7 @@ org.hjug.refactorfirst refactor-first - 0.6.3-SNAPSHOT + 0.7.0-SNAPSHOT org.hjug.refactorfirst.effortranker @@ -20,14 +20,11 @@ org.hjug.refactorfirst.testresources test-resources - 0.6.3-SNAPSHOT org.slf4j slf4j-api - jar - 1.7.2 diff --git a/graph-data-generator/pom.xml b/graph-data-generator/pom.xml index 52cf45e2..f898362d 100644 --- a/graph-data-generator/pom.xml +++ b/graph-data-generator/pom.xml @@ -5,7 +5,7 @@ org.hjug.refactorfirst refactor-first - 0.6.3-SNAPSHOT + 0.7.0-SNAPSHOT org.hjug.refactorfirst.graphdatagenerator @@ -15,7 +15,6 @@ org.hjug.refactorfirst.costbenefitcalculator cost-benefit-calculator - 0.6.3-SNAPSHOT diff --git a/pom.xml b/pom.xml index 62946c38..32b13a76 100644 --- a/pom.xml +++ b/pom.xml @@ -1,10 +1,11 @@ - + 4.0.0 org.hjug.refactorfirst refactor-first - 0.6.3-SNAPSHOT + 0.7.0-SNAPSHOT pom https://github.com/refactorfirst/RefactorFirst @@ -19,7 +20,7 @@ Apache License 2.0 - http://www.apache.org/licenses/ + http://www.apache.org/licenses/ repo @@ -41,8 +42,8 @@ scm:git:https://github.com/refactorfirst/RefactorFirst scm:git:https://github.com/refactorfirst/RefactorFirst https://github.com/refactorfirst/RefactorFirst - HEAD - + HEAD + GitHub @@ -56,14 +57,7 @@ - 1.18.30 - - 4.0.0 - 4.0.3 - 1.10.1 - - - + 1.18.36 jimbethancourt_RefactorFirst ${project.artifactId} jimbethancourt-github @@ -74,7 +68,8 @@ test-resources - circular-reference-detector + codebase-graph-builder + dsm change-proneness-ranker effort-ranker cost-benefit-calculator @@ -101,8 +96,8 @@ - org.hjug.refactorfirst.circularreferencedetector - circular-reference-detector + org.hjug.refactorfirst.dsm + dsm ${project.version} @@ -137,6 +132,12 @@ test + + org.hjug.refactorfirst.codebasegraphbuilder + codebase-graph-builder + ${project.version} + + org.eclipse.jgit org.eclipse.jgit @@ -144,6 +145,23 @@ compile + + org.jgrapht + jgrapht-core + 1.5.2 + + + org.jgrapht + jgrapht-opt + 1.5.2 + + + + in.wilsonl.minifyhtml + minify-html + 0.15.0 + + net.sourceforge.pmd pmd-java @@ -169,7 +187,27 @@ com.fasterxml.jackson.core jackson-databind - 2.13.4.2 + 2.18.3 + + + + + + com.google.guava + guava + 33.4.0-jre + + + + org.apache.maven + maven-core + ${maven.core.version} + + + com.google.guava + guava + + + + + org.slf4j + slf4j-api + 2.0.17 + + + org.slf4j + slf4j-simple + 2.0.17 + @@ -210,7 +259,6 @@ ${lombok.version} true - @@ -219,24 +267,24 @@ org.apache.maven.plugins maven-compiler-plugin 3.8.1 - - - -XDcompilePolicy=simple - - - - - org.projectlombok - lombok - ${lombok.version} - - - - 11 + + + -XDcompilePolicy=simple + + + + + org.projectlombok + lombok + ${lombok.version} + + + + 11 @@ -276,13 +324,13 @@ org.pitest pitest-maven 1.16.1 - - - org.pitest - pitest-junit5-plugin - 1.2.1 - - + + + org.pitest + pitest-junit5-plugin + 1.2.1 + + @@ -303,12 +351,12 @@ com.github.spotbugs spotbugs-maven-plugin - ${spotbugs.maven.plugin.version} + 4.9.2.0 com.github.spotbugs spotbugs - ${spotbugs.version} + 4.9.3 @@ -321,7 +369,7 @@ com.h3xstream.findsecbugs findsecbugs-plugin - ${findsecbugs.plugin.version} + 1.13.0 @@ -353,8 +401,8 @@ *.java - - + + true 4 @@ -362,7 +410,7 @@ - + @@ -386,7 +434,7 @@ org.owasp dependency-check-maven - 6.1.0 + 12.1.0 8.0 @@ -410,7 +458,8 @@ maven-release-plugin 2.5.3 - https://oss.sonatype.org/content/repositories/snapshots/ + https://oss.sonatype.org/content/repositories/snapshots/ + @@ -501,6 +550,4 @@ - - diff --git a/refactor-first-maven-plugin/pom.xml b/refactor-first-maven-plugin/pom.xml index 19f0ed6e..9b3a1ff0 100644 --- a/refactor-first-maven-plugin/pom.xml +++ b/refactor-first-maven-plugin/pom.xml @@ -5,7 +5,7 @@ org.hjug.refactorfirst refactor-first - 0.6.3-SNAPSHOT + 0.7.0-SNAPSHOT org.hjug.refactorfirst.plugin @@ -24,10 +24,20 @@ + + + com.google.guava + guava + + + + org.iq80.snappy + snappy + 0.5 + org.apache.maven maven-core - ${maven.core.version} diff --git a/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstHtmlReport.java b/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstHtmlReport.java index b7b378f9..7d86e8f0 100644 --- a/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstHtmlReport.java +++ b/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstHtmlReport.java @@ -23,6 +23,24 @@ public class RefactorFirstHtmlReport extends AbstractMojo { @Parameter(property = "showDetails") private boolean showDetails = false; + @Parameter(property = "backEdgeAnalysisCount") + protected int backEdgeAnalysisCount = 50; + + @Parameter(property = "analyzeCycles") + private boolean analyzeCycles = true; + + @Parameter(property = "minifyHtml") + private boolean minifyHtml = false; + + @Parameter(property = "excludeTests") + private boolean excludeTests = true; + + /** + * The test source directory containing test class sources. + */ + @Parameter(property = "testSourceDirectory") + private String testSourceDirectory; + @Parameter(defaultValue = "${project.name}") private String projectName; @@ -41,13 +59,18 @@ public void execute() { log.info(outputDirectory.getPath()); HtmlReport htmlReport = new HtmlReport(); htmlReport.execute( + backEdgeAnalysisCount, + analyzeCycles, showDetails, + minifyHtml, + excludeTests, + testSourceDirectory, projectName, projectVersion, + project.getBasedir(), project.getModel() .getReporting() .getOutputDirectory() - .replace("${project.basedir}" + File.separator, ""), - project.getBasedir()); + .replace("${project.basedir}" + File.separator, "")); } } diff --git a/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenReport.java b/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenReport.java index 9c03b906..26c27930 100644 --- a/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenReport.java +++ b/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenReport.java @@ -1,11 +1,5 @@ package org.hjug.mavenreport; -import java.io.File; -import java.nio.file.Paths; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; import java.util.*; import lombok.extern.slf4j.Slf4j; import org.apache.maven.doxia.markup.HtmlMarkup; @@ -17,14 +11,7 @@ import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.reporting.AbstractMavenReport; -import org.apache.maven.reporting.MavenReportException; -import org.hjug.cbc.CostBenefitCalculator; -import org.hjug.cbc.RankedCycle; -import org.hjug.cbc.RankedDisharmony; -import org.hjug.gdg.GraphDataGenerator; -import org.hjug.git.GitLogReader; -import org.jgrapht.Graph; -import org.jgrapht.graph.DefaultWeightedEdge; +import org.hjug.refactorfirst.report.HtmlReport; @Slf4j @Mojo( @@ -39,16 +26,27 @@ public class RefactorFirstMavenReport extends AbstractMavenReport { @Parameter(property = "showDetails") private boolean showDetails = false; + @Parameter(property = "backEdgeAnalysisCount") + protected int backEdgeAnalysisCount = 50; + + @Parameter(property = "analyzeCycles") + private boolean analyzeCycles = true; + + @Parameter(property = "excludeTests") + private boolean excludeTests = true; + + /** + * The test source directory containing test class sources. + */ + @Parameter(property = "testSourceDirectory") + private String testSourceDirectory; + @Parameter(defaultValue = "${project.name}") private String projectName; @Parameter(defaultValue = "${project.version}") private String projectVersion; - private DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) - .withLocale(Locale.getDefault()) - .withZone(ZoneId.systemDefault()); - public String getOutputName() { // This report will generate simple-report.html when invoked in a project with `mvn site` return "refactor-first-report"; @@ -65,813 +63,75 @@ public String getDescription(Locale locale) { + " have the highest priority values."; } - public final String[] classCycleTableHeadings = {"Classes", "Relationships"}; - - private Graph classGraph; - @Override - public void executeReport(Locale locale) throws MavenReportException { - - final String[] godClassSimpleTableHeadings = { - "Class", - "Priority", - "Change Proneness Rank", - "Effort Rank", - "Method Count", - "Most Recent Commit Date", - "Commit Count" - }; - - final String[] godClassDetailedTableHeadings = { - "Class", - "Priority", - "Raw Priority", - "Change Proneness Rank", - "Effort Rank", - "WMC", - "WMC Rank", - "ATFD", - "ATFD Rank", - "TCC", - "TCC Rank", - "Date of First Commit", - "Most Recent Commit Date", - "Commit Count", - "Full Path" - }; - - final String[] cboTableHeadings = { - "Class", "Priority", "Change Proneness Rank", "Coupling Count", "Most Recent Commit Date", "Commit Count" - }; + public void executeReport(Locale locale) { + HtmlReport htmlReport = new HtmlReport(); - final String[] godClassTableHeadings = - showDetails ? godClassDetailedTableHeadings : godClassSimpleTableHeadings; - - if (Objects.equals(project.getName(), "Maven Stub Project (No POM)")) { - projectName = new File(Paths.get("").toAbsolutePath().toString()).getName(); - } - - String filename = getOutputName() + ".html"; - log.info("Generating {} for {} - {}", filename, projectName, projectVersion); - - // Get the Maven Doxia Sink, which will be used to generate the - // various elements of the document Sink mainSink = getSink(); - if (mainSink == null) { - throw new MavenReportException("Could not get the Doxia sink"); - } - - // Page head + printHead(mainSink); + String report = htmlReport + .generateReport( + showDetails, + backEdgeAnalysisCount, + analyzeCycles, + excludeTests, + testSourceDirectory, + projectName, + projectVersion, + project.getBasedir(), + 300) + .toString(); + + mainSink.rawText(report); + } + + private void printHead(Sink mainSink) { mainSink.head(); mainSink.title(); mainSink.text("Refactor First Report for " + projectName + " " + projectVersion); mainSink.title_(); - /** - * @See https://maven.apache.org/doxia/developers/sink.html#How_to_inject_javascript_code_into_HTML - */ - SinkEventAttributeSet githubButtonJS = new SinkEventAttributeSet(); - githubButtonJS.addAttribute(SinkEventAttributes.SRC, "https://buttons.github.io/buttons.js"); - - String script = "script"; - mainSink.unknown(script, new Object[] {HtmlMarkup.TAG_TYPE_START}, githubButtonJS); - mainSink.unknown(script, new Object[] {HtmlMarkup.TAG_TYPE_END}, null); - - SinkEventAttributeSet googleChartImport = new SinkEventAttributeSet(); - googleChartImport.addAttribute(SinkEventAttributes.TYPE, "text/javascript"); - googleChartImport.addAttribute(SinkEventAttributes.SRC, "https://www.gstatic.com/charts/loader.js"); - - mainSink.unknown(script, new Object[] {HtmlMarkup.TAG_TYPE_START}, googleChartImport); - mainSink.unknown(script, new Object[] {HtmlMarkup.TAG_TYPE_END}, null); - - SinkEventAttributeSet d3js = new SinkEventAttributeSet(); - d3js.addAttribute(SinkEventAttributes.TYPE, "text/javascript"); - d3js.addAttribute(SinkEventAttributes.SRC, "https://d3js.org/d3.v5.min.js"); - - mainSink.unknown(script, new Object[] {HtmlMarkup.TAG_TYPE_START}, d3js); - mainSink.unknown(script, new Object[] {HtmlMarkup.TAG_TYPE_END}, null); - - SinkEventAttributeSet graphViz = new SinkEventAttributeSet(); - graphViz.addAttribute(SinkEventAttributes.TYPE, "text/javascript"); - graphViz.addAttribute(SinkEventAttributes.SRC, "https://unpkg.com/d3-graphviz@3.0.5/build/d3-graphviz.min.js"); - - mainSink.unknown(script, new Object[] {HtmlMarkup.TAG_TYPE_START}, graphViz); - mainSink.unknown(script, new Object[] {HtmlMarkup.TAG_TYPE_END}, null); - - SinkEventAttributeSet wasm = new SinkEventAttributeSet(); - wasm.addAttribute(SinkEventAttributes.TYPE, "text/javascript"); - wasm.addAttribute(SinkEventAttributes.SRC, "https://unpkg.com/@hpcc-js/wasm@0.3.11/dist/index.min.js"); - - mainSink.unknown(script, new Object[] {HtmlMarkup.TAG_TYPE_START}, wasm); - mainSink.unknown(script, new Object[] {HtmlMarkup.TAG_TYPE_END}, null); + // GH Buttons import + renderJsDeclaration(mainSink, "https://buttons.github.io/buttons.js"); + // google chart import + renderJsDeclaration(mainSink, "https://www.gstatic.com/charts/loader.js"); + // d3 dot graph imports + renderJsDeclaration(mainSink, "https://d3js.org/d3.v5.min.js"); + renderJsDeclaration(mainSink, "https://cdnjs.cloudflare.com/ajax/libs/d3-graphviz/3.0.5/d3-graphviz.min.js"); + renderJsDeclaration(mainSink, "https://unpkg.com/@hpcc-js/wasm@0.3.11/dist/index.min.js"); + + // sigma graph imports - sigma, graphology, graphlib, and graphlib-dot + renderJsDeclaration(mainSink, "https://cdnjs.cloudflare.com/ajax/libs/sigma.js/2.4.0/sigma.min.js"); + renderJsDeclaration(mainSink, "https://cdnjs.cloudflare.com/ajax/libs/graphology/0.25.4/graphology.umd.min.js"); + // may only need graphlib-dot + renderJsDeclaration(mainSink, "https://cdnjs.cloudflare.com/ajax/libs/graphlib/2.1.8/graphlib.min.js"); + renderJsDeclaration(mainSink, "https://cdn.jsdelivr.net/npm/graphlib-dot@0.6.4/dist/graphlib-dot.min.js"); + renderJsDeclaration(mainSink, "https://unpkg.com/3d-force-graph"); + + // renderJsDeclaration(mainSink, HtmlReport.SUGIYAMA_SIGMA_GRAPH); + // renderJsDeclaration(mainSink, HtmlReport.FORCE_3D_GRAPH); + // renderJsDeclaration(mainSink, HtmlReport.POPUP_FUNCTIONS); + + // renderStyle(mainSink); mainSink.head_(); - - mainSink.body(); - - // Heading 1 - mainSink.section1(); - mainSink.sectionTitle1(); - mainSink.text("RefactorFirst Report for " + projectName + " " + projectVersion); - mainSink.sectionTitle1_(); - - GitLogReader gitLogReader = new GitLogReader(); - String projectBaseDir; - Optional optionalGitDir; - - File baseDir = project.getBasedir(); - if (baseDir != null) { - projectBaseDir = baseDir.getPath(); - optionalGitDir = Optional.ofNullable(gitLogReader.getGitDir(baseDir)); - } else { - projectBaseDir = Paths.get("").toAbsolutePath().toString(); - optionalGitDir = Optional.ofNullable(gitLogReader.getGitDir(new File(projectBaseDir))); - } - - File gitDir; - if (optionalGitDir.isPresent()) { - gitDir = optionalGitDir.get(); - } else { - log.info( - "Done! No Git repository found! Please initialize a Git repository and perform an initial commit."); - - StringBuilder stringBuilder = new StringBuilder(); - stringBuilder - .append("No Git repository found in project ") - .append(projectName) - .append(" ") - .append(projectVersion) - .append(". "); - stringBuilder.append("Please initialize a Git repository and perform an initial commit."); - mainSink.text(stringBuilder.toString()); - return; - } - - String parentOfGitDir = gitDir.getParentFile().getPath(); - log.info("Project Base Dir: {} ", projectBaseDir); - log.info("Parent of Git Dir: {}", parentOfGitDir); - - if (!projectBaseDir.equals(parentOfGitDir)) { - log.warn("Project Base Directory does not match Git Parent Directory"); - mainSink.text("Project Base Directory does not match Git Parent Directory. " - + "Please refer to the report at the root of the site directory."); - return; - } - - List rankedGodClassDisharmonies; - List rankedCBODisharmonies; - List rankedCycles; - try (CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(projectBaseDir)) { - costBenefitCalculator.runPmdAnalysis(); - rankedGodClassDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues(); - rankedCBODisharmonies = costBenefitCalculator.calculateCBOCostBenefitValues(); - rankedCycles = runCycleAnalysis(costBenefitCalculator, outputDirectory.getPath()); - classGraph = costBenefitCalculator.getClassReferencesGraph(); - } catch (Exception e) { - log.error("Error running analysis."); - throw new RuntimeException(e); - } - - if (rankedGodClassDisharmonies.isEmpty() && rankedCBODisharmonies.isEmpty() && rankedCycles.isEmpty()) { - mainSink.text("Contratulations! " + projectName + " " + projectVersion - + " has no God classes, highly coupled classes, or cycles!"); - mainSink.section1_(); - renderGitHubButtons(mainSink); - mainSink.body_(); - log.info("Done! No Disharmonies found!"); - return; - } - - /* if (!rankedGodClassDisharmonies.isEmpty() && !rankedCBODisharmonies.isEmpty()) { - SinkEventAttributeSet godClassesLink = new SinkEventAttributeSet(); - godClassesLink.addAttribute(SinkEventAttributes.SRC, "#GOD"); - mainSink.anchor("God Classes", godClassesLink); - mainSink.anchor_(); - - mainSink.lineBreak(); - - SinkEventAttributeSet cboClassesLink = new SinkEventAttributeSet(); - godClassesLink.addAttribute(SinkEventAttributes.SRC, "#CBO"); - mainSink.anchor("Highly Coupled Classes", cboClassesLink); - mainSink.anchor_(); - }*/ - - if (!rankedGodClassDisharmonies.isEmpty()) { - int maxGodClassPriority = rankedGodClassDisharmonies - .get(rankedGodClassDisharmonies.size() - 1) - .getPriority(); - - SinkEventAttributeSet alignCenter = new SinkEventAttributeSet(); - alignCenter.addAttribute(SinkEventAttributes.ALIGN, "center"); - - mainSink.division(alignCenter); - mainSink.section2(); - mainSink.sectionTitle2(); - mainSink.text("God Classes"); - mainSink.sectionTitle2_(); - mainSink.section2_(); - mainSink.division_(); - - String godClassScript = writeGodClassGchartJs(rankedGodClassDisharmonies, maxGodClassPriority - 1); - SinkEventAttributeSet seriesChartDiv = new SinkEventAttributeSet(); - seriesChartDiv.addAttribute(SinkEventAttributes.ID, "series_chart_div"); - seriesChartDiv.addAttribute(SinkEventAttributes.ALIGN, "center"); - mainSink.division(seriesChartDiv); - mainSink.division_(); - - SinkEventAttributeSet godClassJavascript = new SinkEventAttributeSet(); - godClassJavascript.addAttribute(SinkEventAttributes.TYPE, "text/javascript"); - mainSink.unknown(script, new Object[] {HtmlMarkup.TAG_TYPE_START}, godClassJavascript); - mainSink.rawText(godClassScript); - mainSink.unknown(script, new Object[] {HtmlMarkup.TAG_TYPE_END}, null); - - renderGitHubButtons(mainSink); - - String legendHeading = "God Class Chart Legend:"; - String xAxis = "Effort to refactor to a non-God class"; - renderLegend(mainSink, legendHeading, xAxis); - - /* - *God Class table - */ - mainSink.lineBreak(); - mainSink.lineBreak(); - mainSink.division(alignCenter); - mainSink.section3(); - mainSink.sectionTitle3(); - mainSink.text("God classes by the numbers: (Refactor Starting with Priority 1)"); - mainSink.sectionTitle3_(); - mainSink.section3_(); - mainSink.division_(); - - mainSink.table(); - mainSink.tableRows(new int[] {Sink.JUSTIFY_LEFT}, true); - - // header row - mainSink.tableRow(); - for (String heading : godClassTableHeadings) { - drawTableHeaderCell(heading, mainSink); - } - mainSink.tableRow_(); - - for (RankedDisharmony rankedGodClassDisharmony : rankedGodClassDisharmonies) { - mainSink.tableRow(); - - Object[] simpleRankedGodClassDisharmonyData = { - rankedGodClassDisharmony.getFileName(), - rankedGodClassDisharmony.getPriority(), - rankedGodClassDisharmony.getChangePronenessRank(), - rankedGodClassDisharmony.getEffortRank(), - rankedGodClassDisharmony.getWmc(), - rankedGodClassDisharmony.getMostRecentCommitTime(), - rankedGodClassDisharmony.getCommitCount() - }; - - Object[] detailedRankedGodClassDisharmonyData = { - rankedGodClassDisharmony.getFileName(), - rankedGodClassDisharmony.getPriority(), - rankedGodClassDisharmony.getRawPriority(), - rankedGodClassDisharmony.getChangePronenessRank(), - rankedGodClassDisharmony.getEffortRank(), - rankedGodClassDisharmony.getWmc(), - rankedGodClassDisharmony.getWmcRank(), - rankedGodClassDisharmony.getAtfd(), - rankedGodClassDisharmony.getAtfdRank(), - rankedGodClassDisharmony.getTcc(), - rankedGodClassDisharmony.getTccRank(), - rankedGodClassDisharmony.getFirstCommitTime(), - rankedGodClassDisharmony.getMostRecentCommitTime(), - rankedGodClassDisharmony.getCommitCount(), - rankedGodClassDisharmony.getPath() - }; - - final Object[] rankedDisharmonyData = - showDetails ? detailedRankedGodClassDisharmonyData : simpleRankedGodClassDisharmonyData; - - for (Object rowData : rankedDisharmonyData) { - drawTableCell(rowData, mainSink); - } - - mainSink.tableRow_(); - } - - mainSink.tableRows_(); - mainSink.table_(); - } - - if (!rankedCBODisharmonies.isEmpty()) { - if (!rankedGodClassDisharmonies.isEmpty()) { - mainSink.lineBreak(); - mainSink.lineBreak(); - mainSink.lineBreak(); - mainSink.horizontalRule(); - mainSink.lineBreak(); - mainSink.lineBreak(); - } - - int maxCboPriority = - rankedCBODisharmonies.get(rankedCBODisharmonies.size() - 1).getPriority(); - - SinkEventAttributeSet alignCenter = new SinkEventAttributeSet(); - alignCenter.addAttribute(SinkEventAttributes.ALIGN, "center"); - - mainSink.division(alignCenter); - mainSink.section2(); - mainSink.sectionTitle2(); - mainSink.text("Highly Coupled Classes"); - mainSink.sectionTitle2_(); - mainSink.section2_(); - mainSink.division_(); - - SinkEventAttributeSet seriesChartDiv = new SinkEventAttributeSet(); - seriesChartDiv.addAttribute(SinkEventAttributes.ID, "series_chart_div_2"); - seriesChartDiv.addAttribute(SinkEventAttributes.ALIGN, "center"); - mainSink.division(seriesChartDiv); - mainSink.division_(); - - String cboScript = writeGCBOGchartJs(rankedCBODisharmonies, maxCboPriority - 1); - - SinkEventAttributeSet cboJavascript = new SinkEventAttributeSet(); - cboJavascript.addAttribute(SinkEventAttributes.TYPE, "text/javascript"); - mainSink.unknown(script, new Object[] {HtmlMarkup.TAG_TYPE_START}, cboJavascript); - mainSink.rawText(cboScript); - mainSink.unknown(script, new Object[] {HtmlMarkup.TAG_TYPE_END}, null); - - renderGitHubButtons(mainSink); - - String legendHeading = "Highly Coupled Classes Chart Legend:"; - String xAxis = "Number of objects the class is coupled to"; - renderLegend(mainSink, legendHeading, xAxis); - - mainSink.division(alignCenter); - mainSink.section3(); - mainSink.sectionTitle3(); - mainSink.text("Highly Coupled classes by the numbers: (Refactor Starting with Priority 1)"); - mainSink.sectionTitle3_(); - mainSink.section3_(); - mainSink.division_(); - - SinkEventAttributeSet disharmonyTable = new SinkEventAttributeSet(); - mainSink.table(disharmonyTable); - mainSink.tableRows(new int[] {Sink.JUSTIFY_LEFT}, true); - mainSink.tableRow(); - // Content - for (String heading : cboTableHeadings) { - drawTableHeaderCell(heading, mainSink); - } - mainSink.tableRow_(); - - for (RankedDisharmony rankedCboClassDisharmony : rankedCBODisharmonies) { - mainSink.tableRow(); - - String[] rankedCboClassDisharmonyData = { - rankedCboClassDisharmony.getFileName(), - rankedCboClassDisharmony.getPriority().toString(), - rankedCboClassDisharmony.getChangePronenessRank().toString(), - rankedCboClassDisharmony.getEffortRank().toString(), - formatter.format(rankedCboClassDisharmony.getMostRecentCommitTime()), - rankedCboClassDisharmony.getCommitCount().toString() - }; - - for (String rowData : rankedCboClassDisharmonyData) { - drawTableCell(rowData, mainSink); - } - - mainSink.tableRow_(); - } - } - mainSink.tableRows_(); - mainSink.table_(); - - if (!rankedCycles.isEmpty()) { - mainSink.lineBreak(); - mainSink.lineBreak(); - mainSink.horizontalRule(); - mainSink.lineBreak(); - mainSink.lineBreak(); - - renderCycles(outputDirectory.getPath(), mainSink, rankedCycles, formatter); - } - - // Close - mainSink.section1_(); - mainSink.body_(); - - log.info("Done! View the report at target/site/{}", filename); - } - - public List runCycleAnalysis(CostBenefitCalculator costBenefitCalculator, String outputDirectory) { - return costBenefitCalculator.runCycleAnalysis(); - } - - private void renderCycles( - String outputDirectory, Sink mainSink, List rankedCycles, DateTimeFormatter formatter) { - - SinkEventAttributeSet alignCenter = new SinkEventAttributeSet(); - alignCenter.addAttribute(SinkEventAttributes.ALIGN, "center"); - - mainSink.division(alignCenter); - mainSink.section1(); - mainSink.sectionTitle1(); - mainSink.text("Class Cycles"); - mainSink.sectionTitle1_(); - mainSink.section1_(); - mainSink.division_(); - - mainSink.division(alignCenter); - mainSink.section2(); - mainSink.sectionTitle2(); - mainSink.text("Class Cycles by the numbers: (Refactor starting with Priority 1)"); - mainSink.sectionTitle2_(); - mainSink.section2_(); - mainSink.division_(); - - mainSink.paragraph(alignCenter); - mainSink.text("Note: often only one minimum cut relationship needs to be removed"); - mainSink.paragraph_(); - - mainSink.table(); - mainSink.tableRows(new int[] {Sink.JUSTIFY_LEFT}, true); - - // Content - // header row - - String[] cycleTableHeadings; - if (showDetails) { - cycleTableHeadings = new String[] { - "Cycle Name", "Priority", "Change Proneness Rank", "Class Count", "Relationship Count", "Minimum Cuts" - }; - } else { - cycleTableHeadings = - new String[] {"Cycle Name", "Priority", "Class Count", "Relationship Count", "Minimum Cuts"}; - } - - mainSink.tableRow(); - for (String heading : cycleTableHeadings) { - drawTableHeaderCell(heading, mainSink); - } - mainSink.tableRow_(); - - for (RankedCycle rankedCycle : rankedCycles) { - mainSink.tableRow(); - - StringBuilder edgesToCut = new StringBuilder(); - for (DefaultWeightedEdge minCutEdge : rankedCycle.getMinCutEdges()) { - edgesToCut.append(minCutEdge + ":" + (int) classGraph.getEdgeWeight(minCutEdge)); - } - - String[] rankedCycleData; - if (showDetails) { - // "Cycle Name", "Priority", "Change Proneness Rank", "Class Count", "Relationship Count", "Min Cuts" - rankedCycleData = new String[] { - rankedCycle.getCycleName(), - rankedCycle.getPriority().toString(), - rankedCycle.getChangePronenessRank().toString(), - String.valueOf(rankedCycle.getCycleNodes().size()), - String.valueOf(rankedCycle.getEdgeSet().size()), - edgesToCut.toString() - }; - } else { - // "Cycle Name", "Priority", "Class Count", "Relationship Count", "Min Cuts" - rankedCycleData = new String[] { - rankedCycle.getCycleName(), - rankedCycle.getPriority().toString(), - String.valueOf(rankedCycle.getCycleNodes().size()), - String.valueOf(rankedCycle.getEdgeSet().size()), - edgesToCut.toString() - }; - } - - for (String rowData : rankedCycleData) { - drawCycleTableCell(rowData, mainSink); - } - - mainSink.tableRow_(); - } - mainSink.tableRows_(); - - mainSink.table_(); - - for (RankedCycle rankedCycle : rankedCycles) { - renderCycle(outputDirectory, mainSink, rankedCycle, formatter); - } - } - - private void renderCycle(String outputDirectory, Sink mainSink, RankedCycle cycle, DateTimeFormatter formatter) { - - mainSink.lineBreak(); - mainSink.lineBreak(); - mainSink.lineBreak(); - mainSink.lineBreak(); - mainSink.lineBreak(); - - SinkEventAttributeSet alignCenter = new SinkEventAttributeSet(); - alignCenter.addAttribute(SinkEventAttributes.ALIGN, "center"); - - mainSink.division(alignCenter); - mainSink.section2(); - mainSink.sectionTitle2(); - mainSink.text("Class Cycle : " + cycle.getCycleName()); - mainSink.sectionTitle2_(); - mainSink.section2_(); - mainSink.division_(); - - renderCycleImage(classGraph, cycle, mainSink); - - mainSink.division(alignCenter); - mainSink.bold(); - mainSink.text("\"*\" indicates relationship(s) to remove to decompose cycle"); - mainSink.bold_(); - mainSink.division_(); - - mainSink.table(); - mainSink.tableRows(new int[] {Sink.JUSTIFY_LEFT}, true); - - // Content - mainSink.tableRow(); - for (String heading : classCycleTableHeadings) { - drawTableHeaderCell(heading, mainSink); - } - mainSink.tableRow_(); - - for (String vertex : cycle.getVertexSet()) { - mainSink.tableRow(); - drawTableCell(vertex, mainSink); - StringBuilder edges = new StringBuilder(); - for (org.jgrapht.graph.DefaultWeightedEdge edge : cycle.getEdgeSet()) { - if (edge.toString().startsWith("(" + vertex + " :")) { - if (cycle.getMinCutEdges().contains(edge)) { - edges.append(edge); - edges.append(":") - .append((int) classGraph.getEdgeWeight(edge)) - .append("*"); - } else { - edges.append(edge); - edges.append(":").append((int) classGraph.getEdgeWeight(edge)); - } - } - } - drawCycleTableCell(edges.toString(), mainSink); - mainSink.tableRow_(); - } - - mainSink.tableRows_(); - mainSink.table_(); - } - - private void renderLegend(Sink mainSink, String legendHeading, String xAxis) { - SinkEventAttributeSet width = new SinkEventAttributeSet(); - width.addAttribute(SinkEventAttributes.STYLE, "width:350px"); - mainSink.division(width); - mainSink.table(); - mainSink.tableRows(new int[] {Sink.JUSTIFY_LEFT}, true); - mainSink.tableRow(width); - drawTableHeaderCell(legendHeading, mainSink); - mainSink.tableRow_(); - legendRow(mainSink, "X-Axis:", xAxis); - legendRow(mainSink, "Y-Axis:", "Relative Churn"); - legendRow(mainSink, "Color:", "Priority of what to fix first"); - legendRow(mainSink, "Circle Size:", "Priority (Visual) of what to fix first"); - mainSink.tableRows_(); - mainSink.table_(); - mainSink.division_(); - } - - private static void legendRow(Sink mainSink, String boldText, String explanation) { - mainSink.tableRow(); - mainSink.tableCell(); - mainSink.bold(); - mainSink.text(boldText); - mainSink.bold_(); - mainSink.text(explanation); - mainSink.tableCell_(); - mainSink.tableRow_(); - } - - void drawTableHeaderCell(String cellText, Sink mainSink) { - mainSink.tableHeaderCell(); - mainSink.text(cellText); - mainSink.tableHeaderCell_(); - } - - void drawTableCell(Object cellText, Sink mainSink) { - SinkEventAttributeSet align = new SinkEventAttributeSet(); - if (cellText instanceof Integer || cellText instanceof Instant) { - align.addAttribute(SinkEventAttributes.ALIGN, "right"); - } else { - align.addAttribute(SinkEventAttributes.ALIGN, "left"); - } - - mainSink.tableCell(align); - - if (cellText instanceof Instant) { - mainSink.text(formatter.format((Instant) cellText)); - } else { - mainSink.text(cellText.toString()); - } - - mainSink.tableCell_(); - } - - void drawCycleTableCell(String cellText, Sink mainSink) { - SinkEventAttributeSet align = new SinkEventAttributeSet(); - align.addAttribute(SinkEventAttributes.ALIGN, "left"); - - mainSink.tableCell(align); - - for (String string : cellText.split("\\(")) { - if (string.contains("*")) { - mainSink.bold(); - mainSink.text("(" + string); - mainSink.bold_(); - } else { - if (string.contains(")")) { - mainSink.text("(" + string); - } else { - mainSink.text(string); - } - } - - mainSink.lineBreak(); - } - - mainSink.tableCell_(); - } - - /* - Star - Fork - Watch - Issue - Sponsor - */ - void renderGitHubButtons(Sink mainSink) { - SinkEventAttributeSet alignCenter = new SinkEventAttributeSet(); - alignCenter.addAttribute(SinkEventAttributes.ALIGN, "center"); - - mainSink.division(alignCenter); - mainSink.text("Show RefactorFirst some ❤️"); - mainSink.lineBreak(); - - renderGitHubButton( - mainSink, - "https://github.com/refactorfirst/refactorfirst", - "octicon-star", - "true", - "Star refactorfirst/refactorfirst on GitHub", - "Star"); - renderGitHubButton( - mainSink, - "https://github.com/refactorfirst/refactorfirst/fork", - "octicon-repo-forked", - "true", - "Fork refactorfirst/refactorfirst on GitHub", - "Fork"); - renderGitHubButton( - mainSink, - "https://github.com/refactorfirst/refactorfirst/subscription", - "octicon-eye", - "true", - "Watch refactorfirst/refactorfirst on GitHub", - "Watch"); - renderGitHubButton( - mainSink, - "https://github.com/refactorfirst/refactorfirst/issue", - "octicon-issue-opened", - "false", - "Issue refactorfirst/refactorfirst on GitHub", - "Issue"); - renderGitHubButton( - mainSink, - "https://github.com/jimbethancourt/refactorfirst/issue", - "octicon-heart", - "false", - "Sponsor @jimbethancourt on GitHub", - "Sponsor"); - - mainSink.division_(); - } - - private static void renderGitHubButton( - Sink mainSink, - String url, - String dataIconValue, - String dataShowCount, - String ariaLabel, - String anchorText) { - SinkEventAttributeSet starButton = new SinkEventAttributeSet(); - starButton.addAttribute(SinkEventAttributes.HREF, url); - starButton.addAttribute("class", "github-button"); - starButton.addAttribute("data-icon", dataIconValue); - starButton.addAttribute("data-size", "large"); - starButton.addAttribute("data-show-count", dataShowCount); - starButton.addAttribute("aria-label", ariaLabel); - mainSink.unknown("a", new Object[] {HtmlMarkup.TAG_TYPE_START}, starButton); - mainSink.text(anchorText); - mainSink.unknown("a", new Object[] {HtmlMarkup.TAG_TYPE_END}, null); } - String writeGodClassGchartJs(List rankedDisharmonies, int maxPriority) { - GraphDataGenerator graphDataGenerator = new GraphDataGenerator(); - String scriptStart = graphDataGenerator.getGodClassScriptStart(); - String bubbleChartData = graphDataGenerator.generateGodClassBubbleChartData(rankedDisharmonies, maxPriority); - String scriptEnd = graphDataGenerator.getGodClassScriptEnd(); - - return scriptStart + bubbleChartData + scriptEnd; - } - - String writeGCBOGchartJs(List rankedDisharmonies, int maxPriority) { - GraphDataGenerator graphDataGenerator = new GraphDataGenerator(); - String scriptStart = graphDataGenerator.getCBOScriptStart(); - String bubbleChartData = graphDataGenerator.generateCBOBubbleChartData(rankedDisharmonies, maxPriority); - String scriptEnd = graphDataGenerator.getCBOScriptEnd(); - - return scriptStart + bubbleChartData + scriptEnd; - } - - void renderCycleImage(Graph classGraph, RankedCycle cycle, Sink mainSink) { - - SinkEventAttributeSet graphDivAttrs = new SinkEventAttributeSet(); - graphDivAttrs.addAttribute(SinkEventAttributes.ALIGN, "center"); - graphDivAttrs.addAttribute(SinkEventAttributes.ID, cycle.getCycleName()); - graphDivAttrs.addAttribute(SinkEventAttributes.STYLE, "border: thin solid black"); - - mainSink.division(graphDivAttrs); - mainSink.division_(); - - String dot = buildDot(classGraph, cycle); - - StringBuilder d3chart = new StringBuilder(); - d3chart.append("d3.select(\"#" + cycle.getCycleName() + "\")\n"); - d3chart.append(".graphviz()\n"); - d3chart.append(".width(screen.width - 300)\n"); - d3chart.append(".height(screen.height)\n"); - d3chart.append(".fit(true)\n"); - d3chart.append(".renderDot(" + dot + ");\n"); - - SinkEventAttributeSet dotChartScript = new SinkEventAttributeSet(); - dotChartScript.addAttribute(SinkEventAttributes.TYPE, "text/javascript"); - - String script = "script"; - mainSink.unknown(script, new Object[] {HtmlMarkup.TAG_TYPE_START}, dotChartScript); - - mainSink.rawText(d3chart.toString()); - mainSink.unknown(script, new Object[] {HtmlMarkup.TAG_TYPE_END}, null); - - SinkEventAttributeSet alignCenter = new SinkEventAttributeSet(); - alignCenter.addAttribute(SinkEventAttributes.ALIGN, "center"); - - mainSink.paragraph(alignCenter); - mainSink.text("Red arrows represent relationship(s) to remove to decompose cycle"); - mainSink.paragraph_(); - - mainSink.lineBreak(); - mainSink.lineBreak(); + /** + * @See https://maven.apache.org/doxia/developers/sink.html#How_to_inject_javascript_code_into_HTML + */ + private void renderJsDeclaration(Sink mainSink, String scriptUrl) { + SinkEventAttributeSet githubButtonJS = new SinkEventAttributeSet(); + githubButtonJS.addAttribute(SinkEventAttributes.TYPE, "text/javascript"); + githubButtonJS.addAttribute(SinkEventAttributes.SRC, scriptUrl); + mainSink.unknown("script", new Object[] {HtmlMarkup.TAG_TYPE_START}, githubButtonJS); + mainSink.unknown("script", new Object[] {HtmlMarkup.TAG_TYPE_END}, null); } - String buildDot(Graph classGraph, RankedCycle cycle) { - StringBuilder dot = new StringBuilder(); - - dot.append("'strict digraph G {\\n' +\n"); - - // render vertices - // e.g DownloadManager; - for (String vertex : cycle.getVertexSet()) { - dot.append("'"); - dot.append(vertex); - dot.append(";\\n' +\n"); - } - - for (DefaultWeightedEdge edge : cycle.getEdgeSet()) { - // 'DownloadManager -> Download [ label="1" color="red" ];' - - // render edge - String[] vertexes = - edge.toString().replace("(", "").replace(")", "").split(":"); - - String start = vertexes[0].trim(); - String end = vertexes[1].trim(); - - dot.append("'"); - dot.append(start); - dot.append(" -> "); - dot.append(end); - - // render edge attributes - dot.append(" [ "); - dot.append("label = \""); - dot.append((int) classGraph.getEdgeWeight(edge)); - dot.append("\""); - - if (cycle.getMinCutEdges().contains(edge)) { - dot.append(" color = \"red\""); - } - - dot.append(" ];\\n' +\n"); - } - - dot.append("'}'"); - - return dot.toString(); + private void renderStyle(Sink mainSink) { + SinkEventAttributeSet githubButtonJS = new SinkEventAttributeSet(); + githubButtonJS.addAttribute(SinkEventAttributes.SRC, HtmlReport.POPUP_STYLE); + mainSink.unknown("script", new Object[] {HtmlMarkup.TAG_TYPE_START}, githubButtonJS); + mainSink.unknown("script", new Object[] {HtmlMarkup.TAG_TYPE_END}, null); } } diff --git a/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstSimpleHtmlReport.java b/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstSimpleHtmlReport.java index 40cefb2d..f1fb9b55 100644 --- a/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstSimpleHtmlReport.java +++ b/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstSimpleHtmlReport.java @@ -23,6 +23,24 @@ public class RefactorFirstSimpleHtmlReport extends AbstractMojo { @Parameter(property = "showDetails") private boolean showDetails = false; + @Parameter(property = "backEdgeAnalysisCount") + private int backEdgeAnalysisCount = 50; + + @Parameter(property = "analyzeCycles") + private boolean analyzeCycles = true; + + @Parameter(property = "minifyHtml") + private boolean minifyHtml = false; + + @Parameter(property = "excludeTests") + private boolean excludeTests = true; + + /** + * The test source directory containing test class sources. + */ + @Parameter(property = "testSourceDirectory") + private String testSourceDirectory; + @Parameter(defaultValue = "${project.name}") private String projectName; @@ -41,13 +59,18 @@ public void execute() { log.info(outputDirectory.getPath()); SimpleHtmlReport htmlReport = new SimpleHtmlReport(); htmlReport.execute( + backEdgeAnalysisCount, + analyzeCycles, showDetails, + minifyHtml, + excludeTests, + testSourceDirectory, projectName, projectVersion, + project.getBasedir(), project.getModel() .getReporting() .getOutputDirectory() - .replace("${project.basedir}" + File.separator, ""), - project.getBasedir()); + .replace("${project.basedir}" + File.separator, "")); } } diff --git a/report/pom.xml b/report/pom.xml index 6f79b22e..17ea3699 100644 --- a/report/pom.xml +++ b/report/pom.xml @@ -4,7 +4,7 @@ org.hjug.refactorfirst refactor-first - 0.6.3-SNAPSHOT + 0.7.0-SNAPSHOT org.hjug.refactorfirst.report @@ -19,6 +19,11 @@ com.fasterxml.jackson.core jackson-databind + + + in.wilsonl.minifyhtml + minify-html + \ No newline at end of file diff --git a/report/src/main/java/org/hjug/refactorfirst/report/CsvReport.java b/report/src/main/java/org/hjug/refactorfirst/report/CsvReport.java index 30ee3455..e63a2650 100644 --- a/report/src/main/java/org/hjug/refactorfirst/report/CsvReport.java +++ b/report/src/main/java/org/hjug/refactorfirst/report/CsvReport.java @@ -66,7 +66,7 @@ public void execute( .append(projectVersion) .append(". "); contentBuilder.append("Please initialize a Git repository and perform an initial commit."); - writeReportToDisk(outputDirectory, filename, contentBuilder); + writeReportToDisk(outputDirectory, filename, contentBuilder.toString()); return; } @@ -103,7 +103,7 @@ public void execute( .append(" has no God classes!"); log.info("Done! No God classes found!"); - writeReportToDisk(outputDirectory, filename, contentBuilder); + writeReportToDisk(outputDirectory, filename, contentBuilder.toString()); return; } @@ -123,7 +123,7 @@ public void execute( log.info(contentBuilder.toString()); - writeReportToDisk(outputDirectory, filename, contentBuilder); + writeReportToDisk(outputDirectory, filename, contentBuilder.toString()); } private DateTimeFormatter createFileDateTimeFormatter() { diff --git a/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java b/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java index 11e60d47..c09526d6 100644 --- a/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java +++ b/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java @@ -1,7 +1,9 @@ package org.hjug.refactorfirst.report; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Set; import lombok.extern.slf4j.Slf4j; import org.hjug.cbc.RankedCycle; import org.hjug.cbc.RankedDisharmony; @@ -12,7 +14,353 @@ @Slf4j public class HtmlReport extends SimpleHtmlReport { - public static final String GOD_CLASS_CHART_LEGEND = + int d3Threshold = 700; + + // use Files.readString(Path.of(file)) + // Created by generative AI and modified slightly + public static final String SUGIYAMA_SIGMA_GRAPH = ""; + + public static final String FORCE_3D_GRAPH = + ""; + + // Created by generative AI and modified + public static final String POPUP_STYLE = ""; + + // Created by generative AI and modified + public static final String POPUP_FUNCTIONS = ""; + + private static final String GOD_CLASS_CHART_LEGEND = "

God Class Chart Legend:

" + " \n" + " \n" + " \n" @@ -23,7 +371,7 @@ public class HtmlReport extends SimpleHtmlReport { + "
X-Axis: Effort to refactor to a non-God class
" + "
"; - public static final String COUPLING_BETWEEN_OBJECT_CHART_LEGEND = + private static final String COUPLING_BETWEEN_OBJECT_CHART_LEGEND = "

Coupling Between Objects Chart Legend:

" + " \n" + " \n" + " \n" @@ -35,48 +383,57 @@ public class HtmlReport extends SimpleHtmlReport { + "
"; @Override - public void printHead(StringBuilder stringBuilder) { - stringBuilder.append("" - + "\n" - + "\n" + public String printHead() { + // !Remember to update RefactorFirstMavenReport if this is modified + return // GH Buttons import + "\n" + // google chart import + + "\n" + // d3 dot graph imports + "\n" - + "\n" + + "\n" + "\n" - + " \n"); + // sigma graph imports - sigma, graphology, graphlib, and graphlib-dot + + "\n" + + "\n" + // may only need graphlib-dot + + "\n" + + "\n" + + "\n"; + } + + String printScripts() { + return SUGIYAMA_SIGMA_GRAPH + FORCE_3D_GRAPH + POPUP_FUNCTIONS + POPUP_STYLE; } @Override - public void printTitle(String projectName, String projectVersion, StringBuilder stringBuilder) { - stringBuilder - .append("Refactor First Report for ") - .append(projectName) - .append(" ") - .append(projectVersion) - .append(" \n"); + public String printOpenBodyTag() { + return " \n" + printOverlay(); + } + + private String printOverlay() { + return "
"; } @Override - void renderGithubButtons(StringBuilder stringBuilder) { - stringBuilder.append("
\n"); - stringBuilder.append("Show RefactorFirst some ❤️\n"); - stringBuilder.append("
\n"); - stringBuilder.append( - "Star\n"); - stringBuilder.append( - "Fork\n"); - stringBuilder.append( - "Watch\n"); - stringBuilder.append( - "Issue\n"); - stringBuilder.append( - "Sponsor\n"); - stringBuilder.append("
"); + public String printTitle(String projectName, String projectVersion) { + return "Refactor First Report for " + projectName + " " + projectVersion + " \n"; } @Override - String writeGodClassGchartJs( - List rankedDisharmonies, int maxPriority, String reportOutputDirectory) { + String renderGithubButtons() { + return "
\n" + "Show RefactorFirst some ❤️\n" + + "
\n" + + "Star\n" + + "Fork\n" + + "Watch\n" + + "Issue\n" + + "Sponsor\n" + + "
"; + } + + @Override + String writeGodClassGchartJs(List rankedDisharmonies, int maxPriority) { GraphDataGenerator graphDataGenerator = new GraphDataGenerator(); String scriptStart = graphDataGenerator.getGodClassScriptStart(); String bubbleChartData = graphDataGenerator.generateGodClassBubbleChartData(rankedDisharmonies, maxPriority); @@ -86,7 +443,7 @@ String writeGodClassGchartJs( } @Override - String writeGCBOGchartJs(List rankedDisharmonies, int maxPriority, String reportOutputDirectory) { + String writeGCBOGchartJs(List rankedDisharmonies, int maxPriority) { GraphDataGenerator graphDataGenerator = new GraphDataGenerator(); String scriptStart = graphDataGenerator.getCBOScriptStart(); String bubbleChartData = graphDataGenerator.generateCBOBubbleChartData(rankedDisharmonies, maxPriority); @@ -107,98 +464,230 @@ public String getDescription(Locale locale) { } @Override - void renderGodClassChart( - String outputDirectory, - List rankedGodClassDisharmonies, - int maxGodClassPriority, - StringBuilder stringBuilder) { - String godClassChart = - writeGodClassGchartJs(rankedGodClassDisharmonies, maxGodClassPriority - 1, outputDirectory); + String renderGodClassChart(List rankedGodClassDisharmonies, int maxGodClassPriority) { + StringBuilder stringBuilder = new StringBuilder(); + + String godClassChart = writeGodClassGchartJs(rankedGodClassDisharmonies, maxGodClassPriority - 1); stringBuilder.append( "
\n"); - renderGithubButtons(stringBuilder); + stringBuilder.append(renderGithubButtons()); stringBuilder.append(GOD_CLASS_CHART_LEGEND); + + return stringBuilder.toString(); } @Override - void renderCBOChart( - String outputDirectory, - List rankedCBODisharmonies, - int maxCboPriority, - StringBuilder stringBuilder) { - String cboChart = writeGCBOGchartJs(rankedCBODisharmonies, maxCboPriority - 1, outputDirectory); + String renderCBOChart(List rankedCBODisharmonies, int maxCboPriority) { + StringBuilder stringBuilder = new StringBuilder(); + + String cboChart = writeGCBOGchartJs(rankedCBODisharmonies, maxCboPriority - 1); stringBuilder.append( "
\n"); - renderGithubButtons(stringBuilder); + stringBuilder.append(renderGithubButtons()); stringBuilder.append(COUPLING_BETWEEN_OBJECT_CHART_LEGEND); + return stringBuilder.toString(); } @Override - public void renderCycleImage( - Graph classGraph, RankedCycle cycle, StringBuilder stringBuilder) { - String dot = buildDot(classGraph, cycle); + public String renderClassGraphDotImage() { + String dot = buildClassGraphDot(classGraph); + + String classGraphName = "classGraph"; - stringBuilder.append("
\n"); + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("

Class Map

"); + stringBuilder.append( + "
Excludes classes that have no incoming and outgoing edges
"); stringBuilder.append("\n"); + stringBuilder.append(generateForce3DPopup(classGraphName)); + stringBuilder.append(generate2DPopup(classGraphName)); + stringBuilder.append(generateHidePopup(classGraphName)); + + stringBuilder.append("
\nRed lines represent back edges to remove.
\n"); + stringBuilder.append("Zoom in / out with your mouse wheel and click/move to drag the image.\n"); + stringBuilder.append("
\n"); - stringBuilder.append("
"); - stringBuilder.append("

Red arrows represent relationship(s) to remove to decompose cycle

"); - stringBuilder.append("
"); - stringBuilder.append("
"); - stringBuilder.append("
"); + // revisit and add D3 popup button as well + if (classGraph.vertexSet().size() + classGraph.edgeSet().size() < d3Threshold) { + stringBuilder.append( + "
\n"); + stringBuilder.append("\n"); + } else { + // revisit and add D3 SVG popup button + stringBuilder.append("
\nClass Map SVG is too big to render SVG quickly
\n"); + } + + return stringBuilder.toString(); } - String buildDot(Graph classGraph, RankedCycle cycle) { + String buildClassGraphDot(Graph classGraph) { StringBuilder dot = new StringBuilder(); + dot.append("`strict digraph G {\n"); + + Set vertexesToRender = new HashSet<>(); + for (DefaultWeightedEdge edge : classGraph.edgeSet()) { + // DownloadManager -> Download [ label="1" color="red" ]; + + // render edge + String[] vertexes = extractVertexes(edge); + String start = getClassName(vertexes[0].trim()).replace("$", "_"); + String end = getClassName(vertexes[1].trim()).replace("$", "_"); - dot.append("'strict digraph G {\\n' +\n"); + vertexesToRender.add(vertexes[0].trim()); + vertexesToRender.add(vertexes[1].trim()); + + dot.append(start); + dot.append(" -> "); + dot.append(end); + + // render edge attributes + int edgeWeight = (int) classGraph.getEdgeWeight(edge); + dot.append(" [ "); + dot.append("label = \""); + dot.append(edgeWeight); + dot.append("\" "); + dot.append("weight = \""); + dot.append(edgeWeight); + dot.append("\""); + + if (edgesAboveDiagonal.contains(edge)) { + dot.append(" color = \"red\""); + } + + dot.append(" ];\n"); + } + + // render vertices + // e.g DownloadManager; + // for (String vertex : classGraph.vertexSet()) { + for (String vertex : vertexesToRender) { + dot.append(getClassName(vertex).replace("$", "_")); + dot.append(";\n"); + } + + dot.append("}`;"); + return dot.toString(); + } + + @Override + public String renderCycleDotImage(RankedCycle cycle) { + String dot = buildCycleDot(classGraph, cycle); + + String cycleName = getClassName(cycle.getCycleName()).replace("$", "_"); + + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("\n"); + stringBuilder.append(generateForce3DPopup(cycleName)); + stringBuilder.append(generate2DPopup(cycleName)); + stringBuilder.append(generateHidePopup(cycleName)); + + stringBuilder.append("
\n"); + stringBuilder.append("Red lines represent back edges to remove.
\n"); + stringBuilder.append("Zoom in / out with your mouse wheel and click/move to drag the image.\n"); + stringBuilder.append("
\n"); + + if (cycle.getCycleNodes().size() + cycle.getEdgeSet().size() < d3Threshold) { + stringBuilder.append( + "
\n"); + stringBuilder.append("\n"); + } else { + // revisit and add D3 SVG popup button + stringBuilder.append( + "
\nCycle " + cycleName + " SVG is too big to render SVG quickly
\n"); + } + + stringBuilder.append("
\n"); + stringBuilder.append("
\n"); + + return stringBuilder.toString(); + } + + String buildCycleDot(Graph classGraph, RankedCycle cycle) { + StringBuilder dot = new StringBuilder(); + + dot.append("`strict digraph G {\n"); // render vertices // e.g DownloadManager; for (String vertex : cycle.getVertexSet()) { - dot.append("'"); - dot.append(vertex); - dot.append(";\\n' +\n"); + dot.append(getClassName(vertex).replace("$", "_")); + dot.append(";\n"); } for (DefaultWeightedEdge edge : cycle.getEdgeSet()) { - // 'DownloadManager -> Download [ label="1" color="red" ];' + // DownloadManager -> Download [ label="1" color="red" ]; // render edge - String[] vertexes = - edge.toString().replace("(", "").replace(")", "").split(":"); - - String start = vertexes[0].trim(); - String end = vertexes[1].trim(); + String[] vertexes = extractVertexes(edge); + String start = getClassName(vertexes[0].trim()).replace("$", "_"); + String end = getClassName(vertexes[1].trim()).replace("$", "_"); - dot.append("'"); dot.append(start); dot.append(" -> "); dot.append(end); // render edge attributes + int edgeWeight = (int) classGraph.getEdgeWeight(edge); dot.append(" [ "); dot.append("label = \""); - dot.append((int) classGraph.getEdgeWeight(edge)); + dot.append(edgeWeight); + dot.append("\" "); + dot.append("weight = \""); + dot.append(edgeWeight); dot.append("\""); - if (cycle.getMinCutEdges().contains(edge)) { + if (edgesAboveDiagonal.contains(edge)) { dot.append(" color = \"red\""); } - dot.append(" ];\\n' +\n"); + if (cycle.getMinCutEdges().contains(edge) && !edgesAboveDiagonal.contains(edge)) { + dot.append(" color = \"blue\""); + } + + dot.append(" ];\n"); } - dot.append("'}'"); + dot.append("}`;"); - return dot.toString(); + return dot.toString().replace("$", "_"); + } + + String generate2DPopup(String cycleName) { + // Created by generative AI and modified + return "\n"; + } + + String generateForce3DPopup(String cycleName) { + // Created by generative AI and modified + return "\n"; + } + + String generateHidePopup(String cycleName) { + return "
\n" + + "×\n" + + "
" + + "\n
\n"; } } diff --git a/report/src/main/java/org/hjug/refactorfirst/report/ReportWriter.java b/report/src/main/java/org/hjug/refactorfirst/report/ReportWriter.java index bd186fbb..421c3955 100644 --- a/report/src/main/java/org/hjug/refactorfirst/report/ReportWriter.java +++ b/report/src/main/java/org/hjug/refactorfirst/report/ReportWriter.java @@ -11,7 +11,7 @@ public class ReportWriter { public static void writeReportToDisk( - final String reportOutputDirectory, final String filename, final StringBuilder stringBuilder) { + final String reportOutputDirectory, final String filename, final String string) { final File reportOutputDir = new File(reportOutputDirectory); if (!reportOutputDir.exists()) { @@ -29,7 +29,7 @@ public static void writeReportToDisk( } try (BufferedWriter writer = Files.newBufferedWriter(reportFile.toPath(), Charset.defaultCharset())) { - writer.write(stringBuilder.toString()); + writer.write(string); } catch (IOException e) { log.error("Error writing chart script file", e); } diff --git a/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java b/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java index 7c868c53..0523fe08 100644 --- a/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java +++ b/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java @@ -2,6 +2,8 @@ import static org.hjug.refactorfirst.report.ReportWriter.writeReportToDisk; +import in.wilsonl.minifyhtml.Configuration; +import in.wilsonl.minifyhtml.MinifyHtml; import java.io.File; import java.nio.file.Paths; import java.time.Instant; @@ -13,8 +15,11 @@ import java.util.Optional; import lombok.extern.slf4j.Slf4j; import org.hjug.cbc.CostBenefitCalculator; +import org.hjug.cbc.CycleRanker; import org.hjug.cbc.RankedCycle; import org.hjug.cbc.RankedDisharmony; +import org.hjug.dsm.DSM; +import org.hjug.dsm.EdgeToRemoveInfo; import org.hjug.git.GitLogReader; import org.jgrapht.Graph; import org.jgrapht.graph.DefaultWeightedEdge; @@ -27,7 +32,7 @@ public class SimpleHtmlReport { public static final String THE_BEGINNING = - "\n" + " \n"; + "\n" + "\n"; public static final String THE_END = "\n" + " \n" + " \n" + "\n"; @@ -65,34 +70,111 @@ public class SimpleHtmlReport { public final String[] classCycleTableHeadings = {"Classes", "Relationships"}; - private Graph classGraph; + Graph classGraph; + DSM dsm; + List edgesAboveDiagonal = List.of(); // initialize for unit tests - private boolean showDetails = false; + int pixels; - public void execute( - boolean showDetails, String projectName, String projectVersion, String outputDirectory, File baseDir) { + DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) + .withLocale(Locale.getDefault()) + .withZone(ZoneId.systemDefault()); - this.showDetails = showDetails; + private final Configuration htmlMinifierConfig = new Configuration.Builder() + .setKeepHtmlAndHeadOpeningTags(true) + .setKeepComments(false) + .setMinifyJs(true) + .setMinifyCss(true) + .build(); - final String[] godClassTableHeadings = - showDetails ? godClassDetailedTableHeadings : godClassSimpleTableHeadings; + public void execute( + int edgeAnalysisCount, + boolean analyzeCycles, + boolean showDetails, + boolean minifyHtml, + boolean excludeTests, + String testSourceDirectory, + String projectName, + String projectVersion, + File baseDir, + String outputDirectory) { String filename = getOutputName() + ".html"; - log.info("Generating {} for {} - {}", filename, projectName, projectVersion); - DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) - .withLocale(Locale.getDefault()) - .withZone(ZoneId.systemDefault()); - StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append(THE_BEGINNING); - printTitle(projectName, projectVersion, stringBuilder); - printHead(stringBuilder); - printBreadcrumbs(stringBuilder); - printProjectHeader(projectName, projectVersion, stringBuilder); + stringBuilder.append(""); + stringBuilder.append(printTitle(projectName, projectVersion)); + stringBuilder.append(printHead()); + stringBuilder.append(""); + stringBuilder.append(generateReport( + showDetails, + edgeAnalysisCount, + analyzeCycles, + excludeTests, + testSourceDirectory, + projectName, + projectVersion, + baseDir)); + + stringBuilder.append(printProjectFooter()); + stringBuilder.append(THE_END); + + String reportHtml; + if (minifyHtml) { + reportHtml = MinifyHtml.minify(stringBuilder.toString(), htmlMinifierConfig); + } else { + reportHtml = stringBuilder.toString(); + } + writeReportToDisk(outputDirectory, filename, reportHtml); + log.info("Done! View the report at target/site/{}", filename); + } + + public StringBuilder generateReport( + boolean showDetails, + int edgeAnalysisCount, + boolean analyzeCycles, + boolean excludeTests, + String testSourceDirectory, + String projectName, + String projectVersion, + File baseDir) { + return generateReport( + showDetails, + edgeAnalysisCount, + analyzeCycles, + excludeTests, + testSourceDirectory, + projectName, + projectVersion, + baseDir, + 200); + } + + // pixels param is for SVG image pixel padding + public StringBuilder generateReport( + boolean showDetails, + int edgeAnalysisCount, + boolean analyzeCycles, + boolean excludeTests, + String testSourceDirectory, + String projectName, + String projectVersion, + File baseDir, + int pixels) { + + if (testSourceDirectory == null || testSourceDirectory.isEmpty()) { + testSourceDirectory = "src" + File.separator + "test"; + } + + this.pixels = pixels; + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(printOpenBodyTag()); + stringBuilder.append(printScripts()); + stringBuilder.append(printBreadcrumbs()); + stringBuilder.append(printProjectHeader(projectName, projectVersion)); GitLogReader gitLogReader = new GitLogReader(); String projectBaseDir; @@ -119,9 +201,8 @@ public void execute( .append(projectVersion) .append(". "); stringBuilder.append("Please initialize a Git repository and perform an initial commit."); - stringBuilder.append(THE_END); - writeReportToDisk(outputDirectory, filename, stringBuilder); - return; + + return stringBuilder; } String parentOfGitDir = gitDir.getParentFile().getPath(); @@ -132,44 +213,58 @@ public void execute( log.warn("Project Base Directory does not match Git Parent Directory"); stringBuilder.append("Project Base Directory does not match Git Parent Directory. " + "Please refer to the report at the root of the site directory."); - stringBuilder.append(THE_END); - return; + return stringBuilder; } - List rankedGodClassDisharmonies; - List rankedCBODisharmonies; - List rankedCycles; + List rankedGodClassDisharmonies = List.of(); + List rankedCBODisharmonies = List.of(); + log.info("Identifying Object Oriented Disharmonies"); try (CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(projectBaseDir)) { costBenefitCalculator.runPmdAnalysis(); rankedGodClassDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues(); rankedCBODisharmonies = costBenefitCalculator.calculateCBOCostBenefitValues(); - if (showDetails) { - rankedCycles = costBenefitCalculator.runCycleAnalysisAndCalculateCycleChurn(); - } else { - rankedCycles = costBenefitCalculator.runCycleAnalysis(); - } - - classGraph = costBenefitCalculator.getClassReferencesGraph(); } catch (Exception e) { log.error("Error running analysis."); throw new RuntimeException(e); } - if (rankedGodClassDisharmonies.isEmpty() && rankedCBODisharmonies.isEmpty() && rankedCycles.isEmpty()) { + CycleRanker cycleRanker = new CycleRanker(projectBaseDir); + List rankedCycles = List.of(); + if (analyzeCycles) { + log.info("Analyzing Cycles"); + rankedCycles = cycleRanker.performCycleAnalysis(excludeTests, testSourceDirectory); + } else { + cycleRanker.generateClassReferencesGraph(excludeTests, testSourceDirectory); + } + + classGraph = cycleRanker.getClassReferencesGraph(); + dsm = new DSM(classGraph); + edgesAboveDiagonal = dsm.getEdgesAboveDiagonal(); + + log.info("Performing edge removal what-if analysis"); + List edgeToRemoveInfos = dsm.getImpactOfEdgesAboveDiagonalIfRemoved(edgeAnalysisCount); + + if (edgeToRemoveInfos.isEmpty() + && rankedGodClassDisharmonies.isEmpty() + && rankedCBODisharmonies.isEmpty() + && rankedCycles.isEmpty()) { stringBuilder .append("Congratulations! ") .append(projectName) .append(" ") .append(projectVersion) - .append(" has no God classes, highly coupled classes, or cycles!"); - renderGithubButtons(stringBuilder); + .append(" has no Back Edges, God classes, Highly Coupled Classes, or Cycles!"); + stringBuilder.append(renderGithubButtons()); log.info("Done! No Disharmonies found!"); - stringBuilder.append(THE_END); - writeReportToDisk(outputDirectory, filename, stringBuilder); - return; + return stringBuilder; } - if (!rankedGodClassDisharmonies.isEmpty() && !rankedCBODisharmonies.isEmpty()) { + if (!edgeToRemoveInfos.isEmpty()) { + stringBuilder.append("Back Edges\n"); + stringBuilder.append("
\n"); + } + + if (!rankedGodClassDisharmonies.isEmpty()) { stringBuilder.append("God Classes\n"); stringBuilder.append("
\n"); } @@ -183,110 +278,146 @@ public void execute( stringBuilder.append("Class Cycles\n"); } - if (!rankedGodClassDisharmonies.isEmpty()) { - renderGodClassInfo( - showDetails, - outputDirectory, - rankedGodClassDisharmonies, - stringBuilder, - godClassTableHeadings, - formatter); + log.info("Generating HTML Report"); + + stringBuilder.append(renderClassGraphDotImage()); + stringBuilder.append("
\n"); + stringBuilder.append(renderGithubButtons()); + + // Display impact of each edge if removed + stringBuilder.append("
\n"); + String edgeInfos = renderEdgeToRemoveInfos(edgeToRemoveInfos); + + if (!edgeToRemoveInfos.isEmpty()) { + stringBuilder.append(edgeInfos); + stringBuilder.append(renderGithubButtons()); + stringBuilder.append("
\n" + "
\n" + "
\n" + "
\n" + "
\n" + "
\n" + "
\n"); } - if (!rankedGodClassDisharmonies.isEmpty() && !rankedCBODisharmonies.isEmpty()) { + if (!rankedGodClassDisharmonies.isEmpty()) { + final String[] godClassTableHeadings = + showDetails ? godClassDetailedTableHeadings : godClassSimpleTableHeadings; + stringBuilder.append(renderGodClassInfo(showDetails, rankedGodClassDisharmonies, godClassTableHeadings)); stringBuilder.append("
\n" + "
\n" + "
\n" + "
\n" + "
\n" + "
\n" + "
\n"); } if (!rankedCBODisharmonies.isEmpty()) { - renderHighlyCoupledClassInfo(outputDirectory, stringBuilder, rankedCBODisharmonies, formatter); + stringBuilder.append(renderHighlyCoupledClassInfo(rankedCBODisharmonies)); + stringBuilder.append("
\n" + "
\n" + "
\n" + "
\n" + "
\n" + "
\n" + "
\n"); } if (!rankedCycles.isEmpty()) { - if (!rankedGodClassDisharmonies.isEmpty() || !rankedCBODisharmonies.isEmpty()) { - stringBuilder.append("
\n"); - stringBuilder.append("
\n"); - stringBuilder.append("
\n"); - stringBuilder.append("
\n"); - stringBuilder.append("
\n"); - } - renderCycles(outputDirectory, stringBuilder, rankedCycles, formatter); + stringBuilder.append(renderCycles(rankedCycles)); } stringBuilder.append("\n"); - printProjectFooter(stringBuilder, formatter); - stringBuilder.append(THE_END); log.debug(stringBuilder.toString()); - - writeReportToDisk(outputDirectory, filename, stringBuilder); - log.info("Done! View the report at target/site/{}", filename); + return stringBuilder; } - private void renderCycles( - String outputDirectory, - StringBuilder stringBuilder, - List rankedCycles, - DateTimeFormatter formatter) { + private String renderCycles(List rankedCycles) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(renderClassCycleSummary(rankedCycles)); - stringBuilder.append("\n"); + stringBuilder.append("
\n"); + + rankedCycles.stream().limit(10).map(this::renderSingleCycle).forEach(stringBuilder::append); + + return stringBuilder.toString(); + } + + private String renderEdgeToRemoveInfos(List edges) { + StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append( - "

Class Cycles by the numbers: (Refactor starting with Priority 1)

\n"); - stringBuilder.append( - "

Note: often only one minimum cut relationship needs to be removed

"); + "\n"); + stringBuilder.append("
\n"); + + stringBuilder + .append("Current Cycle Count: ") + .append(dsm.getCycles().size()) + .append("
\n"); + stringBuilder + .append("Current Average Cycle Node Count: ") + .append(dsm.getAverageCycleNodeCount()) + .append("
\n"); + stringBuilder + .append("Current Total Back Edge Count: ") + .append(dsm.getEdgesAboveDiagonal().size()) + .append("
\n"); + stringBuilder + .append("Current Total Min Weight Back Edge Count: ") + .append(dsm.getMinimumWeightEdgesAboveDiagonal().size()) + .append("
\n"); + stringBuilder.append("
\n"); + stringBuilder.append("
X-Axis: Number of objects the class is coupled to
\n"); - String[] cycleTableHeadings; - if (showDetails) { - cycleTableHeadings = new String[] { - "Cycle Name", "Priority", "Change Proneness Rank", "Class Count", "Relationship Count", "Minimum Cuts" - }; - } else { - cycleTableHeadings = - new String[] {"Cycle Name", "Priority", "Class Count", "Relationship Count", "Minimum Cuts"}; + // Content + stringBuilder.append("\n\n"); + for (String heading : getEdgesToRemoveInfoTableHeadings()) { + stringBuilder.append("\n"); + } + stringBuilder.append("\n"); + + stringBuilder.append("\n"); + for (EdgeToRemoveInfo edge : edges) { + stringBuilder.append("\n"); + + for (String rowData : getEdgeToRemoveInfos(edge)) { + stringBuilder.append(drawTableCell(rowData)); + } + + stringBuilder.append("\n"); + } + + stringBuilder.append("\n"); + stringBuilder.append("
").append(heading).append("
\n"); + + return stringBuilder.toString(); + } + + private String renderClassCycleSummary(List rankedCycles) { + StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append("\n"); + if (rankedCycles.size() > 10) { + stringBuilder.append( + "
10 largest cycles are shown in the sections below
\n"); } + stringBuilder.append("

Class Cycles by the numbers:

\n"); + // stringBuilder.append("

Bold edges are backward edges causing + // cycles

"); + stringBuilder.append("\n"); + // Content stringBuilder.append("\n\n"); - for (String heading : cycleTableHeadings) { + for (String heading : getCycleSummaryTableHeadings()) { stringBuilder.append("\n"); } stringBuilder.append("\n"); stringBuilder.append("\n"); - for (RankedCycle rankedCycle : rankedCycles) { + for (RankedCycle cycle : rankedCycles) { stringBuilder.append("\n"); - StringBuilder edgesToCut = new StringBuilder(); - for (DefaultWeightedEdge minCutEdge : rankedCycle.getMinCutEdges()) { - edgesToCut.append(minCutEdge + ":" + (int) classGraph.getEdgeWeight(minCutEdge)); - edgesToCut.append("
\n"); + StringBuilder edges = new StringBuilder(); + for (DefaultWeightedEdge edge : cycle.getMinCutEdges()) { + + if (edgesAboveDiagonal.contains(edge)) { + stringBuilder.append(""); + edges.append(renderEdge(edge)); + stringBuilder.append(""); + } else { + edges.append(renderEdge(edge)); + } + edges.append("
\n"); } - String[] rankedCycleData; - if (showDetails) { - rankedCycleData = new String[] { - // "Cycle Name", "Priority", "Change Proneness Rank", "Class Count", "Relationship Count", "Min - // Cuts" - rankedCycle.getCycleName(), - rankedCycle.getPriority().toString(), - rankedCycle.getChangePronenessRank().toString(), - String.valueOf(rankedCycle.getCycleNodes().size()), - String.valueOf(rankedCycle.getEdgeSet().size()), - edgesToCut.toString() - }; - } else { - rankedCycleData = new String[] { - // "Cycle Name", "Priority", "Class Count", "Relationship Count", "Min Cuts" - rankedCycle.getCycleName(), - rankedCycle.getPriority().toString(), - String.valueOf(rankedCycle.getCycleNodes().size()), - String.valueOf(rankedCycle.getEdgeSet().size()), - edgesToCut.toString() - }; - } - for (String rowData : rankedCycleData) { - drawTableCell(rowData, stringBuilder); + for (String rowData : getRankedCycleSummaryData(cycle, edges)) { + stringBuilder.append(drawTableCell(rowData)); } stringBuilder.append("\n"); @@ -295,13 +426,62 @@ private void renderCycles( stringBuilder.append("\n"); stringBuilder.append("
").append(heading).append("
\n"); - for (RankedCycle rankedCycle : rankedCycles) { - renderSingleCycle(outputDirectory, stringBuilder, rankedCycle, formatter); - } + return stringBuilder.toString(); } - private void renderSingleCycle( - String outputDirectory, StringBuilder stringBuilder, RankedCycle cycle, DateTimeFormatter formatter) { + private String renderEdge(DefaultWeightedEdge edge) { + StringBuilder edgesToCut = new StringBuilder(); + String[] vertexes = extractVertexes(edge); + String start = getClassName(vertexes[0].trim()); + String end = getClassName(vertexes[1].trim()); + + // → is HTML "Right Arrow" code + return edgesToCut + .append(start + " → " + end + " : " + (int) classGraph.getEdgeWeight(edge)) + .toString(); + } + + private String[] getCycleSummaryTableHeadings() { + return new String[] {"Cycle Name", "Priority", "Class Count", "Relationship Count" /*, "Minimum Cuts"*/}; + } + + private String[] getEdgesToRemoveInfoTableHeadings() { + return new String[] { + "Edge", + "Edge Weight", + "In # of Cycles", + "New Cycle Count", + "New Avg Cycle Node Count", + "Avg Node Δ ÷ Effort" + }; + } + + private String[] getEdgeToRemoveInfos(EdgeToRemoveInfo edgeToRemoveInfo) { + return new String[] { + // "Edge", "Edge Weight", "In # of Cycles", "New Cycle Count", "New Avg Cycle Node Count", "Avg Node Count / + // Effort" + renderEdge(edgeToRemoveInfo.getEdge()), + String.valueOf((int) edgeToRemoveInfo.getEdgeWeight()), + String.valueOf(edgeToRemoveInfo.getEdgeInCycleCount()), + String.valueOf(edgeToRemoveInfo.getNewCycleCount()), + String.valueOf(edgeToRemoveInfo.getAverageCycleNodeCount()), + String.valueOf(edgeToRemoveInfo.getPayoff()) + }; + } + + private String[] getRankedCycleSummaryData(RankedCycle rankedCycle, StringBuilder edgesToCut) { + return new String[] { + // "Cycle Name", "Priority", "Class Count", "Relationship Count", "Min Cuts" + getClassName(rankedCycle.getCycleName()), + rankedCycle.getPriority().toString(), + String.valueOf(rankedCycle.getCycleNodes().size()), + String.valueOf(rankedCycle.getEdgeSet().size()) // , + // edgesToCut.toString() + }; + } + + private String renderSingleCycle(RankedCycle cycle) { + StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("
\n"); stringBuilder.append("
\n"); @@ -309,13 +489,13 @@ private void renderSingleCycle( stringBuilder.append("
\n"); stringBuilder.append("
\n"); - stringBuilder.append("

Class Cycle : " + cycle.getCycleName() + "

\n"); - // renderCycleImage(cycle.getCycleName(), stringBuilder, outputDirectory); - renderCycleImage(classGraph, cycle, stringBuilder); + stringBuilder.append("

Class Cycle : " + getClassName(cycle.getCycleName()) + "

\n"); + stringBuilder.append(renderCycleDotImage(cycle)); stringBuilder.append("
"); stringBuilder.append(""); - stringBuilder.append("\"*\" indicates relationship(s) to remove to decompose cycle"); + stringBuilder.append("Bold text indicates back edge to remove to decompose cycle"); + // stringBuilder.append("
\"*\" indicates that edge is also a minimum cut edge in the cycle"); stringBuilder.append("
"); stringBuilder.append("
\n"); @@ -332,53 +512,54 @@ private void renderSingleCycle( for (String vertex : cycle.getVertexSet()) { stringBuilder.append(""); - drawTableCell(vertex, stringBuilder); + stringBuilder.append(drawTableCell(getClassName(vertex))); StringBuilder edges = new StringBuilder(); - for (org.jgrapht.graph.DefaultWeightedEdge edge : cycle.getEdgeSet()) { + for (DefaultWeightedEdge edge : cycle.getEdgeSet()) { if (edge.toString().startsWith("(" + vertex + " :")) { - if (cycle.getMinCutEdges().contains(edge)) { + + if (edgesAboveDiagonal.contains(edge)) { edges.append(""); - edges.append(edge); - edges.append(":") - .append((int) classGraph.getEdgeWeight(edge)) - .append("*"); + edges.append(renderEdge(edge)); + if (cycle.getMinCutEdges().contains(edge)) { + edges.append("*"); + } edges.append(""); } else { - edges.append(edge); - edges.append(":").append((int) classGraph.getEdgeWeight(edge)); + edges.append(renderEdge(edge)); } edges.append("
\n"); } } - drawTableCell(edges.toString(), stringBuilder); + stringBuilder.append(drawTableCell(edges.toString())); stringBuilder.append("\n"); } stringBuilder.append("\n"); stringBuilder.append("\n"); + + return stringBuilder.toString(); } - public void renderCycleImage( - Graph classGraph, RankedCycle cycle, StringBuilder stringBuilder) { - // empty on purpose + public String renderClassGraphDotImage() { + return ""; // empty on purpose } - private void renderGodClassInfo( - boolean showDetails, - String outputDirectory, - List rankedGodClassDisharmonies, - StringBuilder stringBuilder, - String[] godClassTableHeadings, - DateTimeFormatter formatter) { + public String renderCycleDotImage(RankedCycle cycle) { + return ""; // empty on purpose + } + + private String renderGodClassInfo( + boolean showDetails, List rankedGodClassDisharmonies, String[] godClassTableHeadings) { int maxGodClassPriority = rankedGodClassDisharmonies .get(rankedGodClassDisharmonies.size() - 1) .getPriority(); + StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("\n"); - renderGodClassChart(outputDirectory, rankedGodClassDisharmonies, maxGodClassPriority, stringBuilder); + stringBuilder.append(renderGodClassChart(rankedGodClassDisharmonies, maxGodClassPriority)); stringBuilder.append( "

God classes by the numbers: (Refactor Starting with Priority 1)

\n"); @@ -427,7 +608,7 @@ private void renderGodClassInfo( showDetails ? detailedRankedGodClassDisharmonyData : simpleRankedGodClassDisharmonyData; for (String rowData : rankedDisharmonyData) { - drawTableCell(rowData, stringBuilder); + stringBuilder.append(drawTableCell(rowData)); } stringBuilder.append("\n"); @@ -435,20 +616,19 @@ private void renderGodClassInfo( stringBuilder.append("\n"); stringBuilder.append("\n"); + + return stringBuilder.toString(); } - private void renderHighlyCoupledClassInfo( - String outputDirectory, - StringBuilder stringBuilder, - List rankedCBODisharmonies, - DateTimeFormatter formatter) { + private String renderHighlyCoupledClassInfo(List rankedCBODisharmonies) { + StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append( ""); int maxCboPriority = rankedCBODisharmonies.get(rankedCBODisharmonies.size() - 1).getPriority(); - renderCBOChart(outputDirectory, rankedCBODisharmonies, maxCboPriority, stringBuilder); + stringBuilder.append(renderCBOChart(rankedCBODisharmonies, maxCboPriority)); stringBuilder.append( "

Highly Coupled classes by the numbers: (Refactor starting with Priority 1)

"); @@ -475,7 +655,7 @@ private void renderHighlyCoupledClassInfo( }; for (String rowData : rankedCboClassDisharmonyData) { - drawTableCell(rowData, stringBuilder); + stringBuilder.append(drawTableCell(rowData)); } stringBuilder.append(""); @@ -483,13 +663,23 @@ private void renderHighlyCoupledClassInfo( stringBuilder.append(""); stringBuilder.append(""); + + return stringBuilder.toString(); } - void drawTableCell(String rowData, StringBuilder stringBuilder) { + String drawTableCell(String rowData) { if (isNumber(rowData) || isDateTime(rowData)) { - stringBuilder.append("").append(rowData).append("\n"); + return new StringBuilder() + .append("") + .append(rowData) + .append("\n") + .toString(); } else { - stringBuilder.append("").append(rowData).append("\n"); + return new StringBuilder() + .append("") + .append(rowData) + .append("\n") + .toString(); } } @@ -501,50 +691,52 @@ boolean isDateTime(String rowData) { return rowData.contains(", "); } - public void printTitle(String projectName, String projectVersion, StringBuilder stringBuilder) { - // empty on purpose + public String printTitle(String projectName, String projectVersion) { + return ""; // empty on purpose + } + + public String printHead() { + return ""; // empty on purpose + } + + String printScripts() { + return ""; // empty on purpose } - public void printHead(StringBuilder stringBuilder) { - // empty on purpose + public String printOpenBodyTag() { + return " \n"; } - public void printBreadcrumbs(StringBuilder stringBuilder) { - stringBuilder.append(" \n" - + "
\n" + public String printBreadcrumbs() { + return "
\n" + "
\n" + "
\n" - + "
\n"); + + "
\n"; } - public void printProjectHeader(String projectName, String projectVersion, StringBuilder stringBuilder) { - - stringBuilder.append("
\n" + "
\n" + public String printProjectHeader(String projectName, String projectVersion) { + return "
\n" + "
\n" + "
\n" + "
\n" - + "
\n"); - - stringBuilder - .append( - "
\n" - + "

RefactorFirst Report for ") - .append(projectName) - .append(" ") - .append(projectVersion) - .append("

\n"); + + "
\n" + "
\n" + + "

RefactorFirst Report for " + + projectName + + " " + + projectVersion + + "

\n"; } - public void printProjectFooter(StringBuilder stringBuilder, DateTimeFormatter formatter) { - stringBuilder - .append("
\n" + "
\n" + "
\n") - .append("Last Published: ") - .append(formatter.format(Instant.now())) - .append("
\n" + "
\n" + "
\n") - .append("
"); + public String printProjectFooter() { + return "
\n" + "
\n" + "
\n" + + "Last Published: " + + formatter.format(Instant.now()) + + "
\n" + + "
\n" + "
\n" + "
"; } - void renderGithubButtons(StringBuilder stringBuilder) { - // empty on purpose + String renderGithubButtons() { + return ""; // empty on purpose } String getOutputName() { @@ -552,30 +744,35 @@ String getOutputName() { return "refactor-first-report"; } - void renderGodClassChart( - String outputDirectory, - List rankedGodClassDisharmonies, - int maxGodClassPriority, - StringBuilder stringBuilder) { - // empty on purpose + String renderGodClassChart(List rankedGodClassDisharmonies, int maxGodClassPriority) { + return ""; // empty on purpose } - String writeGodClassGchartJs( - List rankedDisharmonies, int maxPriority, String reportOutputDirectory) { + String writeGodClassGchartJs(List rankedDisharmonies, int maxPriority) { // return empty string on purpose return ""; } - String writeGCBOGchartJs(List rankedDisharmonies, int maxPriority, String reportOutputDirectory) { + String writeGCBOGchartJs(List rankedDisharmonies, int maxPriority) { // return empty string on purpose return ""; } - void renderCBOChart( - String outputDirectory, - List rankedCBODisharmonies, - int maxCboPriority, - StringBuilder stringBuilder) { - // empty on purpose + String renderCBOChart(List rankedCBODisharmonies, int maxCboPriority) { + return ""; // empty on purpose + } + + String getClassName(String fqn) { + // handle no package + if (!fqn.contains(".")) { + return fqn; + } + + int lastIndex = fqn.lastIndexOf("."); + return fqn.substring(lastIndex + 1); + } + + static String[] extractVertexes(DefaultWeightedEdge edge) { + return edge.toString().replace("(", "").replace(")", "").split(":"); } } diff --git a/report/src/main/java/org/hjug/refactorfirst/report/json/JsonReportExecutor.java b/report/src/main/java/org/hjug/refactorfirst/report/json/JsonReportExecutor.java index cf552fc8..92b7efdb 100644 --- a/report/src/main/java/org/hjug/refactorfirst/report/json/JsonReportExecutor.java +++ b/report/src/main/java/org/hjug/refactorfirst/report/json/JsonReportExecutor.java @@ -49,7 +49,7 @@ public void execute(File baseDir, String outputDirectory) { try { final String reportJson = MAPPER.writeValueAsString(report); - writeReportToDisk(outputDirectory, FILE_NAME, new StringBuilder(reportJson)); + writeReportToDisk(outputDirectory, FILE_NAME, new StringBuilder(reportJson).toString()); } catch (final JsonProcessingException jsonProcessingException) { final String errorMessage = "Could not generate a json report: " + jsonProcessingException; @@ -64,7 +64,8 @@ public void execute(File baseDir, String outputDirectory) { private void writeErrorReport(final JsonReport errorReport, String outputDirectory) { try { - writeReportToDisk(outputDirectory, FILE_NAME, new StringBuilder(MAPPER.writeValueAsString(errorReport))); + writeReportToDisk( + outputDirectory, FILE_NAME, new StringBuilder(MAPPER.writeValueAsString(errorReport)).toString()); } catch (final JsonProcessingException jsonProcessingException) { log.error("failed to write error report: ", jsonProcessingException); } diff --git a/report/src/test/java/org/hjug/refactorfirst/report/HtmlReportTest.java b/report/src/test/java/org/hjug/refactorfirst/report/HtmlReportTest.java index 363576db..734c6507 100644 --- a/report/src/test/java/org/hjug/refactorfirst/report/HtmlReportTest.java +++ b/report/src/test/java/org/hjug/refactorfirst/report/HtmlReportTest.java @@ -12,7 +12,7 @@ import org.jgrapht.graph.DefaultWeightedEdge; import org.junit.jupiter.api.Test; -public class HtmlReportTest { +class HtmlReportTest { private HtmlReport mavenReport = new HtmlReport(); @@ -38,7 +38,7 @@ void getDescription() { } @Test - void buildDot() { + void buildCycleDot() { Graph classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class); classGraph.addVertex("A"); classGraph.addVertex("B"); @@ -59,18 +59,17 @@ void buildDot() { cycleName, 0, classGraph.vertexSet(), classGraph.edgeSet(), minCutCount, minCutEdges, cycleNodes); HtmlReport htmlReport = new HtmlReport(); - String dot = htmlReport.buildDot(classGraph, rankedCycle); + String dot = htmlReport.buildCycleDot(classGraph, rankedCycle); StringBuilder expectedDot = new StringBuilder(); - expectedDot.append("'strict digraph G {\\n' +\n"); - expectedDot.append("'A;\\n' +\n"); - expectedDot.append("'B;\\n' +\n"); - expectedDot.append("'C;\\n' +\n"); - // 'DownloadManager -> Download [ label = "1" color = "red" ];' - expectedDot.append("'A -> B [ label = \"2\" ];\\n' +\n"); - expectedDot.append("'B -> C [ label = \"1\" color = \"red\" ];\\n' +\n"); - expectedDot.append("'C -> A [ label = \"1\" color = \"red\" ];\\n' +\n"); - expectedDot.append("'}'"); + expectedDot.append("`strict digraph G {\n" + + "A;\n" + + "B;\n" + + "C;\n" + + "A -> B [ label = \"2\" weight = \"2\" ];\n" + + "B -> C [ label = \"1\" weight = \"1\" color = \"blue\" ];\n" + + "C -> A [ label = \"1\" weight = \"1\" color = \"blue\" ];\n" + + "}`;"); assertEquals(expectedDot.toString(), dot); } diff --git a/report/src/test/resources/highlight.html b/report/src/test/resources/highlight.html new file mode 100644 index 00000000..2dba127a --- /dev/null +++ b/report/src/test/resources/highlight.html @@ -0,0 +1,114 @@ + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/report/src/test/resources/sigmaPlayground.html b/report/src/test/resources/sigmaPlayground.html new file mode 100644 index 00000000..08eb2380 --- /dev/null +++ b/report/src/test/resources/sigmaPlayground.html @@ -0,0 +1,714 @@ + + + + + + + DOT Graph with Sigma.js and Graphology + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + \ No newline at end of file diff --git a/report/src/test/resources/spriteText.html b/report/src/test/resources/spriteText.html new file mode 100644 index 00000000..dff5dde8 --- /dev/null +++ b/report/src/test/resources/spriteText.html @@ -0,0 +1,62 @@ + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/test-resources/pom.xml b/test-resources/pom.xml index c4b6a17e..43c87165 100644 --- a/test-resources/pom.xml +++ b/test-resources/pom.xml @@ -5,7 +5,7 @@ org.hjug.refactorfirst refactor-first - 0.6.3-SNAPSHOT + 0.7.0-SNAPSHOT org.hjug.refactorfirst.testresources