From b141c03e36cd704f27e3c6563968b68fbce8e010 Mon Sep 17 00:00:00 2001
From: Andre Maroneze <andre.maroneze@cea.fr>
Date: Thu, 20 Oct 2022 14:42:28 +0200
Subject: [PATCH] [analysis-scripts] add type annotations in Python scripts

---
 share/analysis-scripts/build.py         | 56 +++++++++++++------------
 share/analysis-scripts/source_filter.py | 33 ++++++++-------
 2 files changed, 46 insertions(+), 43 deletions(-)

diff --git a/share/analysis-scripts/build.py b/share/analysis-scripts/build.py
index f392d8a1913..c253198c149 100755
--- a/share/analysis-scripts/build.py
+++ b/share/analysis-scripts/build.py
@@ -40,6 +40,8 @@ import subprocess
 import function_finder
 import source_filter
 
+from typing import Callable, TypeVar
+
 script_dir = os.path.dirname(sys.argv[0])
 
 # Command-line parsing ########################################################
@@ -114,21 +116,21 @@ dot_framac_dir = Path(".frama-c")
 
 # Check required environment variables and commands in the PATH ###############
 
-framac_bin = os.getenv("FRAMAC_BIN")
-if not framac_bin:
+framac_bin_str = os.getenv("FRAMAC_BIN")
+if not framac_bin_str:
     sys.exit("error: FRAMAC_BIN not in environment (set by frama-c-script)")
-framac_bin = Path(framac_bin)
+framac_bin = Path(framac_bin_str)
 
 under_test = os.getenv("PTESTS_TESTING")
 
 # Prepare blug-related variables and functions ################################
 
-blug = os.getenv("BLUG")
-if not blug:
-    blug = shutil.which("blug")
-    if not blug:
+blug_str = os.getenv("BLUG")
+if not blug_str:
+    blug_str = shutil.which("blug")
+    if not blug_str:
         sys.exit("error: path to 'blug' binary must be in PATH or variable BLUG")
-blug = Path(blug)
+blug = Path(blug_str)
 blug_dir = blug.resolve().parent
 # to import blug_jbdb
 sys.path.insert(0, blug_dir.as_posix())
@@ -139,20 +141,20 @@ from blug_jbdb import prettify
 # Auxiliary functions #########################################################
 
 
-def call_and_get_output(command_and_args):
+def call_and_get_output(command_and_args: list[str]) -> str:
     try:
         return subprocess.check_output(command_and_args, stderr=subprocess.STDOUT).decode()
     except subprocess.CalledProcessError as e:
         sys.exit(f"error running command: {command_and_args}\n{e}")
 
 
-def ask_if_overwrite(path):
+def ask_if_overwrite(path: Path) -> None:
     yn = input(f"warning: {path} already exists. Overwrite? [y/N] ")
     if yn == "" or not (yn[0] == "Y" or yn[0] == "y"):
         sys.exit("Exiting without overwriting.")
 
 
-def insert_lines_after(lines, line_pattern, new_lines):
+def insert_lines_after(lines: list[str], line_pattern: str, new_lines: list[str]) -> None:
     re_line = re.compile(line_pattern)
     for i, line in enumerate(lines):
         if re_line.search(line):
@@ -163,7 +165,7 @@ def insert_lines_after(lines, line_pattern, new_lines):
 
 
 # delete the first occurrence of [line_pattern]
-def delete_line(lines, line_pattern):
+def delete_line(lines: list[str], line_pattern: str) -> None:
     re_line = re.compile(line_pattern)
     for i, line in enumerate(lines):
         if re_line.search(line):
@@ -172,7 +174,7 @@ def delete_line(lines, line_pattern):
     sys.exit(f"error: no lines found matching pattern: {line_pattern}")
 
 
-def replace_line(lines, line_pattern, value, all_occurrences=False):
+def replace_line(lines: list[str], line_pattern: str, value: str, all_occurrences=False) -> None:
     replaced = False
     re_line = re.compile(line_pattern)
     for i, line in enumerate(lines):
@@ -187,28 +189,28 @@ def replace_line(lines, line_pattern, value, all_occurrences=False):
 
 
 # replaces '/' and '.' with '_' so that a valid target name is created
-def make_target_name(target):
-    return prettify(target).replace("/", "_").replace(".", "_")
+def make_target_name(target: Path) -> str:
+    return prettify(str(target)).replace("/", "_").replace(".", "_")
 
 
 # sources are pretty-printed relatively to the .frama-c directory, where the
 # GNUmakefile will reside
-def rel_prefix(path):
-    return path if os.path.isabs(path) else os.path.relpath(path, start=dot_framac_dir)
+def rel_prefix(path: Path) -> str:
+    return str(path) if os.path.isabs(path) else os.path.relpath(path, start=dot_framac_dir)
 
 
-def pretty_sources(sources):
+def pretty_sources(sources: list[Path]) -> list[str]:
     return [f"  {rel_prefix(source)} \\" for source in sources]
 
 
-def lines_of_file(path):
+def lines_of_file(path: Path) -> list[str]:
     return path.read_text().splitlines()
 
 
 fc_stubs_copied = False
 
 
-def copy_fc_stubs():
+def copy_fc_stubs() -> Path:
     global fc_stubs_copied
     dest = dot_framac_dir / "fc_stubs.c"
     if not fc_stubs_copied:
@@ -230,7 +232,7 @@ def copy_fc_stubs():
 # 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, filename):
+def find_definitions(funcname: str, filename: str) -> list[tuple[str, bool]]:
     file_content = source_filter.open_and_filter(filename, not under_test)
     file_lines = file_content.splitlines(keepends=True)
     newlines = function_finder.compute_newline_offsets(file_lines)
@@ -250,21 +252,21 @@ def find_definitions(funcname, filename):
         res.append((d[2], has_args))
     return res
 
-
-def list_partition(f, l):
+T = TypeVar('T')
+def list_partition(f: Callable[[T], bool], l: list[T]) -> tuple[list[T], list[T]]:
     """Equivalent to OCaml's List.partition: returns 2 lists with the elements of l,
     partitioned according to predicate f."""
     l1 = []
     l2 = []
     for e in l:
-        if f(l):
+        if f(e):
             l1.append(e)
         else:
             l2.append(e)
     return l1, l2
 
 
-def pp_list(l):
+def pp_list(l: list[str]) -> list[str]:
     """Applies prettify to a list of sources/targets and sorts the result."""
     return sorted([prettify(e) for e in l])
 
@@ -342,7 +344,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 = {}
+main_definitions: dict[Path, list[tuple[Path, str, bool]]] = {}
 for target, sources in sources_map.items():
     main_definitions[target] = []
     for source in sources:
@@ -369,7 +371,7 @@ if not dot_framac_dir.is_dir():
     logging.debug("creating %s", dot_framac_dir)
     dot_framac_dir.mkdir(parents=True, exist_ok=False)
 
-fc_config = json.loads(call_and_get_output([framac_bin / "frama-c", "-print-config-json"]))
+fc_config = json.loads(call_and_get_output([str(framac_bin / "frama-c"), "-print-config-json"]))
 lib_dir = Path(fc_config["lib_dir"])
 
 # copy fc_stubs if at least one main function has arguments
diff --git a/share/analysis-scripts/source_filter.py b/share/analysis-scripts/source_filter.py
index c255e89c28a..1feb7a8e8be 100644
--- a/share/analysis-scripts/source_filter.py
+++ b/share/analysis-scripts/source_filter.py
@@ -42,32 +42,33 @@ from pathlib import Path
 import shutil
 import subprocess
 import sys
+from typing import Optional
 
 # warnings about missing commands are disabled during testing
 emit_warns = os.getenv("PTESTS_TESTING") is None
 
-# Cache for get_command
-cached_commands = {}
+# Cache for get_command.
+cached_commands : dict[str, Optional[Path]] = {}
 
 
-def resource_path(relative_path):
+def resource_path(relative_path) -> 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) -> Optional[Path]:
     """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)
+    cmd = os.getenv(env_var_name)
+    if cmd:
+        p = Path(cmd)
     else:
-        p = shutil.which(command)
-        if p:
-            p = Path(p)
+        cmd = shutil.which(command)
+        if cmd:
+            p = Path(cmd)
         else:
             p = Path(resource_path(command))
             if not p.exists():
@@ -81,7 +82,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: list[str], input_data: str) -> str:
     try:
         return subprocess.check_output(
             command_and_args,
@@ -94,25 +95,25 @@ def run_and_check(command_and_args, input_data):
         sys.exit(f"error running command: {command_and_args}\n{e}")
 
 
-def filter_with_scc(input_data):
+def filter_with_scc(input_data: str) -> str:
     scc = get_command("scc", "SCC")
     if scc:
-        return run_and_check([scc, "-k"], input_data)
+        return run_and_check([str(scc), "-k"], input_data)
     else:
         return input_data
 
 
-def filter_with_astyle(input_data):
+def filter_with_astyle(input_data: str) -> str:
     astyle = get_command("astyle", "ASTYLE")
     if astyle:
         return run_and_check(
-            [astyle, "--keep-one-line-blocks", "--keep-one-line-statements"], input_data
+            [str(astyle), "--keep-one-line-blocks", "--keep-one-line-statements"], input_data
         )
     else:
         return input_data
 
 
-def open_and_filter(filename, apply_filters):
+def open_and_filter(filename: str, apply_filters: bool) -> str:
     # we ignore encoding errors and use ASCII to avoid issues when
     # opening files with different encodings (UTF-8, ISO-8859, etc)
     with open(filename, "r", encoding="ascii", errors="ignore") as f:
-- 
GitLab