Summary: When someone runs --changed-only mode, there is a risk of corrupting the results for future analyses. The problem is that changed-only mode does not analyze the callers of changed procedures. If a subsequent analysis relies on the specs of one of these callers, they will be stale and may give the wrong results. To be concrete, let's say we know `Parent.foo()` calls `Child.bar()` and we do the following rounds of analysis: Analysis round 1: Analyze all files, including `Parent` and `Child` Analysis round 2: Analyze `Child.bar()` only with `--changed-only flag`. `Parent.foo()` is now stale. Analysis round 3: Add procedure `Parent.baz()` that calls `Parent.foo()`, analyze in (any) incremental mode. The analysis will only analyze `Parent.baz()`. However, the specs for `Parent.foo()` are stale and may give us bad results for `Parent.baz()`. We want the analysis to re-analyze `Parent.baz()`, but before this diff it will not. This diff fixes this problem by adding a `STALE` status bit to procedure summaries. In `--changed-only` mode, the callers of a changed procedures are not re-analyzed, but their summaries are marked as stale. For both `--changed-only` and regular incremental mode, callees of changed procedures that are marked as stale are re-analyzed even if they have not changed. This is better than a more obvious solution like deleting stale procedure summaries, since that would force the next analysis to re-analyze all stale procedures even if it does not need the results for whatever analysis it is doing. This scheme implemented in this diff ensures that each analysis only does the work that it needs to compute reliable results for its changed procedures.master
parent
bdbc524f53
commit
cc2fda8165
@ -0,0 +1,41 @@
|
||||
v1_files = glob([ '**/*.java.v1'])
|
||||
v2_files = glob(['**/*.java.v2'])
|
||||
v3_files = glob(['**/*.java.v3'])
|
||||
sources = v1_files + v2_files + v3_files
|
||||
|
||||
def copy_files_strip_suffix_cmd(sfx, files):
|
||||
return ' && '.join([' '.join(['cp', f, f.replace(sfx, '')]) for f in files])
|
||||
|
||||
out = 'out'
|
||||
clean_out = ' '.join(['rm', '-rf', out])
|
||||
clean_java = ' '.join(['rm', '-rf', '*.java'])
|
||||
clean_cmd = ' && '.join([clean_out, clean_java])
|
||||
stripped_suffix_files = map(lambda f: f.replace('.v1', ''), v1_files)
|
||||
to_compile = ' '.join(stripped_suffix_files)
|
||||
infer_cmd = ' '.join([
|
||||
'infer',
|
||||
'-i',
|
||||
'--changed-only',
|
||||
'--absolute-paths',
|
||||
'-o', out,
|
||||
'-a', 'infer',
|
||||
'--',
|
||||
'javac',
|
||||
to_compile
|
||||
])
|
||||
v1_copy_cmd = copy_files_strip_suffix_cmd('.v1', v1_files)
|
||||
v2_copy_cmd = copy_files_strip_suffix_cmd('.v2', v2_files)
|
||||
v3_copy_cmd = copy_files_strip_suffix_cmd('.v3', v3_files)
|
||||
stats_copy_cmd = ' '.join(['cp', out + '/stats.json', '$OUT'])
|
||||
command = ' && '.join([clean_cmd, v1_copy_cmd, infer_cmd, v2_copy_cmd, infer_cmd, v3_copy_cmd, infer_cmd, stats_copy_cmd])
|
||||
|
||||
genrule(
|
||||
name = 'analyze',
|
||||
srcs = sources,
|
||||
out = 'stats.json',
|
||||
cmd = command,
|
||||
deps = [],
|
||||
visibility = [
|
||||
'PUBLIC',
|
||||
]
|
||||
)
|
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2015 - present Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package codetoanalyze.java.incremental.changed_only_future;
|
||||
|
||||
class Child {
|
||||
|
||||
Object bar() {
|
||||
return new Object();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2015 - present Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package codetoanalyze.java.incremental.changed_only_future;
|
||||
|
||||
class Child {
|
||||
|
||||
Object bar() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2015 - present Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package codetoanalyze.java.incremental.changed_only_future;
|
||||
|
||||
class Parent {
|
||||
|
||||
Object foo() {
|
||||
Child c = new Child();
|
||||
return c.bar();
|
||||
}
|
||||
|
||||
void bar() {
|
||||
Object o1 = new Object();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2015 - present Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package codetoanalyze.java.incremental.changed_only_future;
|
||||
|
||||
class Parent {
|
||||
|
||||
Object foo() {
|
||||
Child c = new Child();
|
||||
return c.bar();
|
||||
}
|
||||
|
||||
void bar() {
|
||||
Object o1 = new Object();
|
||||
}
|
||||
|
||||
void baz() {
|
||||
foo().toString(); // should be an NPE with Child.v2
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2015 - present Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package endtoend.java.incremental;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static utils.matchers.NumberOfFilesAnalyzed.numberOfFilesAnalyzed;
|
||||
import static utils.matchers.NumberOfProceduresAnalyzed.numberOfProceduresAnalyzed;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.Path;
|
||||
import java.io.*;
|
||||
|
||||
import utils.InferException;
|
||||
import utils.InferStats;
|
||||
|
||||
/**
|
||||
* Make sure the incremental --changed-only mode doesn't corrupt the results for future analyses by
|
||||
* refusing to analyze callers of changed procedures (and thereby creating stale specs for some poor
|
||||
* future analysis).
|
||||
*/
|
||||
public class ChangedOnlyFuture {
|
||||
|
||||
public static final String SOURCE_DIR =
|
||||
"/infer/tests/codetoanalyze/java/incremental/changed_only_future/";
|
||||
|
||||
private static InferStats inferStats;
|
||||
|
||||
@BeforeClass
|
||||
public static void loadResults() throws InterruptedException, IOException {
|
||||
inferStats = InferStats.loadInferStats(ChangedOnlyModeTest.class, SOURCE_DIR);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlyChangedFileReanalyzedInChangedOnlyMode()
|
||||
throws IOException, InterruptedException, InferException {
|
||||
assertThat(
|
||||
"Only the changed file should be re-analyzed",
|
||||
inferStats,
|
||||
numberOfFilesAnalyzed(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changedAndStaleProcsReanalyze()
|
||||
throws IOException, InterruptedException, InferException {
|
||||
assertThat(
|
||||
"Both the new (changed) procedure and its stale (unchanged) callee should be re-analyzed",
|
||||
inferStats,
|
||||
numberOfProceduresAnalyzed(2));
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in new issue