diff --git a/graph-builder/build.gradle b/graph-builder/build.gradle index ad77132..9725f85 100644 --- a/graph-builder/build.gradle +++ b/graph-builder/build.gradle @@ -82,6 +82,12 @@ processResources { from sourceSets.main.java.filter {it.toString().endsWith("MatcherContainer.java")}.first().toString() } +task copySourceResForTests(type: Copy){ + from sourceSets.test.java.filter {it.toString().contains("subclassestests")} + into file(sourceSets.test.output.resourcesDir.path + "/subclassestests") +} +processTestResources.dependsOn copySourceResForTests + sourceSets { test.java.srcDirs += 'src/test/kotlin' } diff --git a/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/DrawerUtil.java b/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/DrawerUtil.java index 849eb9b..e765f04 100644 --- a/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/DrawerUtil.java +++ b/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/DrawerUtil.java @@ -37,7 +37,7 @@ public static void drawAllStartableClasses(AnalyzerWithModel analyzerWithModel, AsciiDocIndexBuilder asciiDocIndexBuilder = new AsciiDocIndexBuilder(analyzerWithModel.getAnalysisName()); - final List startableByRPCClasses = analyzerWithModel.getClassesByAnnotation(StartableByRPC.class); + final List startableByRPCClasses = analyzerWithModel.getClassesToBeAnalyzed(); LOGGER.info("Found these classes annotated with @StartableByRPC: "); Paths.get(outPath, "images").toFile().mkdirs(); //create all directories necessary for the output for (CtClass klass : startableByRPCClasses) { diff --git a/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/Main.java b/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/Main.java index 6cb38f1..4771299 100644 --- a/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/Main.java +++ b/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/Main.java @@ -1,6 +1,8 @@ package com.github.lucacampanella.callgraphflows; +import com.github.lucacampanella.callgraphflows.staticanalyzer.DecompilerEnum; import com.github.lucacampanella.callgraphflows.staticanalyzer.JarAnalyzer; +import com.github.lucacampanella.callgraphflows.staticanalyzer.SourceAndJarAnalyzer; import org.slf4j.LoggerFactory; import picocli.CommandLine; @@ -10,11 +12,10 @@ public class Main implements Callable { - @CommandLine.Parameters(arity = "1", index="0", paramLabel = "JarFile", description = "Jar file to process.") - private String inputJarPath; - - @CommandLine.Parameters(arity = "0..*", index="1..*", paramLabel = "AdditionalJarFiles", description = "Additional jars to be added to classpath") - private String[] additionalJarsPath; + @CommandLine.Parameters(arity = "1..*", index="0..*", paramLabel = "filesToAnalyze", + description = "The paths to the files that need to be analyzed, they can be " + + ".java files, folders or .jar files") + private String[] filesPaths; @CommandLine.Option(names = {"-o", "--output"}, defaultValue = "graphs", description = "Output folder path") private String outputPath; @@ -25,6 +26,10 @@ public class Main implements Callable { @CommandLine.Option(names = {"-l", "--draw-line-numbers"}, description = "draw the line numbers") boolean drawLineNumbers = false; + @CommandLine.Option(names = {"-s", "--only-source-files"}, description = "analyze only the source files and not " + + "the decompiled code") + boolean analyzeOnlySources = false; + public static void main(String []args) throws IOException { final Main app = CommandLine.populateCommand(new Main(), args); @@ -40,14 +45,8 @@ public Integer call() throws IOException { System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "error"); } LoggerFactory.getLogger(Main.class).trace("Logger level = {}", loggerLevel); - JarAnalyzer analyzer; - - if(additionalJarsPath == null) { - analyzer = new JarAnalyzer(decompilerName, inputJarPath); - } - else { - analyzer = new JarAnalyzer(decompilerName, inputJarPath, additionalJarsPath); - } + SourceAndJarAnalyzer analyzer = new SourceAndJarAnalyzer(filesPaths, + DecompilerEnum.fromStringOrDefault(decompilerName), analyzeOnlySources); LoggerFactory.getLogger(Main.class).trace("drawLineNumbers = {}", drawLineNumbers); DrawerUtil.setDrawLineNumbers(drawLineNumbers); diff --git a/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/AnalyzerWithModel.java b/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/AnalyzerWithModel.java index 440177e..8bdc0c1 100644 --- a/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/AnalyzerWithModel.java +++ b/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/AnalyzerWithModel.java @@ -3,6 +3,7 @@ import com.github.lucacampanella.callgraphflows.AnalysisErrorException; import com.github.lucacampanella.callgraphflows.staticanalyzer.matchers.MatcherHelper; import net.corda.core.flows.InitiatedBy; +import net.corda.core.flows.StartableByRPC; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import spoon.reflect.CtModel; @@ -126,7 +127,12 @@ public List getClassesByAnnotation(Class annotationClass) { .collect(Collectors.toList()); } - public CtClass getDeeperClassInitiatedBy(CtClass initiatingClass) { + public List getClassesToBeAnalyzed() { + return getClassesByAnnotation(StartableByRPC.class); + } + + + public CtClass getDeeperClassInitiatedBy(CtClass initiatingClass) { CtClass deeperInitiatedByClass = null; final List generalInitiatedByList = getClassesByAnnotation(InitiatedBy.class); for(CtClass klass : generalInitiatedByList) { diff --git a/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/CustomJarLauncher.java b/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/CustomJarLauncher.java index e743fdc..f4f176f 100644 --- a/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/CustomJarLauncher.java +++ b/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/CustomJarLauncher.java @@ -24,17 +24,6 @@ public class CustomJarLauncher extends Launcher { private static final Logger LOGGER = LoggerFactory.getLogger(CustomJarLauncher.class); - public enum DecompilerEnum { - CFR, FERNFLOWER; - - public Decompiler getDecompiler(File decompiledSrc) { - if(this == FERNFLOWER) { - return new FernflowerDecompiler(decompiledSrc); - } - return new CFRDecompiler(decompiledSrc); - } - } - public static class Builder { String decompiledSrcPath = Paths.get(System.getProperty("java.io.tmpdir"), "spoon-tmp", "decompiledSrc").toString(); DecompilerEnum decompilerEnum = DecompilerEnum.CFR; @@ -118,7 +107,6 @@ private CustomJarLauncher(){ //use builder } - // public CustomJarLauncher(List jarPaths) { // this(jarPaths, (String)null, null); // } diff --git a/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/DecompilerEnum.java b/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/DecompilerEnum.java new file mode 100644 index 0000000..45a6c15 --- /dev/null +++ b/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/DecompilerEnum.java @@ -0,0 +1,38 @@ +package com.github.lucacampanella.callgraphflows.staticanalyzer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spoon.decompiler.CFRDecompiler; +import spoon.decompiler.Decompiler; +import spoon.decompiler.FernflowerDecompiler; + +import java.io.File; + +public enum DecompilerEnum { + CFR, FERNFLOWER; + + private static final Logger LOGGER = LoggerFactory.getLogger(DecompilerEnum.class); + + public Decompiler getDecompiler(File decompiledSrc) { + if(this == FERNFLOWER) { + return new FernflowerDecompiler(decompiledSrc); + } + return new CFRDecompiler(decompiledSrc); + } + + public static DecompilerEnum getDefault() { + return CFR; + } + + public static DecompilerEnum fromStringOrDefault(String value) { + + DecompilerEnum result; + try { + result = DecompilerEnum.valueOf(value.toUpperCase()); + } catch (Exception e) { + result = getDefault(); + LOGGER.error("Could not find decompiler {}, defaulting on {}", value, result); + } + return result; + } +} diff --git a/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/JarAnalyzer.java b/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/JarAnalyzer.java index 1af5ca1..8dc702c 100644 --- a/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/JarAnalyzer.java +++ b/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/JarAnalyzer.java @@ -43,12 +43,12 @@ public JarAnalyzer(String decompilerName, String pathToJar, String[] additionalJ if(decompilerName != null && decompilerName.equalsIgnoreCase("CFR")) { LOGGER.trace("Using CFR (default) decompiler"); jr = new CustomJarLauncher.Builder(jarsList) - .withDecompilerEnum(CustomJarLauncher.DecompilerEnum.CFR).build(); + .withDecompilerEnum(DecompilerEnum.CFR).build(); } else if(decompilerName != null && decompilerName.equalsIgnoreCase("Fernflower")) { LOGGER.trace("Using Fernflower decompiler"); jr = new CustomJarLauncher.Builder(jarsList) - .withDecompilerEnum(CustomJarLauncher.DecompilerEnum.FERNFLOWER).build(); + .withDecompilerEnum(DecompilerEnum.FERNFLOWER).build(); } else { LOGGER.error("Decompiler name {} not recognised, using default decompiler", decompilerName); diff --git a/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/SourceAndJarAnalyzer.java b/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/SourceAndJarAnalyzer.java new file mode 100644 index 0000000..9899639 --- /dev/null +++ b/graph-builder/src/main/java/com/github/lucacampanella/callgraphflows/staticanalyzer/SourceAndJarAnalyzer.java @@ -0,0 +1,168 @@ +package com.github.lucacampanella.callgraphflows.staticanalyzer; + +import net.corda.core.flows.StartableByRPC; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.LineIterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spoon.Launcher; +import spoon.SpoonException; +import spoon.decompiler.Decompiler; +import spoon.reflect.declaration.CtClass; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +public class SourceAndJarAnalyzer extends AnalyzerWithModel { + + private boolean analyzeOnlySources = false; + private Set srcClassNamesSet; + + private static final Logger LOGGER = LoggerFactory.getLogger(SourceAndJarAnalyzer.class); + + public SourceAndJarAnalyzer(List pathsToFoldersOrSrc) throws IOException { + init(pathsToFoldersOrSrc, null, null, false); + } + + public SourceAndJarAnalyzer(String[] unsortedTypesFiles, DecompilerEnum decompilerEnum, + boolean analyzeOnlySources) throws IOException { + List jarPaths = new ArrayList<>(); + List otherPaths = new ArrayList<>(); + for(String path : unsortedTypesFiles) { + if(path.endsWith(".jar")) { + jarPaths.add(path); + } + else { + otherPaths.add(path); + } + } + init(otherPaths, jarPaths, decompilerEnum, analyzeOnlySources); + } + + public SourceAndJarAnalyzer(List pathsToFoldersOrSrc, List pathsToJars, + DecompilerEnum decompilerEnum, boolean analyzeOnlySources) throws IOException { + init(pathsToFoldersOrSrc, pathsToJars, decompilerEnum, analyzeOnlySources); + } + + private void init(List pathsToFoldersOrSrc, List pathsToJars, DecompilerEnum decompilerEnum, + boolean analyzeOnlySources) throws IOException { + + this.analyzeOnlySources = analyzeOnlySources; + + analysisName = pathsToFoldersOrSrc.stream().map( + pathToJar -> pathToJar.substring(pathToJar.lastIndexOf(System.getProperty("file.separator"))+1)). + collect(Collectors.joining(", ")); + + Set addedClassesNamesSet = new HashSet<>(); + + Launcher spoon = new Launcher(); + + if(pathsToFoldersOrSrc != null) { + for (String path : pathsToFoldersOrSrc) { + final File folderOrSrc = new File(path); + if (!folderOrSrc.exists()) { + throw new RuntimeException("File or folder " + folderOrSrc.getPath() + " does not exist"); + } + if (folderOrSrc.isDirectory()) { + addFolderToModel(addedClassesNamesSet, spoon, folderOrSrc); + } else { + addSingleFileToModel(addedClassesNamesSet, spoon, folderOrSrc); + } + } + } + + if(analyzeOnlySources) { + srcClassNamesSet = new HashSet<>(addedClassesNamesSet); //we save which classes derive from sources and + //not decompilation + } + + if(pathsToJars != null) { + String decompiledSrcPath = + Paths.get(System.getProperty("java.io.tmpdir"), "spoon-camp-tmp", "decompiledSrc").toString(); + final File decompiledSrcFolder = new File(decompiledSrcPath); + FileUtils.deleteDirectory(decompiledSrcFolder); + for(String path : pathsToJars) { + decompileJarToFolder(path, decompiledSrcPath, decompilerEnum); + } + addFolderToModel(addedClassesNamesSet, spoon, decompiledSrcFolder); + } + spoon.buildModel(); + model = spoon.getModel(); + } + + private static void addFolderToModel(Set addedClassesNamesSet, Launcher spoon, File folder) throws IOException { + final Collection sourceFiles = + FileUtils.listFiles(folder, new String[]{"java"}, true); + for (File srcFile : sourceFiles) { + addSingleFileToModel(addedClassesNamesSet, spoon, srcFile); + } + } + + private static void addSingleFileToModel(Set addedClassesNamesSet, Launcher spoon, File srcFile) throws IOException { + final String qualifiedName = findQualifiedName(srcFile); + if(!addedClassesNamesSet.contains(qualifiedName)) { + addedClassesNamesSet.add(qualifiedName); + spoon.addInputResource(srcFile.getAbsolutePath()); + } + else { + LOGGER.trace("File {} represents class {}, which was already added to the model, skipping", srcFile, qualifiedName); + } + } + + public static void decompileJarToFolder(String jarPath, + String ouputDir, + DecompilerEnum decompilerEnum) { + File decompiledDirectory = new File(ouputDir); + if (decompiledDirectory.exists() && !decompiledDirectory.canWrite()) { + throw new SpoonException("Dir " + decompiledDirectory.getPath() + " already exists and is not deletable."); + } + + if (!decompiledDirectory.exists()) { + decompiledDirectory.mkdirs(); + } + + Decompiler decompiler = decompilerEnum.getDecompiler(decompiledDirectory); + + File jar = new File(jarPath); + if (jar.exists() && jar.isFile()) { + decompiler.decompile(jar.getAbsolutePath()); + } else { + throw new SpoonException("Jar " + jar.getPath() + " not found."); + } + } + + public static String findQualifiedName(File srcFile) throws IOException { + String res = ""; + try (LineIterator lineIt = FileUtils.lineIterator(srcFile)) { + while (lineIt.hasNext()) { + String line = lineIt.nextLine(); + if(line.contains("package ")) { + res = line.substring( + line.indexOf("package ") + "package ".length(), + line.indexOf(';')); + break; + } + } + } + final String path = srcFile.getPath(); + res += "." + path + .substring(path + .lastIndexOf(System.getProperty("file.separator"))+1, + path.indexOf(".java")); + + return res; + } + + @Override + public List getClassesToBeAnalyzed() { + if(!analyzeOnlySources) { + return super.getClassesToBeAnalyzed(); + } + return super.getClassesToBeAnalyzed().stream().filter(klass -> + srcClassNamesSet.contains(klass.getTopLevelType().getQualifiedName())).collect(Collectors.toList()); + } + +} diff --git a/graph-builder/src/test/java/com/github/lucacampanella/callgraphflows/staticanalyzer/AnalyzerWithModelTest.java b/graph-builder/src/test/java/com/github/lucacampanella/callgraphflows/staticanalyzer/AnalyzerWithModelTest.java index a84c974..382dc1e 100644 --- a/graph-builder/src/test/java/com/github/lucacampanella/callgraphflows/staticanalyzer/AnalyzerWithModelTest.java +++ b/graph-builder/src/test/java/com/github/lucacampanella/callgraphflows/staticanalyzer/AnalyzerWithModelTest.java @@ -12,6 +12,7 @@ import spoon.reflect.declaration.CtClass; import spoon.reflect.declaration.CtType; +import java.io.IOException; import java.util.List; import java.util.stream.Collectors; @@ -23,7 +24,7 @@ class AnalyzerWithModelTest { static AnalyzerWithModel analyzerWithModel; @BeforeAll - static void setUp() { + static void setUp() throws IOException { analyzerWithModel = new SourceClassAnalyzer(fromClassSrcToPath(InitiatorBaseFlow.class), fromClassSrcToPath(ExtendingSuperclassTestFlow.class), fromClassSrcToPath(DoubleExtendingSuperclassTestFlow.class)); diff --git a/graph-builder/src/test/java/com/github/lucacampanella/callgraphflows/staticanalyzer/SourceAndJarAnalyzerTest.java b/graph-builder/src/test/java/com/github/lucacampanella/callgraphflows/staticanalyzer/SourceAndJarAnalyzerTest.java new file mode 100644 index 0000000..94efced --- /dev/null +++ b/graph-builder/src/test/java/com/github/lucacampanella/callgraphflows/staticanalyzer/SourceAndJarAnalyzerTest.java @@ -0,0 +1,45 @@ +package com.github.lucacampanella.callgraphflows.staticanalyzer; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Paths; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +class SourceAndJarAnalyzerTest extends AnalyzerWithModelTest { + + private static Logger LOGGER = LoggerFactory.getLogger(SourceAndJarAnalyzerTest.class); + private static String TEST_JAR_NAME = "JarAnalyzerTestJar.jar"; + private static String folderPath; + + @BeforeAll + static void setUp() throws IOException { + final URL jarURL = SourceAndJarAnalyzerTest.class.getClassLoader().getResource(TEST_JAR_NAME); + LOGGER.trace("{}", jarURL); + folderPath = Paths.get(Paths.get(jarURL.getPath()).getParent().toString(), "subclassestests").toString(); + LOGGER.trace("{}", folderPath); + analyzerWithModel = new SourceAndJarAnalyzer(Arrays.asList(folderPath), + Arrays.asList(jarURL.getPath()), + DecompilerEnum.CFR, false); + } + + @Test + void getAnalysisName() { + assertThat(analyzerWithModel.getAnalysisName()).isEqualTo("subclassestests"); + } + + @Test + void findQualifiedName() throws IOException { + final String qualifiedName = SourceAndJarAnalyzer.findQualifiedName( + Paths.get(folderPath, "InitiatorBaseFlow.java").toFile()); + assertThat(qualifiedName).isEqualTo( + "com.github.lucacampanella.callgraphflows.staticanalyzer.testclasses.subclassestests.InitiatorBaseFlow"); + } +} \ No newline at end of file diff --git a/plugin/src/main/java/com/github/lucacampanella/plugin/JarAnalyzerJavaExec.java b/plugin/src/main/java/com/github/lucacampanella/plugin/JarAnalyzerJavaExec.java index 6d9e220..d3a717a 100644 --- a/plugin/src/main/java/com/github/lucacampanella/plugin/JarAnalyzerJavaExec.java +++ b/plugin/src/main/java/com/github/lucacampanella/plugin/JarAnalyzerJavaExec.java @@ -34,6 +34,8 @@ public class JarAnalyzerJavaExec extends JavaExec { boolean drawLineNumbers = false; boolean removeJavaAgents = true; //remove agents like quasar that might be pluggen in to any javaexec task by the quasar plugin String logLevel = null; + List sourceFilesPath = null; + boolean analyzeOnlySourceFiles = false; @TaskAction @Override @@ -43,8 +45,12 @@ public void exec() { pathToExecJar = JarExecPathFinderUtils.getPathToExecJar(getProject()); } - List args = new ArrayList<>(Arrays.asList(pathToExecJar, pathToJar, "-o", outPath, "-d", decompilerName)); + List args = new ArrayList<>(Arrays.asList(pathToExecJar, pathToJar)); + if(sourceFilesPath != null) { + args.addAll(sourceFilesPath); + } + args.addAll(Arrays.asList("-o", outPath, "-d", decompilerName)); if(drawLineNumbers) { getLogger().info("drawLineNumbers = true"); args.add("-l"); @@ -52,7 +58,6 @@ public void exec() { getLogger().info("args = {}", args); this.setArgs(args); - //this.args(pathToExecJar, pathToJar, "-o", outPath, "-d", decompilerName); Level slf4jLogLevel = null; if(logLevel != null) { @@ -144,6 +149,17 @@ public String getOutPath() { return outPath; } + @Input + @Optional + public List getSourceFilesPath() { + return sourceFilesPath; + } + + @Input + public boolean isAnalyzeOnlySourceFiles() { + return analyzeOnlySourceFiles; + } + private LogLevel getCurrentLogLevel() { for(LogLevel logLevelIt : LogLevel.values()) { if(this.getLogger().isEnabled(logLevelIt)) { diff --git a/plugin/src/test/java/com/github/lucacampanella/plugin/FlowsDocBuilderPluginTest.java b/plugin/src/test/java/com/github/lucacampanella/plugin/FlowsDocBuilderPluginTest.java index 2e867ac..2029bf2 100644 --- a/plugin/src/test/java/com/github/lucacampanella/plugin/FlowsDocBuilderPluginTest.java +++ b/plugin/src/test/java/com/github/lucacampanella/plugin/FlowsDocBuilderPluginTest.java @@ -94,7 +94,7 @@ void outputSVGIsCorrect() throws ParserConfigurationException, SAXException, XPa assertThat(nodeContents).contains("SimpleFlowTest$Initiator", "@InitiatingFlow", "@StartableByRPC", - "session = initiateFlow(this.otherParty)", + "session = initiateFlow(otherParty)", "sendAndReceive(String, Boolean)", "receive(Boolean)", "send(String)"); diff --git a/simple-flow-project/build.gradle b/simple-flow-project/build.gradle index 7641b1e..7e4846f 100644 --- a/simple-flow-project/build.gradle +++ b/simple-flow-project/build.gradle @@ -50,6 +50,8 @@ jarAnalyzerTask { .orElseThrow { new RuntimeException("Could not find local executable jar") } } + sourceFilesPath = ["src/main/java"] + //logLevel = "wrongloglevel" //removeJavaAgents = false