Repository: mozilla/bugbug

Issue:
Use a single rust-code-analysis API call to get both functions and metrics
The function information is already available in the metrics API call result, so there's no need to do two API calls.


Discussion:
This will mean faster mining of commit data, so can be useful to make the HTTP service replies faster (so it should help with #1422).

Resolving patch:
diff --git a/bugbug/repository.py b/bugbug/repository.py
--- a/bugbug/repository.py
+++ b/bugbug/repository.py
@@ -288,23 +288,33 @@ def hg_modified_files(hg, commit):
     )
 
 
-def get_touched_functions(path, deleted_lines, added_lines, content):
-    if content is None:
-        return set()
+def get_functions_from_metrics(metrics_space):
+    functions = []
+
+    if (
+        metrics_space["kind"] == "function"
+        and metrics_space["name"]
+        and metrics_space["name"] != "<anonymous>"
+    ):
+        functions.append(
+            {
+                "end_line": metrics_space["end_line"],
+                "name": metrics_space["name"],
+                "start_line": metrics_space["start_line"],
+            }
+        )
+
+    for space in metrics_space["spaces"]:
+        functions += get_functions_from_metrics(space)
+
+    return functions
 
-    function_data = code_analysis_server.function(path, content)
-    if not function_data:
-        return set()
 
+def get_touched_functions(metrics_space, deleted_lines, added_lines):
     touched_functions = set()
     touched_function_names = set()
 
-    functions = function_data["spans"]
-    functions = [
-        function
-        for function in functions
-        if not function["error"] and function["name"] != "<anonymous>"
-    ]
+    functions = get_functions_from_metrics(metrics_space)
 
     def get_touched(functions, lines):
         last_f = 0
@@ -355,7 +365,10 @@ def get_touched(functions, lines):
 
 
 def get_metrics(commit, metrics_space):
-    if metrics_space["kind"] == "function":
+    name = metrics_space["name"]
+    error = metrics_space["kind"] in {"unit", "function"} and name == ""
+
+    if metrics_space["kind"] == "function" and not error:
         metrics = metrics_space["metrics"]
         commit.total_cyclomatic += metrics["cyclomatic"]
         commit.total_halstead_n2 += metrics["halstead"]["n2"]
@@ -410,7 +423,9 @@ def get_metrics(commit, metrics_space):
         )
 
     for space in metrics_space["spaces"]:
-        get_metrics(commit, space)
+        error |= get_metrics(commit, space)
+
+    return error
 
 
 def transform(hg, repo_dir, commit):
@@ -468,30 +483,38 @@ def transform(hg, repo_dir, commit):
         elif type_ in SOURCE_CODE_TYPES_TO_EXT:
             commit.source_code_files_modified_num += 1
 
-            touched_functions = get_touched_functions(
-                path, stats["deleted_lines"], stats["added_lines"], after
-            )
-            if len(touched_functions) > 0:
-                commit.functions[path] = list(touched_functions)
-
             commit.source_code_added += len(stats["added_lines"])
             commit.source_code_deleted += len(stats["deleted_lines"])
 
             if size is not None:
                 source_code_sizes.append(size)
-                metrics = code_analysis_server.metrics(path, after, unit=False)
-                if metrics.get("spaces"):
-                    metrics_file_count += 1
-                    get_metrics(commit, metrics["spaces"])
-
-                # Add "Objective-C/C++" type if rust-code-analysis detected this is an Objective-C/C++ file.
-                # We use both C/C++ and Objective-C/C++ as Objective-C/C++ files are few but share most characteristics
-                # with C/C++ files: we don't want to lose this information by just overwriting the type, but we want
-                # the additional information that it is an Objective-C/C++ file.
-                if type_ == "C/C++" and (
-                    metrics.get("language") == "obj-c/c++" or ext in {".m", ".mm"}
-                ):
-                    commit.types.add("Objective-C/C++")
+
+                if type_ != "IDL/IPDL/WebIDL":
+                    metrics = code_analysis_server.metrics(path, after, unit=False)
+                    if metrics.get("spaces"):
+                        metrics_file_count += 1
+                        error = get_metrics(commit, metrics["spaces"])
+                        if error:
+                            logger.debug(
+                                f"rust-code-analysis error on commit {commit.node}, path {path}"
+                            )
+
+                        touched_functions = get_touched_functions(
+                            metrics["spaces"],
+                            stats["deleted_lines"],
+                            stats["added_lines"],
+                        )
+                        if len(touched_functions) > 0:
+                            commit.functions[path] = list(touched_functions)
+
+                    # Add "Objective-C/C++" type if rust-code-analysis detected this is an Objective-C/C++ file.
+                    # We use both C/C++ and Objective-C/C++ as Objective-C/C++ files are few but share most characteristics
+                    # with C/C++ files: we don't want to lose this information by just overwriting the type, but we want
+                    # the additional information that it is an Objective-C/C++ file.
+                    if type_ == "C/C++" and (
+                        metrics.get("language") == "obj-c/c++" or ext in {".m", ".mm"}
+                    ):
+                        commit.types.add("Objective-C/C++")
 
             commit.types.add(type_)
         else:
diff --git a/bugbug/rust_code_analysis_server.py b/bugbug/rust_code_analysis_server.py
--- a/bugbug/rust_code_analysis_server.py
+++ b/bugbug/rust_code_analysis_server.py
@@ -77,12 +77,3 @@ def metrics(self, filename, code, unit=True):
             return {}
 
         return r.json()
-
-    def function(self, filename, code):
-        url = f"{self.base_url}/function?file_name={filename}"
-        r = requests.post(url, data=code, headers=HEADERS)
-
-        if not r.ok:
-            return {}
-
-        return r.json()


Test patch:
diff --git a/tests/test_repository.py b/tests/test_repository.py
--- a/tests/test_repository.py
+++ b/tests/test_repository.py
@@ -1542,13 +1542,10 @@ def test_get_touched_functions():
     # Allow using the local code analysis server.
     responses.add_passthru("http://127.0.0.1")
 
-    repository.code_analysis_server = rust_code_analysis_server.RustCodeAnalysisServer()
+    code_analysis_server = rust_code_analysis_server.RustCodeAnalysisServer()
 
-    # No function touched.
-    touched_functions = repository.get_touched_functions(
+    metrics = code_analysis_server.metrics(
         "file.cpp",
-        [],
-        [],
         """void func1() {
     int i = 1;
 }
@@ -1556,15 +1553,16 @@ def test_get_touched_functions():
 void func2() {
     int i = 2;
 }""",
+        unit=False,
     )
 
+    # No function touched.
+    touched_functions = repository.get_touched_functions(metrics["spaces"], [], [],)
+
     assert touched_functions == set()
 
-    # A function touched by adding a line.
-    touched_functions = repository.get_touched_functions(
+    metrics = code_analysis_server.metrics(
         "file.cpp",
-        [],
-        [1],
         """void func1() {
     int i = 1;
 }
@@ -1572,15 +1570,16 @@ def test_get_touched_functions():
 void func2() {
     int i = 2;
 }""",
+        unit=False,
     )
 
+    # A function touched by adding a line.
+    touched_functions = repository.get_touched_functions(metrics["spaces"], [], [1],)
+
     assert touched_functions == {("func1", 1, 3)}
 
-    # A function touched by removing a line, another function touched by adding a line.
-    touched_functions = repository.get_touched_functions(
+    metrics = code_analysis_server.metrics(
         "file.cpp",
-        [2, 5, 6, 7, 8],
-        [6],
         """void func1() {
     int i = 1;
 }
@@ -1592,15 +1591,18 @@ def test_get_touched_functions():
 void func4() {
     int i = 4;
 }""",
+        unit=False,
+    )
+
+    # A function touched by removing a line, another function touched by adding a line.
+    touched_functions = repository.get_touched_functions(
+        metrics["spaces"], [2, 5, 6, 7, 8], [6],
     )
 
     assert touched_functions == {("func3", 5, 7), ("func1", 1, 3)}
 
-    # A function touched by replacing a line.
-    touched_functions = repository.get_touched_functions(
+    metrics = code_analysis_server.metrics(
         "file.cpp",
-        [6],
-        [6],
         """void func1() {
     int i = 1;
 }
@@ -1608,50 +1610,57 @@ def test_get_touched_functions():
 void func2() {
     int i = 2;
 }""",
+        unit=False,
     )
 
+    # A function touched by replacing a line.
+    touched_functions = repository.get_touched_functions(metrics["spaces"], [6], [6],)
+
     assert touched_functions == {("func2", 5, 7)}
 
-    # top-level and a JavaScript function touched.
-    touched_functions = repository.get_touched_functions(
+    metrics = code_analysis_server.metrics(
         "file.js",
-        [],
-        [1, 4],
         """let j = 0;
 
 function func() {
 let i = 0;
 }""",
+        unit=False,
     )
 
+    # top-level and a JavaScript function touched.
+    touched_functions = repository.get_touched_functions(metrics["spaces"], [], [1, 4],)
+
     assert touched_functions == {("func", 3, 5)}
 
-    # An anonymous function touched inside another function.
-    touched_functions = repository.get_touched_functions(
+    metrics = code_analysis_server.metrics(
         "file.jsm",
-        [],
-        [4],
         """function outer_func() {
 let i = 0;
 let f = function() {
   let j = 0;
 }();
 }""",
+        unit=False,
     )
 
+    # An anonymous function touched inside another function.
+    touched_functions = repository.get_touched_functions(metrics["spaces"], [], [4],)
+
     assert touched_functions == {("outer_func", 1, 6)}
 
-    # A function touched inside another function.
-    touched_functions = repository.get_touched_functions(
+    metrics = code_analysis_server.metrics(
         "file.jsm",
-        [],
-        [4],
         """function outer_func() {
 let i = 0;
 function inner_func() {
   let j = 0;
 }
 }""",
+        unit=False,
     )
 
+    # A function touched inside another function.
+    touched_functions = repository.get_touched_functions(metrics["spaces"], [], [4],)
+
     assert touched_functions == {("outer_func", 1, 6), ("inner_func", 3, 5)}