Coverage for src/renaissance/project/project_scanner.py: 100%
55 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-02 13:12 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-02 13:12 +0000
1"""Language-specific source file scanners used to feed recipes with the files to process."""
3import json
4import subprocess
5from pathlib import Path
8class ProjectScanner:
9 """Base class for language-specific source file scanners."""
11 def find_sources(self) -> list[str]:
12 """Return the paths of source files found by this scanner. Implemented by subclasses."""
13 raise NotImplementedError
16class CppScanner(ProjectScanner):
17 """Scan a C/C++ project's compile_commands.json for source files."""
19 def __init__(self, compile_commands_path: str = "compile_commands.json") -> None:
20 """Store the path to the compile_commands.json to read sources from."""
21 self.compile_commands_path = compile_commands_path
23 def find_sources(self) -> list[str]:
24 """Return every file entry listed in compile_commands.json, sorted and deduplicated."""
25 if not Path(self.compile_commands_path).exists():
26 message = "compile_commands.json not found"
27 raise FileNotFoundError(message)
28 with Path(self.compile_commands_path).open() as f:
29 commands = json.load(f)
30 return sorted({entry["file"] for entry in commands if "file" in entry})
33class JavaScanner(ProjectScanner):
34 """Scan a directory tree for Java source files."""
36 def __init__(self, root_dir: str = ".") -> None:
37 """Store the root directory to scan for .java files."""
38 self.root_dir = root_dir
40 def find_sources(self) -> list[str]:
41 """Return every .java file under root_dir, sorted."""
42 # TODO: is this correct? does this filter out files correctly?
43 # See https://github.com/TNO/Renaissance.Py/issues/200
45 java_files = Path(self.root_dir).rglob("*.java")
46 return sorted(str(f) for f in java_files)
49class PythonScanner(ProjectScanner):
50 """Scan a directory tree for Python source files."""
52 EXCLUDED_DIRS = frozenset({".git", "__pycache__", ".venv", "venv"})
53 # TODO: incomplete list, extend this list with more files/directories that should always be ignored
55 def __init__(self, root_dir: str = ".", package_dirs: list[str] | None = None) -> None:
56 """Store the scan root and optional package_dirs narrowing.
58 package_dirs, when given, narrows the scan to those subdirectories of root_dir.
59 Left as None (the default), the whole of root_dir is scanned instead of assuming a
60 src/lib/test layout, since that assumption silently skipped real third-party layouts.
61 """
62 self.root_dir = root_dir
63 self.package_dirs = package_dirs
65 def find_sources(self) -> list[str]:
66 """Return every .py file under root_dir (or package_dirs, if given), sorted, excluding EXCLUDED_DIRS."""
67 path = Path(self.root_dir)
68 if not path.exists():
69 message = f"root_dir does not exist: {self.root_dir}"
70 raise FileNotFoundError(message)
71 if not path.is_dir():
72 message = f"root_dir is not a directory: {self.root_dir}"
73 raise NotADirectoryError(message)
75 roots = [Path(self.root_dir) / d for d in self.package_dirs] if self.package_dirs else [Path(self.root_dir)]
76 files: list[Path] = []
77 for root in roots:
78 if not root.exists():
79 continue
80 files.extend(path for path in root.rglob("*.py") if not any(part in self.EXCLUDED_DIRS for part in path.parts))
81 return sorted(str(path) for path in files)
84class BearCppScanner(CppScanner):
85 """CppScanner that generates compile_commands.json via Bear when it's missing."""
87 def __init__(self, build_dir: str = ".", compile_commands_path: str = "compile_commands.json") -> None:
88 """Store the build directory to run Bear in, alongside the compile_commands.json path."""
89 super().__init__(compile_commands_path)
90 self.build_dir = build_dir
92 def run_bear(self) -> None:
93 """Run Bear to generate compile_commands.json by wrapping the project's make invocation."""
94 # bear is resolved via PATH intentionally; build_dir isn't attacker-controlled here.
95 result = subprocess.run(["bear", "--", "make", "-C", self.build_dir], check=False) # noqa: S603, S607
96 if result.returncode != 0:
97 message = "Bear failed to run or make failed."
98 raise RuntimeError(message)
100 def find_sources(self) -> list[str]:
101 """Generate compile_commands.json via Bear if missing, then return its listed sources."""
102 if not Path(self.compile_commands_path).exists():
103 self.run_bear()
104 return super().find_sources()