diff --git a/core/src/main/java/edu/ucr/cs/riple/core/Annotator.java b/core/src/main/java/edu/ucr/cs/riple/core/Annotator.java index 6ec338793..bcace0934 100644 --- a/core/src/main/java/edu/ucr/cs/riple/core/Annotator.java +++ b/core/src/main/java/edu/ucr/cs/riple/core/Annotator.java @@ -26,16 +26,18 @@ import com.google.common.collect.ImmutableSet; import edu.ucr.cs.riple.core.explorers.BasicExplorer; -import edu.ucr.cs.riple.core.explorers.DownStreamDependencyExplorer; import edu.ucr.cs.riple.core.explorers.ExhaustiveExplorer; import edu.ucr.cs.riple.core.explorers.Explorer; import edu.ucr.cs.riple.core.explorers.OptimizedExplorer; +import edu.ucr.cs.riple.core.explorers.suppliers.ExhaustiveSupplier; +import edu.ucr.cs.riple.core.explorers.suppliers.TargetModuleSupplier; +import edu.ucr.cs.riple.core.global.GlobalAnalyzer; +import edu.ucr.cs.riple.core.global.GlobalAnalyzerImpl; +import edu.ucr.cs.riple.core.global.NoOpGlobalAnalyzer; import edu.ucr.cs.riple.core.injectors.AnnotationInjector; import edu.ucr.cs.riple.core.injectors.PhysicalInjector; import edu.ucr.cs.riple.core.metadata.field.FieldDeclarationAnalysis; import edu.ucr.cs.riple.core.metadata.field.FieldInitializationAnalysis; -import edu.ucr.cs.riple.core.metadata.index.Bank; -import edu.ucr.cs.riple.core.metadata.index.Error; import edu.ucr.cs.riple.core.metadata.index.Fix; import edu.ucr.cs.riple.core.metadata.method.MethodDeclarationTree; import edu.ucr.cs.riple.core.metadata.trackers.CompoundTracker; @@ -45,24 +47,34 @@ import edu.ucr.cs.riple.injector.location.OnField; import edu.ucr.cs.riple.scanner.Serializer; import java.util.HashMap; -import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +/** + * The main class of the core module. Responsible for analyzing the target module and injecting the + * corresponding annotations. + */ public class Annotator { + /** Injector instance. */ private final AnnotationInjector injector; + /** Annotator config. */ private final Config config; - // Set does not have get method, here we use map which retrieves elements efficiently. - public final HashMap reports; + /** + * Map of fix to their corresponding reports, used to detect processed fixes in a new batch of fix + * suggestion. Note: Set does not have get method, here we use map which retrieves elements + * efficiently. + */ + public final HashMap cachedReports; public Annotator(Config config) { this.config = config; - this.reports = new HashMap<>(); + this.cachedReports = new HashMap<>(); this.injector = new PhysicalInjector(config); } + /** Starts the annotating process consist of preprocess followed by explore phase. */ public void start() { preprocess(); long timer = config.log.startTimer(); @@ -71,10 +83,20 @@ public void start() { Utility.writeLog(config); } + /** + * Performs all the preprocessing tasks. + * + * + */ private void preprocess() { System.out.println("Preprocessing..."); Utility.setScannerCheckerActivation(config.target, true); - this.reports.clear(); + this.cachedReports.clear(); System.out.println("Making the first build..."); Utility.buildTarget(config, true); Set uninitializedFields = @@ -92,6 +114,7 @@ private void preprocess() { this.injector.injectAnnotations(initializers); } + /** Performs iterations of inference/injection until no unseen fix is suggested. */ private void explore() { Utility.setScannerCheckerActivation(config.target, true); Utility.buildTarget(config); @@ -99,91 +122,87 @@ private void explore() { FieldDeclarationAnalysis fieldDeclarationAnalysis = new FieldDeclarationAnalysis(config.target); MethodDeclarationTree tree = new MethodDeclarationTree(config.target.dir.resolve(Serializer.METHOD_INFO_FILE_NAME)); - // downStreamDependencyExplorer analyzes effects of all public APIs on downstream dependencies. + // globalAnalyzer analyzes effects of all public APIs on downstream dependencies. // Through iterations, since the source code for downstream dependencies does not change and the // computation does not depend on the changes in the target module, it will compute the same // result in each iteration, therefore we perform the analysis only once and reuse it in each // iteration. - DownStreamDependencyExplorer downStreamDependencyExplorer = - new DownStreamDependencyExplorer(config, tree); - if (config.downStreamDependenciesAnalysisActivated) { - downStreamDependencyExplorer.explore(); - } - // Set of fixes collected from downstream dependencies that are triggered due to changes in the - // upstream module (target) public API. - Set triggeredFixesFromDownstreamDependencies = new HashSet<>(); - - while (true) { - Utility.buildTarget(config); - ImmutableSet fixes = - Stream.concat( - triggeredFixesFromDownstreamDependencies.stream(), - Utility.readFixesFromOutputDirectory( - config.target, Fix.factory(config, fieldDeclarationAnalysis))) - .filter(fix -> !config.useCache || !reports.containsKey(fix)) - .collect(ImmutableSet.toImmutableSet()); - Bank errorBank = new Bank<>(config.target.dir.resolve("errors.tsv"), Error::new); - Bank fixBank = - new Bank<>( - config.target.dir.resolve("fixes.tsv"), - Fix.factory(config, fieldDeclarationAnalysis)); - tree = new MethodDeclarationTree(config.target.dir.resolve(Serializer.METHOD_INFO_FILE_NAME)); - RegionTracker tracker = new CompoundTracker(config.target, tree); - Explorer explorer = - config.exhaustiveSearch - ? new ExhaustiveExplorer(injector, errorBank, fixBank, fixes, tree, config) - : config.optimized - ? new OptimizedExplorer( - injector, errorBank, fixBank, tracker, fixes, tree, config.depth, config) - : new BasicExplorer(injector, errorBank, fixBank, fixes, tree, config); - ImmutableSet latestReports = explorer.explore(); - int sizeBefore = reports.size(); + GlobalAnalyzer globalAnalyzer = + config.downStreamDependenciesAnalysisActivated + ? new GlobalAnalyzerImpl(config, tree) + : new NoOpGlobalAnalyzer(); + globalAnalyzer.analyzeDownstreamDependencies(); + boolean noNewFixTriggered = false; + while (!noNewFixTriggered) { + ImmutableSet latestReports = + executeNextIteration(globalAnalyzer, fieldDeclarationAnalysis); + int sizeBefore = cachedReports.size(); + // Update cached reports store. latestReports.forEach( report -> { if (config.downStreamDependenciesAnalysisActivated) { - report.computeBoundariesOfEffectivenessOnDownstreamDependencies( - downStreamDependencyExplorer); + report.computeBoundariesOfEffectivenessOnDownstreamDependencies(globalAnalyzer); } - reports.putIfAbsent(report.root, report); - reports.get(report.root).localEffect = report.localEffect; - reports.get(report.root).finished = report.finished; - reports.get(report.root).tree = report.tree; - reports.get(report.root).triggered = report.triggered; + cachedReports.putIfAbsent(report.root, report); + Report cachedReport = cachedReports.get(report.root); + cachedReport.localEffect = report.localEffect; + cachedReport.finished = report.finished; + cachedReport.tree = report.tree; + cachedReport.triggeredFixes = report.triggeredFixes; + cachedReport.triggeredErrors = report.triggeredErrors; }); - injector.injectFixes( + + // Inject approved fixes. + Set selectedFixes = latestReports.stream() .filter(report -> report.getOverallEffect(config) < 1) .flatMap(report -> config.chain ? report.tree.stream() : Stream.of(report.root)) - .collect(Collectors.toSet())); - // Collect impacted parameters from changes in target module due to usages in downstream - // dependencies. - if (config.downStreamDependenciesAnalysisActivated) { - triggeredFixesFromDownstreamDependencies = - latestReports.stream() - .flatMap( - report -> - downStreamDependencyExplorer.getImpactedParameters(report.tree).stream() - .map( - onParameter -> - new Fix( - new AddAnnotation(onParameter, config.nullableAnnot), - "PASSING_NULLABLE", - onParameter.clazz, - onParameter.method))) - // Remove already processed fixes - .filter(input -> !reports.containsKey(input)) - .collect(Collectors.toSet()); - } - if (sizeBefore == this.reports.size() - && triggeredFixesFromDownstreamDependencies.size() == 0) { - System.out.println("\nFinished annotating."); - Utility.writeReports( - config, reports.values().stream().collect(ImmutableSet.toImmutableSet())); - return; - } + .collect(Collectors.toSet()); + injector.injectFixes(selectedFixes); + + // Update impact saved state. + globalAnalyzer.updateImpactsAfterInjection(selectedFixes); + if (config.disableOuterLoop) { - return; + break; } + noNewFixTriggered = sizeBefore == this.cachedReports.size(); } + System.out.println("\nFinished annotating."); + Utility.writeReports( + config, cachedReports.values().stream().collect(ImmutableSet.toImmutableSet())); + } + + /** + * Executes the next iteration of exploration. + * + * @param globalAnalyzer Global Analyzer instance. + * @param fieldDeclarationAnalysis Field Declaration analysis to detect fixes on multiple inline + * field declaration statements. + * @return Immutable set of reports from the executed iteration. + */ + private ImmutableSet executeNextIteration( + GlobalAnalyzer globalAnalyzer, FieldDeclarationAnalysis fieldDeclarationAnalysis) { + Utility.buildTarget(config); + // Suggested fixes of target at the current state. + ImmutableSet fixes = + Utility.readFixesFromOutputDirectory( + config.target, Fix.factory(config, fieldDeclarationAnalysis)) + .filter(fix -> !config.useCache || !cachedReports.containsKey(fix)) + .collect(ImmutableSet.toImmutableSet()); + + // Initializing required explorer instances. + MethodDeclarationTree tree = + new MethodDeclarationTree(config.target.dir.resolve(Serializer.METHOD_INFO_FILE_NAME)); + RegionTracker tracker = new CompoundTracker(config.target, tree); + TargetModuleSupplier supplier = new TargetModuleSupplier(config, tree); + Explorer explorer = + config.exhaustiveSearch + ? new ExhaustiveExplorer(fixes, new ExhaustiveSupplier(config, tree)) + : config.optimized + ? new OptimizedExplorer(fixes, supplier, globalAnalyzer, tracker) + : new BasicExplorer(fixes, supplier, globalAnalyzer); + // Result of the iteration analysis. + return explorer.explore(); } } diff --git a/core/src/main/java/edu/ucr/cs/riple/core/Config.java b/core/src/main/java/edu/ucr/cs/riple/core/Config.java index c4b9aaa5a..f788488b0 100644 --- a/core/src/main/java/edu/ucr/cs/riple/core/Config.java +++ b/core/src/main/java/edu/ucr/cs/riple/core/Config.java @@ -263,7 +263,7 @@ public Config(String[] args) { HelpFormatter formatter = new HelpFormatter(); CommandLineParser parser = new DefaultParser(); - CommandLine cmd = null; + CommandLine cmd; if (args.length == 1 && (args[0].equals("-h") || args[0].equals("--help"))) { showHelp(formatter, options); diff --git a/core/src/main/java/edu/ucr/cs/riple/core/Main.java b/core/src/main/java/edu/ucr/cs/riple/core/Main.java index fa37bbbd4..00e988e9a 100644 --- a/core/src/main/java/edu/ucr/cs/riple/core/Main.java +++ b/core/src/main/java/edu/ucr/cs/riple/core/Main.java @@ -26,8 +26,15 @@ import java.nio.file.Paths; +/** Starting point. */ public class Main { + /** + * Starting point. + * + * @param args if flag '--path' is found, all configurations will be set up based on the given + * json file, otherwise they will be set up according to the set of received cli arguments. + */ public static void main(String[] args) { Config config; if (args.length == 2 && args[0].equals("--path")) { diff --git a/core/src/main/java/edu/ucr/cs/riple/core/Report.java b/core/src/main/java/edu/ucr/cs/riple/core/Report.java index 9d9eccdc3..42fcff19f 100644 --- a/core/src/main/java/edu/ucr/cs/riple/core/Report.java +++ b/core/src/main/java/edu/ucr/cs/riple/core/Report.java @@ -24,11 +24,13 @@ package edu.ucr.cs.riple.core; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; -import edu.ucr.cs.riple.core.explorers.DownStreamDependencyExplorer; +import edu.ucr.cs.riple.core.global.GlobalAnalyzer; +import edu.ucr.cs.riple.core.metadata.index.Error; import edu.ucr.cs.riple.core.metadata.index.Fix; import edu.ucr.cs.riple.injector.location.Location; -import java.util.HashSet; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -48,7 +50,11 @@ public class Report { /** * Set of fixes that will be triggered in target module if fix tree is applied to the source code. */ - public Set triggered; + public ImmutableSet triggeredFixes; + /** + * Set of fixes that will be triggered in target module if fix tree is applied to the source code. + */ + public ImmutableList triggeredErrors; /** If true, all leaves of fix tree are not resolvable by any {@code @Nullable} annotation. */ public boolean finished; /** @@ -67,7 +73,8 @@ public Report(Fix root, int localEffect) { this.root = root; this.tree = Sets.newHashSet(root); this.finished = false; - this.triggered = new HashSet<>(); + this.triggeredFixes = ImmutableSet.of(); + this.triggeredErrors = ImmutableList.of(); this.lowerBoundEffectOnDownstreamDependencies = 0; this.upperBoundEffectOnDownstreamDependencies = 0; } @@ -107,17 +114,15 @@ public boolean testEquals(Config config, Report other) { } this.tree.add(this.root); other.tree.add(other.root); - Set thisTree = - this.tree.stream().map(fix -> fix.change.location).collect(Collectors.toSet()); - Set otherTree = - other.tree.stream().map(fix -> fix.change.location).collect(Collectors.toSet()); + Set thisTree = this.tree.stream().map(Fix::toLocation).collect(Collectors.toSet()); + Set otherTree = other.tree.stream().map(Fix::toLocation).collect(Collectors.toSet()); if (!thisTree.equals(otherTree)) { return false; } Set thisTriggered = - this.triggered.stream().map(fix -> fix.change.location).collect(Collectors.toSet()); + this.triggeredFixes.stream().map(Fix::toLocation).collect(Collectors.toSet()); Set otherTriggered = - other.triggered.stream().map(fix -> fix.change.location).collect(Collectors.toSet()); + other.triggeredFixes.stream().map(Fix::toLocation).collect(Collectors.toSet()); return otherTriggered.equals(thisTriggered); } @@ -128,7 +133,7 @@ public String toString() { + ", " + root + ", " - + tree.stream().map(fix -> fix.change.location).collect(Collectors.toSet()); + + tree.stream().map(Fix::toLocation).collect(Collectors.toSet()); } /** @@ -137,8 +142,7 @@ public String toString() { * * @param explorer Downstream dependency instance. */ - public void computeBoundariesOfEffectivenessOnDownstreamDependencies( - DownStreamDependencyExplorer explorer) { + public void computeBoundariesOfEffectivenessOnDownstreamDependencies(GlobalAnalyzer explorer) { this.lowerBoundEffectOnDownstreamDependencies = explorer.computeLowerBoundOfNumberOfErrors(tree); this.upperBoundEffectOnDownstreamDependencies = @@ -150,12 +154,14 @@ public void computeBoundariesOfEffectivenessOnDownstreamDependencies( * dependency analysis is activated, overall effect will be sum of local effect and lower bound of * number of errors on downstream dependencies. * + * @param config Annotator config. * @return Overall effect ot applying the fix tree. */ public int getOverallEffect(Config config) { - return config.downStreamDependenciesAnalysisActivated - ? this.localEffect + this.lowerBoundEffectOnDownstreamDependencies - : this.localEffect; + if (config.downStreamDependenciesAnalysisActivated) { + return this.localEffect + this.lowerBoundEffectOnDownstreamDependencies; + } + return this.localEffect; } /** @@ -175,4 +181,16 @@ public int getLowerBoundEffectOnDownstreamDependencies() { public int getUpperBoundEffectOnDownstreamDependencies() { return upperBoundEffectOnDownstreamDependencies; } + + /** + * Checks if the report needs further investigation. If a fix is suggested from downstream + * dependencies, it should still be included the next cycle. + * + * @param config Annotator config instance. + * @return true, if report needs further investigation. + */ + public boolean isInProgress(Config config) { + return (!finished && (!config.bailout || localEffect > 0)) + || triggeredFixes.stream().anyMatch(input -> !input.fixSourceIsInTarget); + } } diff --git a/core/src/main/java/edu/ucr/cs/riple/core/explorers/BasicExplorer.java b/core/src/main/java/edu/ucr/cs/riple/core/explorers/BasicExplorer.java index e0d346aa4..055f435f7 100644 --- a/core/src/main/java/edu/ucr/cs/riple/core/explorers/BasicExplorer.java +++ b/core/src/main/java/edu/ucr/cs/riple/core/explorers/BasicExplorer.java @@ -25,27 +25,24 @@ package edu.ucr.cs.riple.core.explorers; import com.google.common.collect.ImmutableSet; -import edu.ucr.cs.riple.core.Config; -import edu.ucr.cs.riple.core.injectors.AnnotationInjector; -import edu.ucr.cs.riple.core.metadata.index.Bank; +import edu.ucr.cs.riple.core.explorers.suppliers.Supplier; +import edu.ucr.cs.riple.core.global.GlobalAnalyzer; +import edu.ucr.cs.riple.core.metadata.graph.Node; import edu.ucr.cs.riple.core.metadata.index.Error; import edu.ucr.cs.riple.core.metadata.index.Fix; import edu.ucr.cs.riple.core.metadata.index.Result; -import edu.ucr.cs.riple.core.metadata.method.MethodDeclarationTree; import edu.ucr.cs.riple.core.util.Utility; +import edu.ucr.cs.riple.injector.changes.AddAnnotation; +import edu.ucr.cs.riple.injector.location.Location; +import java.util.Collection; import java.util.Set; +import java.util.stream.Collectors; import me.tongfei.progressbar.ProgressBar; public class BasicExplorer extends Explorer { - public BasicExplorer( - AnnotationInjector injector, - Bank errorBank, - Bank fixBank, - ImmutableSet fixes, - MethodDeclarationTree methodDeclarationTree, - Config config) { - super(injector, errorBank, fixBank, fixes, methodDeclarationTree, config.depth, config); + public BasicExplorer(ImmutableSet fixes, Supplier supplier, GlobalAnalyzer globalAnalyzer) { + super(fixes, supplier, globalAnalyzer); } @Override @@ -65,14 +62,39 @@ protected void executeNextCycle() { fixBank.saveState(false, true); Result errorComparisonResult = errorBank.compare(); node.effect = errorComparisonResult.size; + Collection fixComparisonResultDif = fixBank.compare().dif; + addTriggeredFixesFromDownstream(node, fixComparisonResultDif); node.updateStatus( errorComparisonResult.size, fixes, - fixBank.compare().dif, + fixComparisonResultDif, errorComparisonResult.dif, methodDeclarationTree); injector.removeFixes(fixes); }); pb.close(); } + + /** + * Updates list of triggered fixes with fixes triggered from downstream dependencies. + * + * @param node Node in process. + * @param localTriggeredFixes Collection of triggered fixes locally. + */ + public void addTriggeredFixesFromDownstream(Node node, Collection localTriggeredFixes) { + Set currentLocationTargetedByTree = + node.tree.stream().map(Fix::toLocation).collect(Collectors.toSet()); + localTriggeredFixes.addAll( + globalAnalyzer.getImpactedParameters(node.tree).stream() + .filter(input -> !currentLocationTargetedByTree.contains(input)) + .map( + onParameter -> + new Fix( + new AddAnnotation(onParameter, config.nullableAnnot), + "PASSING_NULLABLE", + onParameter.clazz, + onParameter.method, + false)) + .collect(Collectors.toList())); + } } diff --git a/core/src/main/java/edu/ucr/cs/riple/core/explorers/DownStreamDependencyExplorer.java b/core/src/main/java/edu/ucr/cs/riple/core/explorers/DownStreamDependencyExplorer.java deleted file mode 100644 index 94d68902d..000000000 --- a/core/src/main/java/edu/ucr/cs/riple/core/explorers/DownStreamDependencyExplorer.java +++ /dev/null @@ -1,352 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2022 Nima Karimipour - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package edu.ucr.cs.riple.core.explorers; - -import com.google.common.collect.ImmutableMultimap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Multimaps; -import edu.ucr.cs.riple.core.Config; -import edu.ucr.cs.riple.core.ModuleInfo; -import edu.ucr.cs.riple.core.Report; -import edu.ucr.cs.riple.core.injectors.AnnotationInjector; -import edu.ucr.cs.riple.core.injectors.VirtualInjector; -import edu.ucr.cs.riple.core.metadata.field.FieldDeclarationAnalysis; -import edu.ucr.cs.riple.core.metadata.index.Bank; -import edu.ucr.cs.riple.core.metadata.index.Error; -import edu.ucr.cs.riple.core.metadata.index.Fix; -import edu.ucr.cs.riple.core.metadata.method.MethodDeclarationTree; -import edu.ucr.cs.riple.core.metadata.method.MethodNode; -import edu.ucr.cs.riple.core.metadata.trackers.MethodRegionTracker; -import edu.ucr.cs.riple.core.metadata.trackers.RegionTracker; -import edu.ucr.cs.riple.core.util.Utility; -import edu.ucr.cs.riple.injector.changes.AddAnnotation; -import edu.ucr.cs.riple.injector.location.OnMethod; -import edu.ucr.cs.riple.injector.location.OnParameter; -import java.util.Collections; -import java.util.HashMap; -import java.util.Optional; -import java.util.OptionalInt; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.annotation.Nullable; - -/** - * Analyzer for downstream dependencies. - * - *

This class analyzes switching the nullability of public APIs of one compilation target, in - * order to compute the effect on said target's downstream dependencies of adding annotations to - * those APIs. It does so by building said downstream dependencies. It collects the effect (number - * of additional errors) of each such change, by summing across all downstream dependencies. This - * data then be fed to the Annotator main process in the decision process. - */ -public class DownStreamDependencyExplorer { - - /** Set of downstream dependencies. */ - private final ImmutableSet modules; - /** Public APIs in the target modules that have a non-primitive return value. */ - private final ImmutableMultimap methods; - /** Annotator Config. */ - private final Config config; - /** Method declaration tree instance. */ - private final MethodDeclarationTree tree; - /** - * VirtualInjector instance. Annotations should only be loaded in a library model and not - * physically injected. - */ - private final VirtualInjector injector; - - public DownStreamDependencyExplorer(Config config, MethodDeclarationTree tree) { - this.config = config; - this.modules = config.downstreamInfo; - this.tree = tree; - this.injector = new VirtualInjector(config); - this.methods = - Multimaps.index( - tree.getPublicMethodsWithNonPrimitivesReturn().stream() - .map(MethodStatus::new) - .collect(ImmutableSet.toImmutableSet()), - MethodStatus::hashCode); - } - - /** - * Exploration phase begins. When this method finishes, all methods effect on downstream - * dependencies are calculated. - */ - public void explore() { - System.out.println("Analysing downstream dependencies..."); - Utility.setScannerCheckerActivation(modules, true); - Utility.buildDownstreamDependencies(config); - Utility.setScannerCheckerActivation(modules, false); - // Collect callers of public APIs in module. - MethodRegionTracker tracker = new MethodRegionTracker(config.downstreamInfo, tree); - // Generate fixes corresponding methods. - ImmutableSet fixes = - methods.values().stream() - .filter( - input -> - !tracker - .getCallersOfMethod(input.node.location.clazz, input.node.location.method) - .isEmpty()) // skip methods that are not called anywhere. - .map( - methodStatus -> - new Fix( - new AddAnnotation( - new OnMethod( - "null", - methodStatus.node.location.clazz, - methodStatus.node.location.method), - config.nullableAnnot), - "null", - "null", - "null")) - .collect(ImmutableSet.toImmutableSet()); - // Explorer initializations. - FieldDeclarationAnalysis fieldDeclarationAnalysis = - new FieldDeclarationAnalysis(config.downstreamInfo); - Bank errorBank = - new Bank<>( - config.downstreamInfo.stream() - .map(info -> info.dir.resolve("errors.tsv")) - .collect(ImmutableSet.toImmutableSet()), - Error::new); - Bank fixBank = - new Bank<>( - config.downstreamInfo.stream() - .map(info -> info.dir.resolve("fixes.tsv")) - .collect(ImmutableSet.toImmutableSet()), - Fix.factory(config, fieldDeclarationAnalysis)); - OptimizedDownstreamDependencyAnalyzer explorer = - new OptimizedDownstreamDependencyAnalyzer( - injector, errorBank, fixBank, tracker, fixes, tree, 1, config); - ImmutableSet reports = explorer.explore(); - // Update method status based on the results. - methods - .values() - .forEach( - method -> { - MethodNode node = method.node; - method.impactedParameters = explorer.getImpactedParameters(node.location); - Optional optional = - reports.stream() - .filter(input -> input.root.toMethod().equals(node.location)) - .findAny(); - optional.ifPresent(report -> method.effect += report.localEffect); - }); - System.out.println("Analysing downstream dependencies completed!"); - } - - /** - * Retrieves the corresponding {@link MethodStatus} to a fix. - * - * @param fix Target fix. - * @return Corresponding {@link MethodStatus}, null if not located. - */ - @Nullable - private MethodStatus fetchStatus(Fix fix) { - if (!fix.isOnMethod()) { - return null; - } - OnMethod onMethod = fix.toMethod(); - int predictedHash = MethodStatus.hash(onMethod.method, onMethod.clazz); - Optional optional = - this.methods.get(predictedHash).stream() - .filter(m -> m.node.location.equals(onMethod)) - .findAny(); - return optional.orElse(null); - } - - /** - * Returns the effect of applying a fix on the target on downstream dependencies. - * - * @param fix Fix targeting an element in target. - * @return Effect on downstream dependencies. - */ - private int effectOnDownstreamDependencies(Fix fix) { - MethodStatus status = fetchStatus(fix); - return status == null ? 0 : status.effect; - } - - /** - * Returns the lower bound of number of errors of applying a fix and its associated chain of fixes - * on the target on downstream dependencies. - * - * @param tree Tree of the fix tree associated to root. - * @return Lower bound of number of errors on downstream dependencies. - */ - public int computeLowerBoundOfNumberOfErrors(Set tree) { - OptionalInt lowerBoundEffectOfChainOptional = - tree.stream().mapToInt(this::effectOnDownstreamDependencies).max(); - if (lowerBoundEffectOfChainOptional.isEmpty()) { - return 0; - } - return lowerBoundEffectOfChainOptional.getAsInt(); - } - - /** - * Returns the upper bound of number of errors of applying a fix and its associated chain of fixes - * on the target on downstream dependencies. - * - * @param tree Tree of the fix tree associated to root. - * @return Lower bound of number of errors on downstream dependencies. - */ - public int computeUpperBoundOfNumberOfErrors(Set tree) { - return tree.stream().mapToInt(this::effectOnDownstreamDependencies).sum(); - } - - /** - * Returns set of parameters that will receive {@code @Nullable}, if any of the methods in the - * fixTree are annotated as {@code @Nullable}. - * - * @param fixTree Fix tree. - * @return Immutable set of impacted parameters. - */ - public ImmutableSet getImpactedParameters(Set fixTree) { - return fixTree.stream() - .filter(Fix::isOnMethod) - .flatMap( - fix -> { - MethodStatus status = fetchStatus(fix); - return status == null ? Stream.of() : status.impactedParameters.stream(); - }) - .collect(ImmutableSet.toImmutableSet()); - } - - /** Container class for storing overall effect of each method. */ - private static class MethodStatus { - /** Node in {@link MethodDeclarationTree} corresponding to a public method. */ - final MethodNode node; - /** - * Set of parameters in target module that will receive {@code Nullable} value if targeted - * method in node is annotated as {@code @Nullable}. - */ - public Set impactedParameters; - /** - * Effect of injecting a {@code Nullable} annotation on pointing method of node on downstream - * dependencies. - */ - int effect; - - public MethodStatus(MethodNode node) { - this.node = node; - this.effect = 0; - } - - @Override - public int hashCode() { - return hash(node.location.method, node.location.clazz); - } - - /** - * Calculates hash. This method is used outside this class to calculate the expected hash based - * on instance's properties value if the actual instance is not available. - * - * @param method Method signature. - * @param clazz Fully qualified name of the containing class. - * @return Expected hash. - */ - public static int hash(String method, String clazz) { - return MethodNode.hash(method, clazz); - } - } - - /** Explorer for analyzing downstream dependencies. */ - private static class OptimizedDownstreamDependencyAnalyzer extends OptimizedExplorer { - - /** - * Map of public methods in target module to parameters in target module, which are source of - * nullable flow back to upstream module (target) from downstream dependencies, if annotated as - * {@code @Nullable}. - */ - private final HashMap> nullableFlowMap; - - public OptimizedDownstreamDependencyAnalyzer( - AnnotationInjector injector, - Bank errorBank, - Bank fixBank, - RegionTracker tracker, - ImmutableSet fixes, - MethodDeclarationTree methodDeclarationTree, - int depth, - Config config) { - super(injector, errorBank, fixBank, tracker, fixes, methodDeclarationTree, depth, config); - this.nullableFlowMap = new HashMap<>(); - } - - @Override - public void rerunAnalysis() { - Utility.buildDownstreamDependencies(config); - } - - @Override - protected void finalizeReports() { - super.finalizeReports(); - // Collect impacted parameters in target module by downstream dependencies. - graph - .getNodes() - .forEach( - node -> - node.root.ifOnMethod( - method -> { - // Impacted parameters. - Set parameters = - node.triggeredErrors.stream() - .filter( - error -> - error.nonnullTarget != null - && error.nonnullTarget.isOnParameter() - // Method is declared in the target module. - && methodDeclarationTree.declaredInModule( - error.nonnullTarget.toMethod())) - .map(error -> error.nonnullTarget.toParameter()) - .collect(Collectors.toSet()); - if (!parameters.isEmpty()) { - // Update uri for each parameter. These triggered fixes does not have an - // actual physical uri since they are provided as a jar file in downstream - // dependencies. - parameters.forEach( - onParameter -> - onParameter.uri = - methodDeclarationTree.findNode( - onParameter.method, onParameter.clazz) - .location - .uri); - nullableFlowMap.put(method, parameters); - } - })); - } - - /** - * Returns set of parameters that will receive {@code @Nullable} if the passed method is - * annotated as {@code @Nullable}. - * - * @param method Method to be annotated. - * @return Set of impacted parameters. If no parameter is impacted, empty set will be returned. - */ - private Set getImpactedParameters(OnMethod method) { - return nullableFlowMap.getOrDefault(method, Collections.emptySet()); - } - } -} diff --git a/core/src/main/java/edu/ucr/cs/riple/core/explorers/ExhaustiveExplorer.java b/core/src/main/java/edu/ucr/cs/riple/core/explorers/ExhaustiveExplorer.java index a480bf6a0..38251eba4 100644 --- a/core/src/main/java/edu/ucr/cs/riple/core/explorers/ExhaustiveExplorer.java +++ b/core/src/main/java/edu/ucr/cs/riple/core/explorers/ExhaustiveExplorer.java @@ -25,23 +25,14 @@ package edu.ucr.cs.riple.core.explorers; import com.google.common.collect.ImmutableSet; -import edu.ucr.cs.riple.core.Config; -import edu.ucr.cs.riple.core.injectors.AnnotationInjector; -import edu.ucr.cs.riple.core.metadata.index.Bank; -import edu.ucr.cs.riple.core.metadata.index.Error; +import edu.ucr.cs.riple.core.explorers.suppliers.ExhaustiveSupplier; +import edu.ucr.cs.riple.core.global.NoOpGlobalAnalyzer; import edu.ucr.cs.riple.core.metadata.index.Fix; -import edu.ucr.cs.riple.core.metadata.method.MethodDeclarationTree; public class ExhaustiveExplorer extends Explorer { - public ExhaustiveExplorer( - AnnotationInjector injector, - Bank errorBank, - Bank fixBank, - ImmutableSet fixes, - MethodDeclarationTree tree, - Config config) { - super(injector, errorBank, fixBank, fixes, tree, config.depth, config); + public ExhaustiveExplorer(ImmutableSet fixes, ExhaustiveSupplier supplier) { + super(fixes, supplier, new NoOpGlobalAnalyzer()); } @Override diff --git a/core/src/main/java/edu/ucr/cs/riple/core/explorers/Explorer.java b/core/src/main/java/edu/ucr/cs/riple/core/explorers/Explorer.java index 5655ee4aa..62850530b 100644 --- a/core/src/main/java/edu/ucr/cs/riple/core/explorers/Explorer.java +++ b/core/src/main/java/edu/ucr/cs/riple/core/explorers/Explorer.java @@ -27,6 +27,8 @@ import com.google.common.collect.ImmutableSet; import edu.ucr.cs.riple.core.Config; import edu.ucr.cs.riple.core.Report; +import edu.ucr.cs.riple.core.explorers.suppliers.Supplier; +import edu.ucr.cs.riple.core.global.GlobalAnalyzer; import edu.ucr.cs.riple.core.injectors.AnnotationInjector; import edu.ucr.cs.riple.core.metadata.graph.ConflictGraph; import edu.ucr.cs.riple.core.metadata.graph.Node; @@ -34,49 +36,44 @@ import edu.ucr.cs.riple.core.metadata.index.Error; import edu.ucr.cs.riple.core.metadata.index.Fix; import edu.ucr.cs.riple.core.metadata.method.MethodDeclarationTree; +import java.util.HashSet; public abstract class Explorer { + protected final AnnotationInjector injector; protected final Bank errorBank; protected final Bank fixBank; protected final ImmutableSet reports; protected final Config config; protected final ConflictGraph graph; - protected final MethodDeclarationTree methodDeclarationTree; - + protected final GlobalAnalyzer globalAnalyzer; protected final int depth; - public Explorer( - AnnotationInjector injector, - Bank errorBank, - Bank fixBank, - ImmutableSet fixes, - MethodDeclarationTree methodDeclarationTree, - int depth, - Config config) { - this.injector = injector; - this.errorBank = errorBank; - this.fixBank = fixBank; - this.methodDeclarationTree = methodDeclarationTree; + public Explorer(ImmutableSet fixes, Supplier supplier, GlobalAnalyzer globalAnalyzer) { + this.injector = supplier.getInjector(); + this.errorBank = supplier.getErrorBank(); + this.fixBank = supplier.getFixBank(); + this.methodDeclarationTree = supplier.getMethodDeclarationTree(); this.reports = fixes.stream().map(fix -> new Report(fix, 1)).collect(ImmutableSet.toImmutableSet()); - this.config = config; - this.depth = depth; + this.globalAnalyzer = globalAnalyzer; + this.depth = supplier.depth(); + this.config = supplier.getConfig(); this.graph = new ConflictGraph(); } protected void initializeFixGraph() { this.graph.clear(); this.reports.stream() - .filter(report -> !report.finished && (!config.bailout || report.localEffect > 0)) + .filter(input -> input.isInProgress(config)) .forEach( report -> { Fix root = report.root; Node node = graph.addNodeToVertices(root); node.setOrigins(fixBank); node.report = report; - node.triggeredFixes = report.triggered; + node.triggeredFixes = new HashSet<>(report.triggeredFixes); node.tree.addAll(report.tree); node.mergeTriggered(); }); @@ -90,7 +87,8 @@ protected void finalizeReports() { Report report = node.report; report.localEffect = node.effect; report.tree = node.tree; - report.triggered = node.triggeredFixes; + report.triggeredFixes = ImmutableSet.copyOf(node.triggeredFixes); + report.triggeredErrors = node.triggeredErrors; report.finished = !node.changed; }); } diff --git a/core/src/main/java/edu/ucr/cs/riple/core/explorers/OptimizedExplorer.java b/core/src/main/java/edu/ucr/cs/riple/core/explorers/OptimizedExplorer.java index acad2bd7c..9cf45816f 100644 --- a/core/src/main/java/edu/ucr/cs/riple/core/explorers/OptimizedExplorer.java +++ b/core/src/main/java/edu/ucr/cs/riple/core/explorers/OptimizedExplorer.java @@ -25,14 +25,12 @@ package edu.ucr.cs.riple.core.explorers; import com.google.common.collect.ImmutableSet; -import edu.ucr.cs.riple.core.Config; -import edu.ucr.cs.riple.core.injectors.AnnotationInjector; +import edu.ucr.cs.riple.core.explorers.suppliers.Supplier; +import edu.ucr.cs.riple.core.global.GlobalAnalyzer; import edu.ucr.cs.riple.core.metadata.graph.Node; -import edu.ucr.cs.riple.core.metadata.index.Bank; import edu.ucr.cs.riple.core.metadata.index.Error; import edu.ucr.cs.riple.core.metadata.index.Fix; import edu.ucr.cs.riple.core.metadata.index.Result; -import edu.ucr.cs.riple.core.metadata.method.MethodDeclarationTree; import edu.ucr.cs.riple.core.metadata.trackers.Region; import edu.ucr.cs.riple.core.metadata.trackers.RegionTracker; import edu.ucr.cs.riple.core.util.Utility; @@ -43,19 +41,15 @@ import java.util.stream.Collectors; import me.tongfei.progressbar.ProgressBar; -public class OptimizedExplorer extends Explorer { +public class OptimizedExplorer extends BasicExplorer { private final RegionTracker tracker; public OptimizedExplorer( - AnnotationInjector injector, - Bank errorBank, - Bank fixBank, - RegionTracker tracker, ImmutableSet fixes, - MethodDeclarationTree tree, - int depth, - Config config) { - super(injector, errorBank, fixBank, fixes, tree, depth, config); + Supplier supplier, + GlobalAnalyzer globalAnalyzer, + RegionTracker tracker) { + super(fixes, supplier, globalAnalyzer); this.tracker = tracker; } @@ -86,18 +80,20 @@ protected void executeNextCycle() { fixBank.saveState(false, true); group.forEach( node -> { - int totalEffect = 0; + int localEffect = 0; List triggeredFixes = new ArrayList<>(); List triggeredErrors = new ArrayList<>(); for (Region region : node.regions) { - Result res = errorBank.compareByMethod(region.clazz, region.method, false); - totalEffect += res.size; - triggeredErrors.addAll(res.dif); + Result errorComparisonResult = + errorBank.compareByMethod(region.clazz, region.method, false); + localEffect += errorComparisonResult.size; + triggeredErrors.addAll(errorComparisonResult.dif); triggeredFixes.addAll( - new ArrayList<>(fixBank.compareByMethod(region.clazz, region.method, false).dif)); + fixBank.compareByMethod(region.clazz, region.method, false).dif); } + addTriggeredFixesFromDownstream(node, triggeredFixes); node.updateStatus( - totalEffect, fixes, triggeredFixes, triggeredErrors, methodDeclarationTree); + localEffect, fixes, triggeredFixes, triggeredErrors, methodDeclarationTree); }); injector.removeFixes(fixes); } diff --git a/core/src/main/java/edu/ucr/cs/riple/core/explorers/suppliers/AbstractSupplier.java b/core/src/main/java/edu/ucr/cs/riple/core/explorers/suppliers/AbstractSupplier.java new file mode 100644 index 000000000..0b74a13d0 --- /dev/null +++ b/core/src/main/java/edu/ucr/cs/riple/core/explorers/suppliers/AbstractSupplier.java @@ -0,0 +1,137 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nima Karimipour + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package edu.ucr.cs.riple.core.explorers.suppliers; + +import com.google.common.collect.ImmutableSet; +import edu.ucr.cs.riple.core.Config; +import edu.ucr.cs.riple.core.ModuleInfo; +import edu.ucr.cs.riple.core.injectors.AnnotationInjector; +import edu.ucr.cs.riple.core.metadata.field.FieldDeclarationAnalysis; +import edu.ucr.cs.riple.core.metadata.index.Bank; +import edu.ucr.cs.riple.core.metadata.index.Error; +import edu.ucr.cs.riple.core.metadata.index.Fix; +import edu.ucr.cs.riple.core.metadata.method.MethodDeclarationTree; + +/** Base class for all instances of {@link Supplier}. */ +public abstract class AbstractSupplier implements Supplier { + + /** Fix bank instance. */ + protected final Bank fixBank; + /** Error Bank instance. */ + protected final Bank errorBank; + /** Injector instance. */ + protected final AnnotationInjector injector; + /** Method declaration tree instance. */ + protected final MethodDeclarationTree tree; + /** Depth of analysis. */ + protected final int depth; + /** Field declaration analysis to detect fixes on inline multiple field declaration statements. */ + protected final FieldDeclarationAnalysis fieldDeclarationAnalysis; + /** Annotator config. */ + protected final Config config; + + public AbstractSupplier( + ImmutableSet modules, Config config, MethodDeclarationTree tree) { + this.config = config; + this.fieldDeclarationAnalysis = new FieldDeclarationAnalysis(modules); + this.tree = tree; + this.fixBank = initializeFixBank(modules); + this.errorBank = initializeErrorBank(modules); + this.injector = initializeInjector(); + this.depth = initializeDepth(); + } + + /** + * Initializer for injector. + * + * @return {@link AnnotationInjector} instance. + */ + protected abstract AnnotationInjector initializeInjector(); + + /** + * Initializer for depth. + * + * @return depth of analysis. + */ + protected abstract int initializeDepth(); + + /** + * Initializer for errorBank. + * + * @param modules Set of modules involved in the analysis. + * @return {@link Bank} of {@link Error} instances. + */ + protected Bank initializeErrorBank(ImmutableSet modules) { + return new Bank<>( + modules.stream() + .map(info -> info.dir.resolve("errors.tsv")) + .collect(ImmutableSet.toImmutableSet()), + Error::new); + } + + /** + * Initializer for fixBank. + * + * @param modules Set of modules involved in the analysis. + * @return {@link Bank} of {@link Fix} instances. + */ + protected Bank initializeFixBank(ImmutableSet modules) { + return new Bank<>( + modules.stream() + .map(info -> info.dir.resolve("fixes.tsv")) + .collect(ImmutableSet.toImmutableSet()), + Fix.factory(config, fieldDeclarationAnalysis)); + } + + @Override + public Bank getFixBank() { + return fixBank; + } + + @Override + public Bank getErrorBank() { + return errorBank; + } + + @Override + public AnnotationInjector getInjector() { + return injector; + } + + @Override + public MethodDeclarationTree getMethodDeclarationTree() { + return tree; + } + + @Override + public int depth() { + return depth; + } + + @Override + public Config getConfig() { + return config; + } +} diff --git a/core/src/main/java/edu/ucr/cs/riple/core/explorers/suppliers/DownstreamDependencySupplier.java b/core/src/main/java/edu/ucr/cs/riple/core/explorers/suppliers/DownstreamDependencySupplier.java new file mode 100644 index 000000000..098472f7c --- /dev/null +++ b/core/src/main/java/edu/ucr/cs/riple/core/explorers/suppliers/DownstreamDependencySupplier.java @@ -0,0 +1,56 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nima Karimipour + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package edu.ucr.cs.riple.core.explorers.suppliers; + +import edu.ucr.cs.riple.core.Config; +import edu.ucr.cs.riple.core.injectors.AnnotationInjector; +import edu.ucr.cs.riple.core.injectors.VirtualInjector; +import edu.ucr.cs.riple.core.metadata.method.MethodDeclarationTree; + +/** + * Supplier for downstream dependency analysis. It has the following characteristics: + * + *

    + *
  • Annotations are virtually injected on downstream dependencies. + *
  • Analysis is performed only to depth 1. + *
  • Global impact of annotations are neglected. + *
+ */ +public class DownstreamDependencySupplier extends AbstractSupplier { + + public DownstreamDependencySupplier(Config config, MethodDeclarationTree tree) { + super(config.downstreamInfo, config, tree); + } + + @Override + protected AnnotationInjector initializeInjector() { + return new VirtualInjector(config); + } + + @Override + protected int initializeDepth() { + return 1; + } +} diff --git a/core/src/main/java/edu/ucr/cs/riple/core/explorers/suppliers/ExhaustiveSupplier.java b/core/src/main/java/edu/ucr/cs/riple/core/explorers/suppliers/ExhaustiveSupplier.java new file mode 100644 index 000000000..317b0fa5f --- /dev/null +++ b/core/src/main/java/edu/ucr/cs/riple/core/explorers/suppliers/ExhaustiveSupplier.java @@ -0,0 +1,42 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nima Karimipour + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package edu.ucr.cs.riple.core.explorers.suppliers; + +import edu.ucr.cs.riple.core.Config; +import edu.ucr.cs.riple.core.metadata.method.MethodDeclarationTree; + +/** Supplier for exhaustive analysis. This will be mostly used in experiments. */ +public class ExhaustiveSupplier extends TargetModuleSupplier { + + /** + * Constructor for exhaustive supplier. + * + * @param config Annotator config instance. + * @param tree Method declaration tree instance for methods in target module. + */ + public ExhaustiveSupplier(Config config, MethodDeclarationTree tree) { + super(config, tree); + } +} diff --git a/core/src/main/java/edu/ucr/cs/riple/core/explorers/suppliers/Supplier.java b/core/src/main/java/edu/ucr/cs/riple/core/explorers/suppliers/Supplier.java new file mode 100644 index 000000000..5e8485d1a --- /dev/null +++ b/core/src/main/java/edu/ucr/cs/riple/core/explorers/suppliers/Supplier.java @@ -0,0 +1,78 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nima Karimipour + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package edu.ucr.cs.riple.core.explorers.suppliers; + +import edu.ucr.cs.riple.core.Config; +import edu.ucr.cs.riple.core.injectors.AnnotationInjector; +import edu.ucr.cs.riple.core.metadata.index.Bank; +import edu.ucr.cs.riple.core.metadata.index.Error; +import edu.ucr.cs.riple.core.metadata.index.Fix; +import edu.ucr.cs.riple.core.metadata.method.MethodDeclarationTree; + +/** Supplier for initializing an {@link edu.ucr.cs.riple.core.explorers.Explorer} instance. */ +public interface Supplier { + + /** + * Getter for {@link Bank} of {@link Fix} instances. + * + * @return Fix Bank instance. + */ + Bank getFixBank(); + + /** + * Getter for {@link Bank} of {@link Error} instance. + * + * @return Error Bank instance. + */ + Bank getErrorBank(); + + /** + * Getter for {@link AnnotationInjector} instance. + * + * @return Annotation Injector instance. + */ + AnnotationInjector getInjector(); + + /** + * Getter for {@link MethodDeclarationTree} instance. + * + * @return MethodDeclarationTree instance. + */ + MethodDeclarationTree getMethodDeclarationTree(); + + /** + * Getter for depth of analysis. + * + * @return depth. + */ + int depth(); + + /** + * Getter for {@link Config} instance. + * + * @return Config instance. + */ + Config getConfig(); +} diff --git a/core/src/main/java/edu/ucr/cs/riple/core/explorers/suppliers/TargetModuleSupplier.java b/core/src/main/java/edu/ucr/cs/riple/core/explorers/suppliers/TargetModuleSupplier.java new file mode 100644 index 000000000..5404c33a2 --- /dev/null +++ b/core/src/main/java/edu/ucr/cs/riple/core/explorers/suppliers/TargetModuleSupplier.java @@ -0,0 +1,63 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nima Karimipour + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package edu.ucr.cs.riple.core.explorers.suppliers; + +import com.google.common.collect.ImmutableSet; +import edu.ucr.cs.riple.core.Config; +import edu.ucr.cs.riple.core.injectors.AnnotationInjector; +import edu.ucr.cs.riple.core.injectors.PhysicalInjector; +import edu.ucr.cs.riple.core.metadata.method.MethodDeclarationTree; + +/** + * Supplier for target module analysis. It has the following characteristics: + * + *
    + *
  • Annotations are physically injected on target module. + *
  • Analysis is performed to depth set in config. + *
  • Depending on the config, global impact of annotations can be considered. + *
+ */ +public class TargetModuleSupplier extends AbstractSupplier { + + /** + * Constructor for target module supplier instance. + * + * @param config Annotator config instance. + * @param tree Method declaration tree for methods in target module. + */ + public TargetModuleSupplier(Config config, MethodDeclarationTree tree) { + super(ImmutableSet.of(config.target), config, tree); + } + + @Override + protected AnnotationInjector initializeInjector() { + return new PhysicalInjector(config); + } + + @Override + protected int initializeDepth() { + return config.depth; + } +} diff --git a/core/src/main/java/edu/ucr/cs/riple/core/global/DownstreamImpactExplorer.java b/core/src/main/java/edu/ucr/cs/riple/core/global/DownstreamImpactExplorer.java new file mode 100644 index 000000000..6a420cc11 --- /dev/null +++ b/core/src/main/java/edu/ucr/cs/riple/core/global/DownstreamImpactExplorer.java @@ -0,0 +1,113 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nima Karimipour + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package edu.ucr.cs.riple.core.global; + +import com.google.common.collect.ImmutableSet; +import edu.ucr.cs.riple.core.explorers.OptimizedExplorer; +import edu.ucr.cs.riple.core.explorers.suppliers.DownstreamDependencySupplier; +import edu.ucr.cs.riple.core.metadata.index.Fix; +import edu.ucr.cs.riple.core.metadata.trackers.RegionTracker; +import edu.ucr.cs.riple.core.util.Utility; +import edu.ucr.cs.riple.injector.location.OnMethod; +import edu.ucr.cs.riple.injector.location.OnParameter; +import java.util.Collections; +import java.util.HashMap; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Explorer for analyzing downstream dependencies. Used by {@link GlobalAnalyzerImpl} to compute the + * effects of changes in upstream on downstream dependencies. This explorer cannot be used to + * compute the effects in target module. + */ +class DownstreamImpactExplorer extends OptimizedExplorer { + + /** + * Map of public methods in target module to parameters in target module, which are source of + * nullable flow back to upstream module (target) from downstream dependencies, if annotated as + * {@code @Nullable}. + */ + private final HashMap> nullableFlowMap; + + public DownstreamImpactExplorer( + ImmutableSet fixes, DownstreamDependencySupplier supplier, RegionTracker tracker) { + super(fixes, supplier, new NoOpGlobalAnalyzer(), tracker); + this.nullableFlowMap = new HashMap<>(); + } + + @Override + public void rerunAnalysis() { + Utility.buildDownstreamDependencies(config); + } + + @Override + protected void finalizeReports() { + super.finalizeReports(); + // Collect impacted parameters in target module by downstream dependencies. + graph + .getNodes() + .forEach( + node -> + node.root.ifOnMethod( + method -> { + // Impacted parameters. + Set parameters = + node.triggeredErrors.stream() + .filter( + error -> + error.nonnullTarget != null + && error.nonnullTarget.isOnParameter() + // Method is declared in the target module. + && methodDeclarationTree.declaredInModule( + error.nonnullTarget)) + .map(error -> error.nonnullTarget.toParameter()) + .collect(Collectors.toSet()); + if (!parameters.isEmpty()) { + // Update uri for each parameter. These triggered fixes does not have an + // actual physical uri since they are provided as a jar file in downstream + // dependencies. + parameters.forEach( + onParameter -> + onParameter.uri = + methodDeclarationTree.findNode( + onParameter.method, onParameter.clazz) + .location + .uri); + nullableFlowMap.put(method, parameters); + } + })); + } + + /** + * Returns set of parameters that will receive {@code @Nullable} if the passed method is annotated + * as {@code @Nullable}. + * + * @param method Method to be annotated. + * @return Set of impacted parameters. If no parameter is impacted, empty set will be returned. + */ + public Set getImpactedParameters(OnMethod method) { + return nullableFlowMap.getOrDefault(method, Collections.emptySet()); + } +} diff --git a/core/src/main/java/edu/ucr/cs/riple/core/global/GlobalAnalyzer.java b/core/src/main/java/edu/ucr/cs/riple/core/global/GlobalAnalyzer.java new file mode 100644 index 000000000..9c4ddc242 --- /dev/null +++ b/core/src/main/java/edu/ucr/cs/riple/core/global/GlobalAnalyzer.java @@ -0,0 +1,91 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nima Karimipour + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package edu.ucr.cs.riple.core.global; + +import com.google.common.collect.ImmutableSet; +import edu.ucr.cs.riple.core.metadata.index.Error; +import edu.ucr.cs.riple.core.metadata.index.Fix; +import edu.ucr.cs.riple.injector.location.OnParameter; +import java.util.List; +import java.util.Set; + +/** + * Analyzer for downstream dependencies. + * + *

This class analyzes switching the nullability of public APIs of one compilation target, in + * order to compute the effect on said target's downstream dependencies of adding annotations to + * those APIs. It does so by building said downstream dependencies. It collects the effect (number + * of additional errors) of each such change, by summing across all downstream dependencies. This + * data then be fed to the Annotator main process in the decision process. + */ +public interface GlobalAnalyzer { + + /** Analyzes effects of changes in public methods in downstream dependencies. */ + void analyzeDownstreamDependencies(); + + /** + * Returns the lower bound of number of errors of applying a fix and its associated chain of fixes + * on the target on downstream dependencies. + * + * @param tree Tree of the fix tree associated to root. + * @return Lower bound of number of errors on downstream dependencies. + */ + int computeLowerBoundOfNumberOfErrors(Set tree); + + /** + * Returns the upper bound of number of errors of applying a fix and its associated chain of fixes + * on the target on downstream dependencies. + * + * @param tree Tree of the fix tree associated to root. + * @return Upper bound of number of errors on downstream dependencies. + */ + int computeUpperBoundOfNumberOfErrors(Set tree); + + /** + * Returns set of parameters that will receive {@code @Nullable}, if any of the methods in the + * fixTree are annotated as {@code @Nullable}. + * + * @param fixTree Fix tree. + * @return Immutable set of impacted parameters. + */ + ImmutableSet getImpactedParameters(Set fixTree); + + /** + * Collects list of triggered errors in downstream dependencies if fix is applied in the target + * module. It also includes triggered errors that can be resolved via an annotation in target + * (upstream) module. + * + * @param fix Fix instance to be applied in the target module. + * @return List of triggered errors. + */ + List getTriggeredErrors(Fix fix); + + /** + * Updates state of methods after injection of fixes in target module. + * + * @param fixes Set of injected fixes. + */ + void updateImpactsAfterInjection(Set fixes); +} diff --git a/core/src/main/java/edu/ucr/cs/riple/core/global/GlobalAnalyzerImpl.java b/core/src/main/java/edu/ucr/cs/riple/core/global/GlobalAnalyzerImpl.java new file mode 100644 index 000000000..1c7826a59 --- /dev/null +++ b/core/src/main/java/edu/ucr/cs/riple/core/global/GlobalAnalyzerImpl.java @@ -0,0 +1,215 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nima Karimipour + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package edu.ucr.cs.riple.core.global; + +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimaps; +import edu.ucr.cs.riple.core.Config; +import edu.ucr.cs.riple.core.ModuleInfo; +import edu.ucr.cs.riple.core.Report; +import edu.ucr.cs.riple.core.explorers.suppliers.DownstreamDependencySupplier; +import edu.ucr.cs.riple.core.metadata.index.Error; +import edu.ucr.cs.riple.core.metadata.index.Fix; +import edu.ucr.cs.riple.core.metadata.method.MethodDeclarationTree; +import edu.ucr.cs.riple.core.metadata.method.MethodNode; +import edu.ucr.cs.riple.core.metadata.trackers.MethodRegionTracker; +import edu.ucr.cs.riple.core.util.Utility; +import edu.ucr.cs.riple.injector.changes.AddAnnotation; +import edu.ucr.cs.riple.injector.location.Location; +import edu.ucr.cs.riple.injector.location.OnMethod; +import edu.ucr.cs.riple.injector.location.OnParameter; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nullable; + +/** Implementation for {@link GlobalAnalyzer} interface. */ +public class GlobalAnalyzerImpl implements GlobalAnalyzer { + + /** Set of downstream dependencies. */ + private final ImmutableSet downstreamModules; + /** Public APIs in the target modules that have a non-primitive return value. */ + private final ImmutableMultimap methods; + /** Annotator Config. */ + private final Config config; + /** Method declaration tree instance. */ + private final MethodDeclarationTree tree; + + public GlobalAnalyzerImpl(Config config, MethodDeclarationTree tree) { + this.config = config; + this.downstreamModules = config.downstreamInfo; + this.tree = tree; + this.methods = + Multimaps.index( + tree.getPublicMethodsWithNonPrimitivesReturn().stream() + .map(MethodImpact::new) + .collect(ImmutableSet.toImmutableSet()), + MethodImpact::hashCode); + } + + @Override + public void analyzeDownstreamDependencies() { + System.out.println("Analyzing downstream dependencies..."); + Utility.setScannerCheckerActivation(downstreamModules, true); + Utility.buildDownstreamDependencies(config); + Utility.setScannerCheckerActivation(downstreamModules, false); + // Collect callers of public APIs in module. + MethodRegionTracker tracker = new MethodRegionTracker(config.downstreamInfo, tree); + // Generate fixes corresponding methods. + ImmutableSet fixes = + methods.values().stream() + .filter( + input -> + !tracker + .getCallersOfMethod(input.node.location.clazz, input.node.location.method) + .isEmpty()) // skip methods that are not called anywhere. + .map( + methodImpact -> + new Fix( + new AddAnnotation( + new OnMethod( + "null", + methodImpact.node.location.clazz, + methodImpact.node.location.method), + config.nullableAnnot), + "null", + "null", + "null", + false)) + .collect(ImmutableSet.toImmutableSet()); + DownstreamImpactExplorer analyzer = + new DownstreamImpactExplorer( + fixes, new DownstreamDependencySupplier(config, tree), tracker); + ImmutableSet reports = analyzer.explore(); + // Update method status based on the results. + methods + .values() + .forEach( + method -> { + MethodNode node = method.node; + Set impactedParameters = analyzer.getImpactedParameters(node.location); + reports.stream() + .filter(input -> input.root.toMethod().equals(node.location)) + .findAny() + .ifPresent(report -> method.setStatus(report, impactedParameters)); + }); + System.out.println("Analyzing downstream dependencies completed!"); + } + + /** + * Retrieves the corresponding {@link MethodImpact} to a fix. + * + * @param fix Target fix. + * @return Corresponding {@link MethodImpact}, null if not located. + */ + @Nullable + private MethodImpact fetchMethodImpactForFix(Fix fix) { + if (!fix.isOnMethod()) { + return null; + } + OnMethod onMethod = fix.toMethod(); + int predictedHash = MethodImpact.hash(onMethod.method, onMethod.clazz); + Optional optional = + this.methods.get(predictedHash).stream() + .filter(m -> m.node.location.equals(onMethod)) + .findAny(); + return optional.orElse(null); + } + + /** + * Returns the effect of applying a fix on the target on downstream dependencies. + * + * @param fix Fix targeting an element in target. + * @param fixesLocation Location in target that will be annotated as {@code @Nullable}. + * @return Effect on downstream dependencies. + */ + private int effectOnDownstreamDependencies(Fix fix, Set fixesLocation) { + MethodImpact methodImpact = fetchMethodImpactForFix(fix); + if (methodImpact == null) { + return 0; + } + int individualEffect = methodImpact.getEffect(); + // Some triggered errors might be resolved due to fixes in the tree, and we should not double + // count them. + List triggeredErrors = methodImpact.getTriggeredErrors(); + long resolvedErrors = + triggeredErrors.stream() + .filter(error -> fixesLocation.contains(error.nonnullTarget)) + .count(); + return individualEffect - (int) resolvedErrors; + } + + @Override + public int computeLowerBoundOfNumberOfErrors(Set tree) { + Set fixLocations = tree.stream().map(Fix::toLocation).collect(Collectors.toSet()); + OptionalInt lowerBoundEffectOfChainOptional = + tree.stream().mapToInt(fix -> effectOnDownstreamDependencies(fix, fixLocations)).max(); + if (lowerBoundEffectOfChainOptional.isEmpty()) { + return 0; + } + return lowerBoundEffectOfChainOptional.getAsInt(); + } + + @Override + public int computeUpperBoundOfNumberOfErrors(Set tree) { + Set fixesLocation = tree.stream().map(Fix::toLocation).collect(Collectors.toSet()); + return tree.stream().mapToInt(fix -> effectOnDownstreamDependencies(fix, fixesLocation)).sum(); + } + + @Override + public ImmutableSet getImpactedParameters(Set fixTree) { + return fixTree.stream() + .filter(Fix::isOnMethod) + .flatMap( + fix -> { + MethodImpact impact = fetchMethodImpactForFix(fix); + return impact == null ? Stream.of() : impact.getImpactedParameters().stream(); + }) + .collect(ImmutableSet.toImmutableSet()); + } + + @Override + public List getTriggeredErrors(Fix fix) { + // We currently only store impact of methods on downstream dependencies. + if (!fix.isOnMethod()) { + return Collections.emptyList(); + } + MethodImpact impact = fetchMethodImpactForFix(fix); + if (impact == null) { + return Collections.emptyList(); + } + return impact.getTriggeredErrors(); + } + + @Override + public void updateImpactsAfterInjection(Set fixes) { + this.methods.values().forEach(methodImpact -> methodImpact.updateStatus(fixes)); + } +} diff --git a/core/src/main/java/edu/ucr/cs/riple/core/global/MethodImpact.java b/core/src/main/java/edu/ucr/cs/riple/core/global/MethodImpact.java new file mode 100644 index 000000000..57a80ea35 --- /dev/null +++ b/core/src/main/java/edu/ucr/cs/riple/core/global/MethodImpact.java @@ -0,0 +1,161 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nima Karimipour + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package edu.ucr.cs.riple.core.global; + +import edu.ucr.cs.riple.core.Report; +import edu.ucr.cs.riple.core.metadata.index.Error; +import edu.ucr.cs.riple.core.metadata.index.Fix; +import edu.ucr.cs.riple.core.metadata.method.MethodDeclarationTree; +import edu.ucr.cs.riple.core.metadata.method.MethodNode; +import edu.ucr.cs.riple.injector.location.OnParameter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** Container class for storing overall effect of each method. */ +public class MethodImpact { + /** Node in {@link MethodDeclarationTree} corresponding to a public method. */ + final MethodNode node; + /** + * Map of parameters in target module that will receive {@code Nullable} value if targeted method + * in node is annotated as {@code @Nullable} with their corresponding triggered errors. + */ + private final HashMap> impactedParametersMap; + /** + * Set of triggered errors in downstream dependencies if target method in node is annotated as + * {@code @Nullable}. + */ + private List triggeredErrors; + /** + * Effect of injecting a {@code Nullable} annotation on pointing method of node on downstream + * dependencies. + */ + private int effect; + + public MethodImpact(MethodNode node) { + this.node = node; + this.effect = 0; + this.impactedParametersMap = new HashMap<>(); + } + + @Override + public int hashCode() { + return hash(node.location.method, node.location.clazz); + } + + /** + * Calculates hash. This method is used outside this class to calculate the expected hash based on + * instance's properties value if the actual instance is not available. + * + * @param method Method signature. + * @param clazz Fully qualified name of the containing class. + * @return Expected hash. + */ + public static int hash(String method, String clazz) { + return MethodNode.hash(method, clazz); + } + + /** + * Updates the status of methods impact on downstream dependencies. + * + * @param report Result of applying making method in node {@code @Nullable} in downstream + * dependencies. + * @param impactedParameters Set of impacted paramaters. + */ + public void setStatus(Report report, Set impactedParameters) { + this.effect = report.localEffect; + this.triggeredErrors = new ArrayList<>(report.triggeredErrors); + // Count the number of times each parameter received a @Nullable. + impactedParameters.forEach( + onParameter -> { + List triggered = + triggeredErrors.stream() + .filter( + error -> + error.nonnullTarget != null && error.nonnullTarget.equals(onParameter)) + .collect(Collectors.toList()); + impactedParametersMap.put(onParameter, triggered); + }); + } + + /** + * Getter for effect. + * + * @return Effect. + */ + public int getEffect() { + return effect; + } + + /** + * Returns list of triggered errors if method is {@code @Nullable} on downstream dependencies. + * + * @return List of errors. + */ + public List getTriggeredErrors() { + return triggeredErrors; + } + + /** + * Returns set of parameters on target module that will receive {@code @Nullable} if method in + * node is annotated as {@code @Nullable}. + * + * @return Set of parameters location. + */ + public Set getImpactedParameters() { + return impactedParametersMap.keySet(); + } + + /** + * Updates the status of method's impact after injection of fixes in target module. Potentially + * part of stored impact result is invalid due to injection of fixes. (e.g. some impacted + * parameters may already be annotated as {@code @Nullable} and will no longer trigger errors on + * downstream dependencies). This method addresses this issue by updating method's status. + * + * @param fixes List of injected fixes. + */ + public void updateStatus(Set fixes) { + Set annotatedParameters = new HashSet<>(); + fixes.forEach( + fix -> + fix.ifOnParameter( + onParameter -> { + if (impactedParametersMap.containsKey(onParameter)) { + List errors = impactedParametersMap.get(onParameter); + effect -= errors.size(); + triggeredErrors.removeAll(errors); + annotatedParameters.add(onParameter); + } + })); + if (effect < 0) { + // This is impossible, however for safety issues, we set it to zero. + effect = 0; + } + annotatedParameters.forEach(impactedParametersMap::remove); + } +} diff --git a/core/src/main/java/edu/ucr/cs/riple/core/global/NoOpGlobalAnalyzer.java b/core/src/main/java/edu/ucr/cs/riple/core/global/NoOpGlobalAnalyzer.java new file mode 100644 index 000000000..5c60940a3 --- /dev/null +++ b/core/src/main/java/edu/ucr/cs/riple/core/global/NoOpGlobalAnalyzer.java @@ -0,0 +1,70 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nima Karimipour + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package edu.ucr.cs.riple.core.global; + +import com.google.common.collect.ImmutableSet; +import edu.ucr.cs.riple.core.metadata.index.Error; +import edu.ucr.cs.riple.core.metadata.index.Fix; +import edu.ucr.cs.riple.injector.location.OnParameter; +import java.util.List; +import java.util.Set; + +/** + * This global analyzer does not have any information regarding the impact of changes in target + * module in dependencies, the main purpose of this class is to avoid initializing GlobalAnalyzer + * instances to {@code null} when impact on dependencies is not considered. + */ +public class NoOpGlobalAnalyzer implements GlobalAnalyzer { + + @Override + public void analyzeDownstreamDependencies() { + // No operation needed. + } + + @Override + public int computeLowerBoundOfNumberOfErrors(Set tree) { + return 0; + } + + @Override + public int computeUpperBoundOfNumberOfErrors(Set tree) { + return 0; + } + + @Override + public ImmutableSet getImpactedParameters(Set fixTree) { + return ImmutableSet.of(); + } + + @Override + public List getTriggeredErrors(Fix fix) { + return List.of(); + } + + @Override + public void updateImpactsAfterInjection(Set fixes) { + // No operation needed. + } +} diff --git a/core/src/main/java/edu/ucr/cs/riple/core/metadata/graph/Node.java b/core/src/main/java/edu/ucr/cs/riple/core/metadata/graph/Node.java index 3533bcc5d..47de6ad04 100644 --- a/core/src/main/java/edu/ucr/cs/riple/core/metadata/graph/Node.java +++ b/core/src/main/java/edu/ucr/cs/riple/core/metadata/graph/Node.java @@ -24,7 +24,7 @@ package edu.ucr.cs.riple.core.metadata.graph; -import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import edu.ucr.cs.riple.core.Report; import edu.ucr.cs.riple.core.metadata.index.Bank; @@ -59,7 +59,7 @@ public class Node { public Set triggeredFixes; /** Collection of triggered errors if tree is applied. */ - public ImmutableSet triggeredErrors; + public ImmutableList triggeredErrors; /** Unique id of Node across all nodes. */ public int id; @@ -83,7 +83,7 @@ public Node(Fix root) { this.regions = new HashSet<>(); this.root = root; this.triggeredFixes = new HashSet<>(); - this.triggeredErrors = ImmutableSet.of(); + this.triggeredErrors = ImmutableList.of(); this.effect = 0; this.tree = Sets.newHashSet(root); this.changed = false; @@ -137,14 +137,14 @@ public boolean hasConflictInRegions(Node other) { * Updates node status. Should be called when all annotations in tree are applied to the source * code and the target project has been rebuilt. * - * @param effect Local effect calculated based on the number of errors in impacted regions. + * @param localEffect Local effect calculated based on the number of errors in impacted regions. * @param fixesInOneRound All fixes applied simultaneously to the source code. * @param triggeredFixes Triggered fixes collected from impacted regions. * @param triggeredErrors Triggered Errors collected from impacted regions. * @param mdt Method declaration tree instance. */ public void updateStatus( - int effect, + int localEffect, Set fixesInOneRound, Collection triggeredFixes, Collection triggeredErrors, @@ -152,7 +152,7 @@ public void updateStatus( // Update list of triggered fixes. this.updateTriggered(triggeredFixes); // Update list of triggered errors. - this.triggeredErrors = ImmutableSet.copyOf(triggeredErrors); + this.triggeredErrors = ImmutableList.copyOf(triggeredErrors); // A fix in a tree, can have a super method that is not part of this node's tree but be present // in another node's tree. In this case since both are applied, an error due to inheritance // violation will not be reported. This calculation below will fix that. @@ -185,7 +185,7 @@ public void updateStatus( } }); // Fix the actual error below. - this.effect = effect + numberOfSuperMethodsAnnotatedOutsideTree[0]; + this.effect = localEffect + numberOfSuperMethodsAnnotatedOutsideTree[0]; } /** @@ -203,6 +203,7 @@ public void updateTriggered(Collection fixes) { /** Merges triggered fixes to the tree, to prepare the analysis for the next depth. */ public void mergeTriggered() { this.tree.addAll(this.triggeredFixes); + this.tree.forEach(fix -> fix.fixSourceIsInTarget = true); this.triggeredFixes.clear(); } diff --git a/core/src/main/java/edu/ucr/cs/riple/core/metadata/index/Fix.java b/core/src/main/java/edu/ucr/cs/riple/core/metadata/index/Fix.java index a2ffe4737..52788a13f 100644 --- a/core/src/main/java/edu/ucr/cs/riple/core/metadata/index/Fix.java +++ b/core/src/main/java/edu/ucr/cs/riple/core/metadata/index/Fix.java @@ -52,10 +52,22 @@ public class Fix extends Enclosed { /** Reasons this fix is suggested by NullAway in string. */ public final Set reasons; - public Fix(AddAnnotation change, String reason, String encClass, String encMethod) { + /** + * If true, the fix is suggested due to an error in the target module, false if the fix is + * suggested due to error in downstream dependencies. + */ + public boolean fixSourceIsInTarget; + + public Fix( + AddAnnotation change, + String reason, + String encClass, + String encMethod, + boolean fixSourceIsInTarget) { super(encClass, encMethod); this.change = change; this.reasons = reason != null ? Sets.newHashSet(reason) : new HashSet<>(); + this.fixSourceIsInTarget = fixSourceIsInTarget; } /** @@ -80,10 +92,20 @@ public static Factory factory(Config config, FieldDeclarationAnalysis analy }); } Preconditions.checkArgument(info[7].equals("nullable"), "unsupported annotation: " + info[7]); - return new Fix(new AddAnnotation(location, config.nullableAnnot), info[6], info[8], info[9]); + return new Fix( + new AddAnnotation(location, config.nullableAnnot), info[6], info[8], info[9], true); }; } + /** + * Returns the targeted location information. + * + * @return Location information. + */ + public Location toLocation() { + return change.location; + } + /** * Checks if fix is targeting a method. * diff --git a/core/src/main/java/edu/ucr/cs/riple/core/metadata/method/MethodDeclarationTree.java b/core/src/main/java/edu/ucr/cs/riple/core/metadata/method/MethodDeclarationTree.java index e56fc768e..1a8d18fd2 100644 --- a/core/src/main/java/edu/ucr/cs/riple/core/metadata/method/MethodDeclarationTree.java +++ b/core/src/main/java/edu/ucr/cs/riple/core/metadata/method/MethodDeclarationTree.java @@ -26,6 +26,7 @@ import com.google.common.collect.ImmutableSet; import edu.ucr.cs.riple.core.metadata.MetaData; +import edu.ucr.cs.riple.injector.location.Location; import edu.ucr.cs.riple.injector.location.OnMethod; import java.nio.file.Path; import java.util.HashMap; @@ -42,16 +43,20 @@ public class MethodDeclarationTree extends MetaData { /** Each method has a unique id across all methods. This hashmap, maps ids to nodes. */ private HashMap nodes; + /** Set of all classes flat name declared in module. */ + private HashSet classNames; + public MethodDeclarationTree(Path path) { super(path); - // The root node of this tree with id: -1. - nodes.put(-1, MethodNode.TOP); } @Override protected void setup() { super.setup(); - nodes = new HashMap<>(); + this.classNames = new HashSet<>(); + this.nodes = new HashMap<>(); + // The root node of this tree with id: -1. + nodes.put(-1, MethodNode.TOP); } @Override @@ -86,6 +91,8 @@ protected MethodNode addNodeByLine(String[] values) { // Parent is already visited. parent.addChild(id); } + // Update list of all declared classes. + this.classNames.add(node.location.clazz); return node; } @@ -164,19 +171,21 @@ public MethodNode findNode(String method, String clazz) { * @return ImmutableSet of method nodes. */ public ImmutableSet getPublicMethodsWithNonPrimitivesReturn() { - return findNodes( - node -> - node.hasNonPrimitiveReturn && node.visibility.equals(MethodNode.Visibility.PUBLIC)) + return findNodes(MethodNode::isPublicMethodWithNonPrimitiveReturnType) .collect(ImmutableSet.toImmutableSet()); } /** - * Checks if the passed method, is declared in the target module. + * Checks if the passed location is targeting an element declared in the target module. * - * @param location Location of the method. - * @return true, if tree contains the method and false otherwise. + * @param location Location of the element. + * @return true, if the passed location is nonnull and is targeting an element in the target + * module, and false otherwise. */ - public boolean declaredInModule(OnMethod location) { - return findNode(location.method, location.clazz) != null; + public boolean declaredInModule(@Nullable Location location) { + if (location == null || location.clazz.equals("null")) { + return false; + } + return this.classNames.contains(location.clazz); } } diff --git a/core/src/main/java/edu/ucr/cs/riple/core/metadata/method/MethodNode.java b/core/src/main/java/edu/ucr/cs/riple/core/metadata/method/MethodNode.java index fd7dac2d3..02e89bd70 100644 --- a/core/src/main/java/edu/ucr/cs/riple/core/metadata/method/MethodNode.java +++ b/core/src/main/java/edu/ucr/cs/riple/core/metadata/method/MethodNode.java @@ -147,6 +147,15 @@ public boolean isNonTop() { return !Objects.equals(this.id, TOP.id); } + /** + * Checks if method is public with non-primitive return type. + * + * @return true if method is public with non-primitive return type and false otherwise. + */ + public boolean isPublicMethodWithNonPrimitiveReturnType() { + return hasNonPrimitiveReturn && visibility.equals(MethodNode.Visibility.PUBLIC); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/core/src/test/java/edu/ucr/cs/riple/core/ArgumentNullableFlowTest.java b/core/src/test/java/edu/ucr/cs/riple/core/ArgumentNullableFlowTest.java index a82e6f2b9..fda01face 100644 --- a/core/src/test/java/edu/ucr/cs/riple/core/ArgumentNullableFlowTest.java +++ b/core/src/test/java/edu/ucr/cs/riple/core/ArgumentNullableFlowTest.java @@ -44,27 +44,41 @@ public void nullableFlowDetectionEnabledTest() { coreTestHelper .addExpectedReports( // Change reduces errors on target by -5, but increases them in downstream dependency - // DepA by 2, DepB by 0 and DepC by 1. Hence, the total effect is: -2. - new TReport(new OnMethod("Foo.java", "test.target.Foo", "returnNullable(int)"), -2), - // Report below should appear due to flow of Nullable back to target by downstream - // dependencies. The triggered error is resolvable by making the method @Nullable, - // therefore, the overall effect is 0. + // DepA by 1 (Resolvable), DepB by 0 and DepC by 2 (1 resolvable). + // Parameters param1 and param2 in bar1 and bar2 will receive Nullable from downstream + // dependencies, + // param1 will be @Nullable with no triggered errors, param2 will trigger one error. + // Hence, the overall effect is: -5 + (from downstream dependency) 1 + (in target) 1 = + // -3. new TReport( - new OnParameter( - "Foo.java", "test.target.Foo", "bar1(java.lang.Object,java.lang.Object)", 0), - 0, + new OnMethod("Foo.java", "test.target.Foo", "returnNullable(int)"), + -3, newHashSet( + new OnParameter( + "Foo.java", + "test.target.Foo", + "bar1(java.lang.Object,java.lang.Object)", + 0), + new OnParameter( + "Foo.java", + "test.target.Foo", + "bar2(java.lang.Object,java.lang.Object)", + 1), new OnMethod( "Foo.java", "test.target.Foo", "bar1(java.lang.Object,java.lang.Object)")), Collections.emptySet()), - // Report below should appear due to flow of Nullable back to target by downstream - // dependencies. The triggered error is not resolvable, therefore, the overall effect 1. + // Change creates two errors on downstream dependencies (1 resolvable) and resolves one + // error locally, therefore the overall effect is 0. new TReport( - new OnParameter( - "Foo.java", "test.target.Foo", "bar2(java.lang.Object,java.lang.Object)", 1), - 1)) + new OnMethod("Foo.java", "test.target.Foo", "getNull()"), + 0, + newHashSet( + new OnParameter( + "Foo.java", "test.target.Foo", "takeNull(java.lang.Object)", 0)), + Collections.emptySet())) + .setPredicate((expected, found) -> expected.testEquals(coreTestHelper.getConfig(), found)) .toDepth(5) - .requestCompleteLoop() + .disableBailOut() .enableDownstreamDependencyAnalysis() .start(); } @@ -73,15 +87,11 @@ public void nullableFlowDetectionEnabledTest() { public void nullableFlowDetectionDisabledTest() { coreTestHelper .addExpectedReports( - // Change reduces errors on target by -5 - new TReport(new OnMethod("Foo.java", "test.target.Foo", "returnNullable(int)"), -5)) - .setPredicate( - (expected, found) -> - expected.root.equals(found.root) - && expected.getOverallEffect(coreTestHelper.getConfig()) - == found.getOverallEffect(coreTestHelper.getConfig())) + new TReport(new OnMethod("Foo.java", "test.target.Foo", "returnNullable(int)"), -5), + new TReport(new OnMethod("Foo.java", "test.target.Foo", "getNull()"), -1)) + .setPredicate((expected, found) -> expected.testEquals(coreTestHelper.getConfig(), found)) .toDepth(5) - .requestCompleteLoop() + .disableBailOut() .start(); } } diff --git a/core/src/test/java/edu/ucr/cs/riple/core/BaseCoreTest.java b/core/src/test/java/edu/ucr/cs/riple/core/BaseCoreTest.java index c86957889..03e19c34a 100644 --- a/core/src/test/java/edu/ucr/cs/riple/core/BaseCoreTest.java +++ b/core/src/test/java/edu/ucr/cs/riple/core/BaseCoreTest.java @@ -61,7 +61,11 @@ public void setup() { Path templates = Paths.get("templates"); Path pathToUnitTestDir = Utility.getPathOfResource(templates.resolve(projectTemplate).toString()); + Path repositoryDirectory = Paths.get(System.getProperty("user.dir")).getParent(); try { + // Create a separate library models loader to avoid races between unit tests. + FileUtils.copyDirectory( + repositoryDirectory.toFile(), outDirPath.resolve("Annotator").toFile()); FileUtils.deleteDirectory(projectPath.toFile()); FileUtils.copyDirectory(pathToUnitTestDir.toFile(), projectPath.toFile()); ProcessBuilder processBuilder = Utility.createProcessInstance(); @@ -74,7 +78,7 @@ public void setup() { commands.addAll(Utility.computeConfigPathsWithGradleArguments(outDirPath, modules)); commands.add( "-Plibrary-model-loader-path=" - + Utility.getPathToLibraryModel().resolve("build").resolve("libs")); + + Utility.getPathToLibraryModel(outDirPath).resolve("build").resolve("libs")); processBuilder.command(commands); int success = processBuilder.start().waitFor(); if (success != 0) { diff --git a/core/src/test/java/edu/ucr/cs/riple/core/tools/CoreTestHelper.java b/core/src/test/java/edu/ucr/cs/riple/core/tools/CoreTestHelper.java index 4e0aa90a2..0d094db0c 100644 --- a/core/src/test/java/edu/ucr/cs/riple/core/tools/CoreTestHelper.java +++ b/core/src/test/java/edu/ucr/cs/riple/core/tools/CoreTestHelper.java @@ -146,7 +146,7 @@ public void start() { expected.root.change.location.equals(found.root.change.location) && expected.getExpectedValue() == found.getOverallEffect(config); } - compare(annotator.reports.values()); + compare(annotator.cachedReports.values()); } /** Checks if all src inputs are subpackages of test package. */ @@ -225,7 +225,7 @@ private void makeAnnotatorConfigFile(Path configPath) { this.projectPath, this.outDirPath, modules); builder.downstreamBuildCommand = builder.buildCommand; builder.nullawayLibraryModelLoaderPath = - Utility.getPathToLibraryModel() + Utility.getPathToLibraryModel(outDirPath) .resolve( Paths.get( "src", diff --git a/core/src/test/java/edu/ucr/cs/riple/core/tools/TFix.java b/core/src/test/java/edu/ucr/cs/riple/core/tools/TFix.java index af1b318f9..4a0ddcfa6 100644 --- a/core/src/test/java/edu/ucr/cs/riple/core/tools/TFix.java +++ b/core/src/test/java/edu/ucr/cs/riple/core/tools/TFix.java @@ -32,6 +32,6 @@ public class TFix extends Fix { public TFix(Location location) { - super(new DefaultAnnotation(location), null, null, null); + super(new DefaultAnnotation(location), null, null, null, true); } } diff --git a/core/src/test/java/edu/ucr/cs/riple/core/tools/TReport.java b/core/src/test/java/edu/ucr/cs/riple/core/tools/TReport.java index 34357e12c..8b13b5255 100644 --- a/core/src/test/java/edu/ucr/cs/riple/core/tools/TReport.java +++ b/core/src/test/java/edu/ucr/cs/riple/core/tools/TReport.java @@ -24,6 +24,8 @@ package edu.ucr.cs.riple.core.tools; +import com.google.common.collect.ImmutableSet; +import edu.ucr.cs.riple.core.Config; import edu.ucr.cs.riple.core.Report; import edu.ucr.cs.riple.core.metadata.index.Fix; import edu.ucr.cs.riple.injector.changes.AddAnnotation; @@ -44,7 +46,8 @@ public TReport(Location root, int effect) { public TReport(Location root, int effect, String encClass, String encMethod) { super( - new Fix(new AddAnnotation(root, "javax.annotation.Nullable"), null, encClass, encMethod), + new Fix( + new AddAnnotation(root, "javax.annotation.Nullable"), null, encClass, encMethod, true), effect); this.expectedValue = effect; } @@ -56,8 +59,10 @@ public TReport( @Nullable Set triggered) { this(root, effect); if (triggered != null) { - this.triggered = - triggered.stream().map((Function) TFix::new).collect(Collectors.toSet()); + this.triggeredFixes = + triggered.stream() + .map((Function) TFix::new) + .collect(ImmutableSet.toImmutableSet()); } if (addToTree != null) { this.tree.addAll( @@ -73,4 +78,9 @@ public TReport( public int getExpectedValue() { return this.expectedValue; } + + @Override + public int getOverallEffect(Config config) { + return expectedValue; + } } diff --git a/core/src/test/java/edu/ucr/cs/riple/core/tools/Utility.java b/core/src/test/java/edu/ucr/cs/riple/core/tools/Utility.java index 928e631f6..afe549687 100644 --- a/core/src/test/java/edu/ucr/cs/riple/core/tools/Utility.java +++ b/core/src/test/java/edu/ucr/cs/riple/core/tools/Utility.java @@ -92,7 +92,7 @@ public static String computeBuildCommand( "%s && ./gradlew compileJava %s -Plibrary-model-loader-path=%s --rerun-tasks", Utility.changeDirCommand(projectPath), String.join(" ", computeConfigPathsWithGradleArguments(outDirPath, modules)), - getPathToLibraryModel().resolve(Paths.get("build", "libs", "librarymodel.jar"))); + getPathToLibraryModel(outDirPath).resolve(Paths.get("build", "libs", "librarymodel.jar"))); } /** @@ -111,15 +111,20 @@ public static String computeBuildCommandWithLibraryModelLoaderDependency( Path projectPath, Path outDirPath, List modules) { return String.format( "%s && ./gradlew library-model-loader:jar --rerun-tasks && %s && ./gradlew compileJava %s -Plibrary-model-loader-path=%s --rerun-tasks", - Utility.changeDirCommand(Paths.get(System.getProperty("user.dir")).getParent()), + Utility.changeDirCommand(outDirPath.resolve("Annotator")), Utility.changeDirCommand(projectPath), String.join(" ", computeConfigPathsWithGradleArguments(outDirPath, modules)), - getPathToLibraryModel().resolve(Paths.get("build", "libs", "librarymodel.jar"))); + getPathToLibraryModel(outDirPath).resolve(Paths.get("build", "libs", "librarymodel.jar"))); } - public static Path getPathToLibraryModel() { - return Paths.get(System.getProperty("user.dir")) - .getParent() - .resolve(Paths.get("library-model-loader")); + /** + * Computes the path to NullAway library models loader. Library models loader resides in the unit + * test temporary directory. + * + * @param unittestDir path to Unit test. + * @return Path to nullaway library model loader. + */ + public static Path getPathToLibraryModel(Path unittestDir) { + return unittestDir.resolve("Annotator").resolve(Paths.get("library-model-loader")); } } diff --git a/core/src/test/resources/templates/nullable-flow-test/DepA/src/main/java/test/depa/DepA.java b/core/src/test/resources/templates/nullable-flow-test/DepA/src/main/java/test/depa/DepA.java index d8a856ac0..602b83d40 100644 --- a/core/src/test/resources/templates/nullable-flow-test/DepA/src/main/java/test/depa/DepA.java +++ b/core/src/test/resources/templates/nullable-flow-test/DepA/src/main/java/test/depa/DepA.java @@ -42,4 +42,10 @@ public void exec() { // pass ans back to Target module. foo.bar1(ans, new Object()); } + + public Object run() { + Object o = Foo.getNull(); + Foo.takeNull(o); + return o; + } } diff --git a/core/src/test/resources/templates/nullable-flow-test/Target/src/main/java/test/target/Foo.java b/core/src/test/resources/templates/nullable-flow-test/Target/src/main/java/test/target/Foo.java index da1f63388..231112131 100644 --- a/core/src/test/resources/templates/nullable-flow-test/Target/src/main/java/test/target/Foo.java +++ b/core/src/test/resources/templates/nullable-flow-test/Target/src/main/java/test/target/Foo.java @@ -53,4 +53,12 @@ public void bar2(Object param1, Object param2) { // If param2 is @Nullable, error below is unresolvable. Object hash = param2.hashCode(); } + + public static Object getNull() { + return null; + } + + public static void takeNull(Object o) { + /* No-Op */ + } }