Skip to content

Commit dd50fcc

Browse files
frank-emrichmeta-codesync[bot]
authored andcommitted
fix for duplicate edge bug
Summary: This fixes a bug introduced in D90081785. We don't want duplicate edges between any pair of nodes and we have an invariant check for that. However, this could be violated in one situation: If the list of `Symbol_def.with_reason` values returned by `get_sucessors` returns duplicates, this causes us to create duplicate edges. Example for a situation where this can actually happen: ``` class Foo { public static function foo(): void { } public static function bar(): void { self::bar(); self::bar(); } } ``` `get_sucessors` for `Foo:foo` will return `Foo::bar` twice. This diff fixes this by de-duplicating the sucessor list before further processing. Reviewed By: madgen Differential Revision: D90251100 fbshipit-source-id: e02e24beec2e56afb88bc9f50b403fedbe245767
1 parent 3369d34 commit dd50fcc

9 files changed

Lines changed: 138 additions & 3 deletions

File tree

hphp/hack/src/server/serverFindMyTests.ml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,13 +305,18 @@ module Symbol_def = struct
305305
(** Invariant: The symbol's constructor name matches that of the reason:
306306
(e.g., a Symbol_def.Method is matched with a Reason.Method) *)
307307
}
308-
[@@deriving show, sexp, hash]
308+
[@@deriving show, sexp, hash, ord]
309309

310310
let class_from_name ctx class_name reason : with_strategy =
311311
let decl = get_class_decl_entry ctx class_name in
312312
let kind = Folded_class.kind decl in
313313
let strategy = Expansion_strategy.of_reason reason in
314314
{ symbol_def = Classish { name = class_name; kind }; strategy }
315+
316+
(* Just here for Hash_set *)
317+
module With_reason = struct
318+
type t = with_reason [@@deriving sexp, hash, ord]
319+
end
315320
end
316321

317322
open Symbol_def
@@ -1106,8 +1111,13 @@ module Selection_graph = struct
11061111
let new_distance = distance + 1 in
11071112
(match get_successors ~ctx ~genv ~env symbol_s with
11081113
| Result.Ok (referencing_defs, referencing_test_files) ->
1109-
List.iter
1110-
referencing_defs
1114+
(* referencing_defs may include duplicates.
1115+
We need to skip them to avoid creating duplicate edges between nodes *)
1116+
let referencing_defs_dedup =
1117+
Hash_set.of_list (module Symbol_def.With_reason) referencing_defs
1118+
in
1119+
Hash_set.iter
1120+
referencing_defs_dedup
11111121
~f:(fun { symbol_def = referencing_def; reason } ->
11121122
let referencing_symbol_s : Symbol_def.with_strategy =
11131123
{
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?hh
2+
3+
class G_CallsTwice {
4+
public static function foo(): void {
5+
G_Root::duplicateRoot();
6+
}
7+
8+
public static function callsFooTwice(): void {
9+
self::foo();
10+
self::foo();
11+
}
12+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?hh
2+
3+
class G_Cycle {
4+
5+
public static function a(): void {
6+
G_Root::cycleRoot();
7+
self::c();
8+
}
9+
10+
public static function b(): void {
11+
self::a();
12+
}
13+
14+
public static function c(): void {
15+
self::b();
16+
}
17+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?hh
2+
3+
class G_MultiPath {
4+
// Short path: root <- path1 <- join (distance 2 from root to join)
5+
public static function path1(): void {
6+
G_Root::multiPathRoot();
7+
}
8+
9+
// Long path: root <- path2a <- path2b <- join (distance 3 from root to join)
10+
public static function path2a(): void {
11+
G_Root::multiPathRoot();
12+
}
13+
14+
public static function path2b(): void {
15+
self::path2a();
16+
}
17+
18+
// Join point: called via both paths
19+
public static function join(): void {
20+
self::path1();
21+
self::path2b();
22+
}
23+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?hh
2+
3+
class G_Root {
4+
public static function duplicateRoot(): void {}
5+
public static function cycleRoot(): void {}
6+
public static function multiPathRoot(): void {}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?hh
2+
3+
class G_CycleTest extends WWWTest {
4+
public function test(): void {
5+
G_Cycle::b();
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?hh
2+
3+
class G_DuplicateTest extends WWWTest {
4+
public function test(): void {
5+
G_CallsTwice::callsFooTwice();
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?hh
2+
3+
class G_MultiPathTest extends WWWTest {
4+
public function test(): void {
5+
G_MultiPath::join();
6+
}
7+
}

hphp/hack/test/integration/test_find_my_tests.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,3 +315,48 @@ def test_typedefs_and_typeconsts2(self) -> None:
315315
],
316316
max_distance=10,
317317
)
318+
319+
def test_duplicates(self) -> None:
320+
"""Tests two kinds of duplication:
321+
- Passing the same root twice
322+
- Having a method call an existing method in the graph twice
323+
324+
Uses files from g/ subdirectory
325+
"""
326+
prefix = os.path.join(self.test_driver.repo_dir, "g", "__tests__")
327+
328+
self.check_has_tests_with_distances(
329+
prefix=prefix,
330+
symbols=["G_Root::duplicateRoot", "G_Root::duplicateRoot"],
331+
expected_test_files=[("G_DuplicateTest.php", 3)],
332+
max_distance=10,
333+
)
334+
335+
def test_cycle(self) -> None:
336+
"""Tests that cycles in the call graph do not cause issues.
337+
338+
Uses files from g/ subdirectory
339+
"""
340+
prefix = os.path.join(self.test_driver.repo_dir, "g", "__tests__")
341+
342+
self.check_has_tests_with_distances(
343+
prefix=prefix,
344+
symbols=["G_Root::cycleRoot"],
345+
expected_test_files=[("G_CycleTest.php", 3)],
346+
max_distance=10,
347+
)
348+
349+
def test_multi_path(self) -> None:
350+
"""Tests that when there are multiple paths to the same method,
351+
we use the shortest distance.
352+
353+
Uses files from g/ subdirectory.
354+
"""
355+
prefix = os.path.join(self.test_driver.repo_dir, "g", "__tests__")
356+
357+
self.check_has_tests_with_distances(
358+
prefix=prefix,
359+
symbols=["G_Root::multiPathRoot"],
360+
expected_test_files=[("G_MultiPathTest.php", 3)],
361+
max_distance=10,
362+
)

0 commit comments

Comments
 (0)