From 6da1b894661f1138b004c41d28105f73daa0ba98 Mon Sep 17 00:00:00 2001
From: Andre Maroneze <andre.maroneze@cea.fr>
Date: Fri, 19 Jul 2024 15:05:09 +0200
Subject: [PATCH] [analysis-scripts] add more typing annotations

---
 share/analysis-scripts/build.py               |  6 ++--
 share/analysis-scripts/detect_recursion.py    |  3 +-
 share/analysis-scripts/estimate_difficulty.py |  4 +--
 share/analysis-scripts/external_tool.py       | 18 +++++-----
 share/analysis-scripts/function_finder.py     | 35 ++++++++++++-------
 .../heuristic_list_functions.py               |  5 +--
 6 files changed, 41 insertions(+), 30 deletions(-)

diff --git a/share/analysis-scripts/build.py b/share/analysis-scripts/build.py
index d912dc3d824..22ea1a474ff 100755
--- a/share/analysis-scripts/build.py
+++ b/share/analysis-scripts/build.py
@@ -252,7 +252,7 @@ def copy_fc_stubs() -> Path:
 # Returns pairs (line_number, has_args) for each likely definition of
 # [funcname] in [filename].
 # [has_args] is used to distinguish between main(void) and main(int, char**).
-def find_definitions(funcname: str, filename: str) -> list[tuple[str, bool]]:
+def find_definitions(funcname: str, filename: Path) -> list[tuple[int, bool]]:
     file_content = source_filter.open_and_filter(
         Path(filename), not under_test and do_filter_source
     )
@@ -261,7 +261,7 @@ def find_definitions(funcname: str, filename: str) -> list[tuple[str, bool]]:
     defs = function_finder.find_definitions_and_declarations(
         True, False, filename, file_content, file_lines, newlines, funcname
     )
-    res = []
+    res: list[tuple[int, bool]] = []
     for d in defs:
         defining_line = file_lines[d[2] - 1]
         after_funcname = defining_line[defining_line.find(funcname) + len(funcname) :]
@@ -367,7 +367,7 @@ if unknown_sources:
 # We also need to check if the main function uses a 'main(void)'-style
 # signature, to patch fc_stubs.c.
 
-main_definitions: dict[Path, list[tuple[Path, str, bool]]] = {}
+main_definitions: dict[Path, list[tuple[Path, int, bool]]] = {}
 for target, sources in sorted(sources_map.items()):
     main_definitions[target] = []
     for source in sources:
diff --git a/share/analysis-scripts/detect_recursion.py b/share/analysis-scripts/detect_recursion.py
index a25078aae0b..a02790df505 100755
--- a/share/analysis-scripts/detect_recursion.py
+++ b/share/analysis-scripts/detect_recursion.py
@@ -25,6 +25,7 @@
 """This script finds files containing likely declarations and definitions
 for a given function name, via heuristic syntactic matching."""
 
+from pathlib import Path
 import sys
 import build_callgraph
 
@@ -34,7 +35,7 @@ if len(sys.argv) < 2:
     print("        prints a heuristic callgraph for the specified files.")
     sys.exit(1)
 else:
-    files = sys.argv[1:]
+    files = set([Path(f) for f in sys.argv[1:]])
 
 cg = build_callgraph.compute(files)
 build_callgraph.detect_recursion(cg)
diff --git a/share/analysis-scripts/estimate_difficulty.py b/share/analysis-scripts/estimate_difficulty.py
index 47437f56cbd..82c6c4264dd 100755
--- a/share/analysis-scripts/estimate_difficulty.py
+++ b/share/analysis-scripts/estimate_difficulty.py
@@ -261,8 +261,8 @@ if not no_cloc:
         data = external_tool.run_and_check(
             [cloc, "--hide-rate", "--progress-rate=0", "--csv"] + list(str(f) for f in files), ""
         )
-        data = data.splitlines()
-        [nfiles, _sum, nblank, ncomment, ncode] = data[-1].split(",")
+        datas = data.splitlines()
+        [nfiles, _sum, nblank, ncomment, ncode] = datas[-1].split(",")
         nlines = int(nblank) + int(ncomment) + int(ncode)
         logging.info(
             "Processing %d file(s), approx. %d lines of code (out of %d lines)",
diff --git a/share/analysis-scripts/external_tool.py b/share/analysis-scripts/external_tool.py
index d1a1b3584e0..717980c6380 100644
--- a/share/analysis-scripts/external_tool.py
+++ b/share/analysis-scripts/external_tool.py
@@ -37,24 +37,24 @@ emit_warns = os.getenv("PTESTS_TESTING") is None
 cached_commands: dict[str, Path | None] = {}
 
 
-def resource_path(relative_path):
+def resource_path(relative_path: str) -> str:
     """Get absolute path to resource; only used by the pyinstaller standalone distribution"""
     base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
     return os.path.join(base_path, relative_path)
 
 
-def get_command(command, env_var_name):
+def get_command(command: str, env_var_name: str) -> Path | None:
     """Returns a Path to the command; priority goes to the environment variable,
     then in the PATH, then in the resource directory (for a pyinstaller binary)."""
     if command in cached_commands:
         return cached_commands[command]
-    p = os.getenv(env_var_name)
-    if p:
-        p = Path(p)
+    p_str = os.getenv(env_var_name)
+    if p_str:
+        p = Path(p_str)
     else:
-        p = shutil.which(command)
-        if p:
-            p = Path(p)
+        p_str = shutil.which(command)
+        if p_str:
+            p = Path(p_str)
         else:
             p = Path(resource_path(command))
             if not p.exists():
@@ -68,7 +68,7 @@ def get_command(command, env_var_name):
     return p
 
 
-def run_and_check(command_and_args, input_data):
+def run_and_check(command_and_args, input_data: str) -> str:
     try:
         return subprocess.check_output(
             command_and_args,
diff --git a/share/analysis-scripts/function_finder.py b/share/analysis-scripts/function_finder.py
index e6410a688bd..e9e84f5545c 100644
--- a/share/analysis-scripts/function_finder.py
+++ b/share/analysis-scripts/function_finder.py
@@ -24,6 +24,7 @@
 
 """Exports find_function_in_file, to be used by other scripts"""
 
+from pathlib import Path
 import bisect
 import os
 import re
@@ -55,7 +56,7 @@ debug = os.getenv("DEBUG", False)
 
 
 # Precomputes the regex for 'fname'
-def prepare_re_specific_name(fname):
+def prepare_re_specific_name(fname: str) -> re.Pattern:
     re_fun = re.compile(
         "^"
         + optional_type_prefix
@@ -72,9 +73,9 @@ def prepare_re_specific_name(fname):
 
 
 # Returns 0 if not found, 1 if declaration, 2 if definition
-def find_specific_name(prepared_re, f):
+def find_specific_name(prepared_re: re.Pattern, f: Path) -> int:
     with open(f, encoding="ascii", errors="ignore") as data:
-        file_content = data.read()
+        file_content: str = data.read()
         has_decl_or_def = prepared_re.search(file_content)
         if has_decl_or_def is None:
             return 0
@@ -86,7 +87,7 @@ def find_specific_name(prepared_re, f):
 # matches function definitions or declarations
 # if funcname is not None, only matches for the specified
 # function name
-def compute_re_def_or_decl(funcname):
+def compute_re_def_or_decl(funcname: str) -> re.Pattern:
     ident = funcname if funcname else c_identifier
     return re.compile(
         "^"
@@ -110,7 +111,7 @@ re_funcall = re.compile("(" + c_identifier + ")" + whitespace + r"\(")
 
 # Computes the offset (in bytes) of each '\n' in the file,
 # returning them as a list
-def compute_newline_offsets(file_lines):
+def compute_newline_offsets(file_lines: list[str]) -> list[int]:
     offsets = []
     current = 0
     for line in file_lines:
@@ -122,7 +123,7 @@ def compute_newline_offsets(file_lines):
 # Returns the line number (starting at 1) containing the character
 # of offset [offset].
 # [offsets] is the sorted list of offsets for newline characters in the file.
-def line_of_offset(offsets, offset):
+def line_of_offset(offsets: list[int], offset: int) -> int:
     return bisect.bisect_right(offsets, offset) + 1
 
 
@@ -132,7 +133,7 @@ def line_of_offset(offsets, offset):
 # This is a heuristic to attempt to detect function closing braces:
 # it assumes that the first '}' (without preceding whitespace) after a
 # function definition denotes its closing brace.
-def compute_closing_braces(file_lines):
+def compute_closing_braces(file_lines: list[str]) -> list[int]:
     braces = []
     for i, line in enumerate(file_lines, start=1):
         # note: lines contain '\n', so they are never empty
@@ -153,7 +154,7 @@ def compute_closing_braces(file_lines):
 # closing braces were found).
 #
 # [line_numbers] must be sorted in ascending order.
-def get_first_line_after(line_numbers, n):
+def get_first_line_after(line_numbers: list[int], n: int) -> int | None:
     try:
         return line_numbers[bisect.bisect_left(line_numbers, n)]
     except IndexError:
@@ -172,8 +173,14 @@ def get_first_line_after(line_numbers, n):
 # itself and avoid considering it as a call. For function definitions,
 # this is the opening brace; for function declarations, this is the semicolon.
 def find_definitions_and_declarations(
-    want_defs, want_decls, filename, file_content, file_lines, newlines, funcname=None
-):
+    want_defs: bool,
+    want_decls: bool,
+    filename: Path,
+    file_content: str,
+    file_lines: list[str],
+    newlines: list[int],
+    funcname=None,
+) -> list[tuple[str, bool, int, int, int]]:
     braces = compute_closing_braces(file_lines)
     res = []
     re_fundef_or_decl = compute_re_def_or_decl(funcname)
@@ -199,8 +206,10 @@ def find_definitions_and_declarations(
             if definition.strip().endswith("}"):
                 end = line_of_offset(newlines, terminator_offset)
             else:
-                end = get_first_line_after(braces, start)
-                if not end:
+                maybe_end = get_first_line_after(braces, start)
+                if maybe_end:
+                    end = maybe_end
+                else:
                     # no closing braces found; try again the "single-line function heuristic"
                     line_of_opening_brace = line_of_offset(newlines, terminator_offset)
                     if start == line_of_opening_brace and definition.rstrip()[-1] == "}":
@@ -230,7 +239,7 @@ calls_blacklist = ["if", "while", "for", "return", "sizeof", "switch", "_Alignas
 #
 # Note: this may include the function prototype itself;
 # it must be filtered by the caller.
-def find_calls(file_content, newlines):
+def find_calls(file_content: str, newlines: list[int]) -> list[tuple[str, int, int]]:
     # create a list of Match objects that fit "pattern" regex
     res = []
     for match in re.finditer(re_funcall, file_content):
diff --git a/share/analysis-scripts/heuristic_list_functions.py b/share/analysis-scripts/heuristic_list_functions.py
index 5caddd0a32d..87614435156 100755
--- a/share/analysis-scripts/heuristic_list_functions.py
+++ b/share/analysis-scripts/heuristic_list_functions.py
@@ -25,6 +25,7 @@
 """This script uses heuristics to list all function definitions and
 declarations in a set of files."""
 
+from pathlib import Path
 import sys
 import os
 import function_finder
@@ -39,7 +40,7 @@ if len(sys.argv) < 4:
     sys.exit(1)
 
 
-def boolish_string(s):
+def boolish_string(s: str) -> bool:
     if s.lower() == "true" or s == "1":
         return True
     if s.lower() == "false" or s == "0":
@@ -49,7 +50,7 @@ def boolish_string(s):
 
 want_defs = boolish_string(sys.argv[1])
 want_decls = boolish_string(sys.argv[2])
-files = sys.argv[3:]
+files = set([Path(f) for f in sys.argv[3:]])
 
 for f in sorted(files):
     with open(f, encoding="ascii", errors="ignore") as data:
-- 
GitLab