diff --git a/.gitignore b/.gitignore index 4d61e89..088786b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ codra.egg-info/ result.json + +viewer/node_modules/ diff --git a/.vscode/launch.json b/.vscode/launch.json index 9130c5b..2654712 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,12 +9,14 @@ "env": { "PYTHONPATH": "${workspaceFolder}" }, - "module": "codra.cli", + "module": "codra.cli.main", "args": [ "${workspaceFolder}/codra", "--threshold-csa", "5", "--threshold-id", "2", - "--threshold-bps", "0.7" + "--threshold-bps", "0.7", + "--log-level", "DEBUG", + "--output", "${workspaceFolder}/report.json" ], "console": "integratedTerminal", "justMyCode": false diff --git a/PROMT.md b/PROMT.md new file mode 100644 index 0000000..08ac8e5 --- /dev/null +++ b/PROMT.md @@ -0,0 +1,50 @@ +1) Prompt for Writing New Code (optimize CSA, ID, BPS) +Prompt: + +You are writing Python code that will be analyzed using three metrics: + +CSA (complexity / symbol usage), + +ID (indirection depth / call graph complexity), + +BPS (branching/condition penalty; complex conditions reduce the score). + +Write code that optimizes all three metrics: + +Keep functions small, single-purpose, and avoid deep call chains (low ID). + +Minimize external symbol usage and keep dependencies localized (low CSA). + +Keep if conditions simple; avoid chained boolean logic and function calls inside conditions (high BPS). + +Precompute values before conditionals, and use helper functions to isolate logic. + +Prefer clear, flat control flow (early returns) over nested branches. + +Produce clean, readable, and maintainable code that adheres to these constraints. + + + + +2) Prompt for Refactoring Existing Code (improve CSA, ID, BPS) +Prompt: + +You are refactoring existing Python code to improve CSA, ID, and BPS metrics: + +CSA improves by reducing external symbol usage and simplifying symbol dependencies. + +ID improves by shortening call chains and reducing indirection depth. + +BPS improves by simplifying conditional expressions and removing calls inside conditions. + +Refactor the code while preserving behavior: + +Extract complex condition logic into small helpers and precompute values outside if statements. + +Split large functions into smaller ones only if it does not create deeper call chains. + +Reduce dependency on external symbols by localizing logic and data. + +Flatten control flow and avoid unnecessary layers of abstraction. + +Keep the output behavior identical, add tests only when needed, and explain how the changes improve CSA, ID, and BPS. \ No newline at end of file diff --git a/README.md b/README.md index 1e706e6..fca4f71 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,19 @@ pip install -e . Analizza un percorso e stampa il report JSON su stdout: ```bash -python3 -m codra.cli /percorso/progetto +python3 -m codra.cli.main /percorso/progetto ``` Esempio con soglie: ```bash -python3 -m codra.cli /percorso/progetto --threshold-csa 10 --threshold-id 2 --threshold-bps 0.7 +python3 -m codra.cli.main /percorso/progetto --threshold-csa 10 --threshold-id 2 --threshold-bps 0.7 +``` + +Esempio con log e file di output: + +```bash +python3 -m codra.cli.main /percorso/progetto --log-level DEBUG --output report.json ``` ## Viewer React (opzionale) diff --git a/codra/__init__.py b/codra/__init__.py index 59736ce..33de38a 100644 --- a/codra/__init__.py +++ b/codra/__init__.py @@ -1,14 +1,14 @@ -from .bps_analyzer import BpsAnalyzer -from .bps_result import BpsResult -from .csa_analyzer import CsaAnalyzer -from .csa_result import CsaResult -from .directory_scanner import DirectoryScanner -from .indirection_analyzer import IndirectionAnalyzer -from .indirection_result import IndirectionResult -from .report import Report -from .report_builder import ReportBuilder -from .report_serializer import ReportSerializer -from .unit_definition import UnitDefinition +from .bps.analyzer import BpsAnalyzer +from .bps.result import BpsResult +from .csa.analyzer import CsaAnalyzer +from .csa.result import CsaResult +from .directory.scanner import DirectoryScanner +from .indirection.analyzer import IndirectionAnalyzer +from .indirection.result import IndirectionResult +from .report.builder import ReportBuilder +from .report.model import Report +from .report.serializer import ReportSerializer +from .unit.definition import UnitDefinition __all__ = [ "BpsAnalyzer", diff --git a/codra/alias/__init__.py b/codra/alias/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codra/alias_collector.py b/codra/alias/collector.py similarity index 86% rename from codra/alias_collector.py rename to codra/alias/collector.py index ef23a78..690dd5a 100644 --- a/codra/alias_collector.py +++ b/codra/alias/collector.py @@ -1,22 +1,24 @@ from __future__ import annotations import ast +from ast import NodeVisitor from dataclasses import dataclass, field @dataclass -class AliasCollector(ast.NodeVisitor): +class AliasCollector(NodeVisitor): + """AST visitor collecting top-level alias assignments via NodeVisitor.""" aliases: dict[str, str] = field(default_factory=dict) depth: int = 0 def collect(self, tree: ast.AST) -> dict[str, str]: - self.visit(tree) + super().visit(tree) return dict(self.aliases) def visit_Module(self, node: ast.Module) -> None: self.depth += 1 for statement in node.body: - self.visit(statement) + super().visit(statement) self.depth -= 1 def visit_FunctionDef(self, node: ast.FunctionDef) -> None: diff --git a/codra/bps/__init__.py b/codra/bps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codra/bps/analyzer.py b/codra/bps/analyzer.py new file mode 100644 index 0000000..dba6ffc --- /dev/null +++ b/codra/bps/analyzer.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import ast +from dataclasses import dataclass + +from ..condition.collector import ConditionMetricsCollector +from ..function.symbol_collector import FunctionSymbolCollector +from ..if_.collector import IfCollector +from ..unit.node import UnitNode +from ..unit.node_collector import UnitNodeCollector +from .result import BpsResult + + +@dataclass +class BpsAnalyzer: + def analyze_file(self, file_path: str) -> list[BpsResult]: + tree = self._parse_file(file_path) + unit_nodes = self._collect_units(tree, file_path) + scores = [self._collect_bps(unit_node) for unit_node in unit_nodes] + return self._build_results(unit_nodes, scores) + + def _parse_file(self, file_path: str) -> ast.AST: + with open(file_path, "r", encoding="utf-8") as handle: + source = handle.read() + return ast.parse(source, filename=file_path) + + def _collect_units(self, tree: ast.AST, file_path: str) -> list[UnitNode]: + unit_collector = UnitNodeCollector(file_path=file_path) + unit_collector.visit(tree) + return unit_collector.units + + def _collect_bps(self, unit_node: UnitNode) -> float: + usage = FunctionSymbolCollector().collect(unit_node.node) + local_names = usage.locals + if_nodes = IfCollector().collect(unit_node.node) + if not if_nodes: + return 1.0 + scores: list[float] = [] + for if_node in if_nodes: + metrics = ConditionMetricsCollector(local_names=local_names).collect( + if_node.test + ) + penalty = ( + metrics.bool_ops + + metrics.compare_ops + + 2 * metrics.calls + + 2 * metrics.external_refs + ) + scores.append(1.0 / (1.0 + penalty)) + return sum(scores) / len(scores) + + def _build_results( + self, unit_nodes: list[UnitNode], scores: list[float] + ) -> list[BpsResult]: + return [ + BpsResult(unit=unit_node.definition, bps=bps) + for unit_node, bps in zip(unit_nodes, scores, strict=True) + ] diff --git a/codra/bps_result.py b/codra/bps/result.py similarity index 77% rename from codra/bps_result.py rename to codra/bps/result.py index 24cffef..9009d56 100644 --- a/codra/bps_result.py +++ b/codra/bps/result.py @@ -2,7 +2,7 @@ from dataclasses import dataclass -from .unit_definition import UnitDefinition +from ..unit.definition import UnitDefinition @dataclass(frozen=True) diff --git a/codra/bps_analyzer.py b/codra/bps_analyzer.py deleted file mode 100644 index 98f720f..0000000 --- a/codra/bps_analyzer.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations - -import ast -from dataclasses import dataclass - -from .bps_result import BpsResult -from .condition_metrics_collector import ConditionMetricsCollector -from .function_symbol_collector import FunctionSymbolCollector -from .if_collector import IfCollector -from .unit_node_collector import UnitNodeCollector - - -@dataclass -class BpsAnalyzer: - def analyze_file(self, file_path: str) -> list[BpsResult]: - with open(file_path, "r", encoding="utf-8") as handle: - source = handle.read() - tree = ast.parse(source, filename=file_path) - unit_collector = UnitNodeCollector(file_path=file_path) - unit_collector.visit(tree) - results: list[BpsResult] = [] - for unit_node in unit_collector.units: - usage = FunctionSymbolCollector().collect(unit_node.node) - local_names = usage.locals - if_nodes = IfCollector().collect(unit_node.node) - if not if_nodes: - bps = 1.0 - else: - scores: list[float] = [] - for if_node in if_nodes: - metrics = ConditionMetricsCollector(local_names=local_names).collect( - if_node.test - ) - penalty = ( - metrics.bool_ops - + metrics.compare_ops - + 2 * metrics.calls - + 2 * metrics.external_refs - ) - scores.append(1.0 / (1.0 + penalty)) - bps = sum(scores) / len(scores) - results.append(BpsResult(unit=unit_node.definition, bps=bps)) - return results diff --git a/codra/call/__init__.py b/codra/call/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codra/call_collector.py b/codra/call/collector.py similarity index 62% rename from codra/call_collector.py rename to codra/call/collector.py index 5ba57e4..b311752 100644 --- a/codra/call_collector.py +++ b/codra/call/collector.py @@ -1,16 +1,18 @@ from __future__ import annotations import ast +from ast import NodeVisitor from dataclasses import dataclass, field @dataclass -class CallCollector(ast.NodeVisitor): +class CallCollector(NodeVisitor): + """AST visitor collecting call expressions via NodeVisitor.""" calls: list[str] = field(default_factory=list) def collect(self, node: ast.AST) -> list[str]: for statement in node.body: - self.visit(statement) + super().visit(statement) return list(self.calls) def visit_FunctionDef(self, node: ast.FunctionDef) -> None: @@ -25,4 +27,7 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: def visit_Call(self, node: ast.Call) -> None: if isinstance(node.func, ast.Name): self.calls.append(node.func.id) - self.generic_visit(node) + elif isinstance(node.func, ast.Attribute): + if isinstance(node.func.value, ast.Name) and node.func.value.id == "self": + self.calls.append(f"self.{node.func.attr}") + super().generic_visit(node) diff --git a/codra/cli.py b/codra/cli.py deleted file mode 100644 index 1c2efae..0000000 --- a/codra/cli.py +++ /dev/null @@ -1,75 +0,0 @@ -from __future__ import annotations - -import argparse -import sys -from dataclasses import dataclass - -from .bps_analyzer import BpsAnalyzer -from .csa_analyzer import CsaAnalyzer -from .file_path_collector import FilePathCollector -from .indirection_analyzer import IndirectionAnalyzer -from .report_builder import ReportBuilder -from .report_serializer import ReportSerializer -from .threshold_config import ThresholdConfig - - -@dataclass(frozen=True) -class CliArgs: - path: str - threshold_csa: int | None - threshold_id: int | None - threshold_bps: float | None - - -@dataclass(frozen=True) -class CliDependencies: - builder: ReportBuilder - serializer: ReportSerializer - - -def parse_args(argv: list[str]) -> CliArgs: - parser = argparse.ArgumentParser() - parser.add_argument("path") - parser.add_argument("--threshold-csa", type=int, default=None) - parser.add_argument("--threshold-id", type=int, default=None) - parser.add_argument("--threshold-bps", type=float, default=None) - o = parser.parse_args(argv) - return CliArgs( - path=o.path, - threshold_csa=o.threshold_csa, - threshold_id=o.threshold_id, - threshold_bps=o.threshold_bps, - ) - - -def build_thresholds(args: CliArgs) -> ThresholdConfig: - return ThresholdConfig( - csa=args.threshold_csa, - indirection=args.threshold_id, - bps=args.threshold_bps, - ) - - -def build_dependencies() -> CliDependencies: - builder = ReportBuilder( - csa_analyzer=CsaAnalyzer(), - indirection_analyzer=IndirectionAnalyzer(), - bps_analyzer=BpsAnalyzer(), - path_collector=FilePathCollector(), - ) - return CliDependencies(builder=builder, serializer=ReportSerializer()) - - -def run_cli(args: CliArgs, deps: CliDependencies) -> int: - report = deps.builder.build(args.path) - sys.stdout.write(deps.serializer.to_json(report)) - sys.stdout.write("\n") - return 0 if deps.builder.check_thresholds(report, build_thresholds(args)) else 1 - - -def main() -> None: - raise SystemExit(run_cli(parse_args(sys.argv[1:]), build_dependencies())) - - -if __name__ == "__main__": - main() diff --git a/codra/cli/__init__.py b/codra/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codra/cli/args.py b/codra/cli/args.py new file mode 100644 index 0000000..93c9cb8 --- /dev/null +++ b/codra/cli/args.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class CliArgs: + path: str + threshold_csa: int | None + threshold_id: int | None + threshold_bps: float | None + log_level: str + output: str | None diff --git a/codra/cli/dependencies.py b/codra/cli/dependencies.py new file mode 100644 index 0000000..0b339ef --- /dev/null +++ b/codra/cli/dependencies.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from ..report.builder import ReportBuilder +from ..report.serializer import ReportSerializer + + +@dataclass(frozen=True) +class CliDependencies: + builder: ReportBuilder + serializer: ReportSerializer diff --git a/codra/cli/main.py b/codra/cli/main.py new file mode 100644 index 0000000..1471eef --- /dev/null +++ b/codra/cli/main.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import argparse +import logging +import sys + +from ..bps.analyzer import BpsAnalyzer +from ..csa.analyzer import CsaAnalyzer +from ..file.path_collector import FilePathCollector +from ..indirection.analyzer import IndirectionAnalyzer +from ..report.builder import ReportBuilder +from ..report.serializer import ReportSerializer +from ..threshold.config import ThresholdConfig +from .args import CliArgs +from .dependencies import CliDependencies + + +logger = logging.getLogger(__name__) + + +def parse_args(argv: list[str]) -> CliArgs: + parser = argparse.ArgumentParser() + parser.add_argument("path") + parser.add_argument("--threshold-csa", type=int, default=None) + parser.add_argument("--threshold-id", type=int, default=None) + parser.add_argument("--threshold-bps", type=float, default=None) + parser.add_argument( + "--log-level", + default="INFO", + help="Logging level (e.g. DEBUG, INFO, WARNING).", + ) + parser.add_argument( + "--output", + default=None, + help="Output file path. Defaults to stdout when omitted.", + ) + o = parser.parse_args(argv) + return CliArgs( + path=o.path, + threshold_csa=o.threshold_csa, + threshold_id=o.threshold_id, + threshold_bps=o.threshold_bps, + log_level=o.log_level, + output=o.output, + ) + + +def build_thresholds(args: CliArgs) -> ThresholdConfig: + return ThresholdConfig( + csa=args.threshold_csa, + indirection=args.threshold_id, + bps=args.threshold_bps, + ) + + +def build_dependencies() -> CliDependencies: + builder = ReportBuilder( + csa_analyzer=CsaAnalyzer(), + indirection_analyzer=IndirectionAnalyzer(), + bps_analyzer=BpsAnalyzer(), + path_collector=FilePathCollector(), + ) + return CliDependencies(builder=builder, serializer=ReportSerializer()) + + +def configure_logging(log_level: str) -> None: + level = logging._nameToLevel.get(log_level.upper(), logging.INFO) + logging.basicConfig( + level=level, + format="%(levelname)s:%(name)s:%(message)s", + ) + + +def run_cli(args: CliArgs, deps: CliDependencies) -> int: + logger.info("Starting analysis for %s", args.path) + report = deps.builder.build(args.path) + payload = deps.serializer.to_json(report) + if args.output: + logger.info("Writing report to %s", args.output) + with open(args.output, "w", encoding="utf-8") as handle: + handle.write(payload) + handle.write("\n") + else: + logger.info("Writing report to stdout") + sys.stdout.write(payload) + sys.stdout.write("\n") + return 0 if deps.builder.check_thresholds(report, build_thresholds(args)) else 1 + + +def main() -> None: + args = parse_args(sys.argv[1:]) + configure_logging(args.log_level) + raise SystemExit(run_cli(args, build_dependencies())) + + +if __name__ == "__main__": + main() diff --git a/codra/condition/__init__.py b/codra/condition/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codra/condition_metrics_collector.py b/codra/condition/collector.py similarity index 58% rename from codra/condition_metrics_collector.py rename to codra/condition/collector.py index a2bcc0a..a0ab219 100644 --- a/codra/condition_metrics_collector.py +++ b/codra/condition/collector.py @@ -1,13 +1,15 @@ from __future__ import annotations import ast +from ast import NodeVisitor from dataclasses import dataclass -from .condition_metrics import ConditionMetrics +from .metrics import ConditionMetrics @dataclass -class ConditionMetricsCollector(ast.NodeVisitor): +class ConditionMetricsCollector(NodeVisitor): + """AST visitor collecting condition metrics via NodeVisitor.""" local_names: set[str] bool_ops: int = 0 compare_ops: int = 0 @@ -15,7 +17,7 @@ class ConditionMetricsCollector(ast.NodeVisitor): external_refs: int = 0 def collect(self, node: ast.AST) -> ConditionMetrics: - self.visit(node) + super().visit(node) return ConditionMetrics( bool_ops=self.bool_ops, compare_ops=self.compare_ops, @@ -25,16 +27,23 @@ def collect(self, node: ast.AST) -> ConditionMetrics: def visit_BoolOp(self, node: ast.BoolOp) -> None: self.bool_ops += max(0, len(node.values) - 1) - self.generic_visit(node) + super().generic_visit(node) def visit_Compare(self, node: ast.Compare) -> None: self.compare_ops += len(node.ops) - self.generic_visit(node) + super().generic_visit(node) def visit_Call(self, node: ast.Call) -> None: self.calls += 1 - self.generic_visit(node) + super().generic_visit(node) def visit_Name(self, node: ast.Name) -> None: - if isinstance(node.ctx, ast.Load) and node.id not in self.local_names: + if self._is_external_name(node): self.external_refs += 1 + + def _is_external_name(self, node: ast.Name) -> bool: + context = node.ctx + is_load = isinstance(context, ast.Load) + name = node.id + is_local = name in self.local_names + return is_load and not is_local diff --git a/codra/condition_metrics.py b/codra/condition/metrics.py similarity index 100% rename from codra/condition_metrics.py rename to codra/condition/metrics.py diff --git a/codra/csa/__init__.py b/codra/csa/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codra/csa/analyzer.py b/codra/csa/analyzer.py new file mode 100644 index 0000000..40bcd5c --- /dev/null +++ b/codra/csa/analyzer.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import ast +import builtins +import sys +from dataclasses import dataclass + +from ..function.symbol_collector import FunctionSymbolCollector +from ..function.symbol_usage import FunctionSymbolUsage +from ..module.symbol_collector import ModuleSymbolCollector +from ..unit.node import UnitNode +from ..unit.node_collector import UnitNodeCollector +from .result import CsaResult + + +@dataclass +class CsaAnalyzer: + def analyze_file(self, file_path: str) -> list[CsaResult]: + tree = self._parse_file(file_path) + module_symbols = self._collect_module_symbols(tree) + builtin_names = set(dir(builtins)) + stdlib_modules = set(sys.stdlib_module_names) + unit_nodes = self._collect_unit_nodes(tree, file_path) + return self._build_results( + unit_nodes, + module_symbols, + builtin_names, + stdlib_modules, + ) + + def _parse_file(self, file_path: str) -> ast.AST: + with open(file_path, "r", encoding="utf-8") as handle: + source = handle.read() + return ast.parse(source, filename=file_path) + + def _collect_module_symbols(self, tree: ast.AST) -> set[str]: + return ModuleSymbolCollector().collect(tree) + + def _collect_unit_nodes(self, tree: ast.AST, file_path: str) -> list[UnitNode]: + unit_collector = UnitNodeCollector(file_path=file_path) + unit_collector.visit(tree) + return unit_collector.units + + def _extract_external_symbols( + self, + usage: FunctionSymbolUsage, + module_symbols: set[str], + builtin_names: set[str], + stdlib_modules: set[str], + ) -> set[str]: + used_names = usage.used_names + local_names = usage.locals + global_symbols = used_names.intersection(module_symbols) - local_names + free_symbols = used_names - local_names - module_symbols + external_symbols = global_symbols.union(free_symbols) + return { + symbol + for symbol in external_symbols + if symbol not in builtin_names and symbol not in stdlib_modules + } + + def _build_results( + self, + unit_nodes: list[UnitNode], + module_symbols: set[str], + builtin_names: set[str], + stdlib_modules: set[str], + ) -> list[CsaResult]: + results: list[CsaResult] = [] + for unit_node in unit_nodes: + usage = FunctionSymbolCollector().collect(unit_node.node) + external_symbols = self._extract_external_symbols( + usage, module_symbols, builtin_names, stdlib_modules + ) + results.append( + CsaResult( + unit=unit_node.definition, + csa_main=len(external_symbols), + external_symbols=sorted(external_symbols), + self_fields_read=sorted(usage.self_fields_read), + ) + ) + return results diff --git a/codra/csa_result.py b/codra/csa/result.py similarity index 83% rename from codra/csa_result.py rename to codra/csa/result.py index 2e600e8..39fc8a7 100644 --- a/codra/csa_result.py +++ b/codra/csa/result.py @@ -2,7 +2,7 @@ from dataclasses import dataclass -from .unit_definition import UnitDefinition +from ..unit.definition import UnitDefinition @dataclass(frozen=True) diff --git a/codra/csa_analyzer.py b/codra/csa_analyzer.py deleted file mode 100644 index 0ed94a2..0000000 --- a/codra/csa_analyzer.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations - -import ast -from dataclasses import dataclass - -from .csa_result import CsaResult -from .function_symbol_collector import FunctionSymbolCollector -from .module_symbol_collector import ModuleSymbolCollector -from .unit_node_collector import UnitNodeCollector - - -@dataclass -class CsaAnalyzer: - def analyze_file(self, file_path: str) -> list[CsaResult]: - with open(file_path, "r", encoding="utf-8") as handle: - source = handle.read() - tree = ast.parse(source, filename=file_path) - module_symbols = ModuleSymbolCollector().collect(tree) - unit_collector = UnitNodeCollector(file_path=file_path) - unit_collector.visit(tree) - results: list[CsaResult] = [] - for unit_node in unit_collector.units: - usage = FunctionSymbolCollector().collect(unit_node.node) - used_names = usage.used_names - local_names = usage.locals - global_symbols = used_names.intersection(module_symbols) - local_names - free_symbols = used_names - local_names - module_symbols - external_symbols = global_symbols.union(free_symbols) - results.append( - CsaResult( - unit=unit_node.definition, - csa_main=len(external_symbols), - external_symbols=sorted(external_symbols), - self_fields_read=sorted(usage.self_fields_read), - ) - ) - return results diff --git a/codra/directory/__init__.py b/codra/directory/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codra/directory_scanner.py b/codra/directory/scanner.py similarity index 77% rename from codra/directory_scanner.py rename to codra/directory/scanner.py index 9b9413b..7c934a8 100644 --- a/codra/directory_scanner.py +++ b/codra/directory/scanner.py @@ -3,8 +3,8 @@ import os from dataclasses import dataclass, field -from .python_file_scanner import PythonFileScanner -from .unit_definition import UnitDefinition +from ..python.file_scanner import PythonFileScanner +from ..unit.definition import UnitDefinition @dataclass @@ -17,7 +17,8 @@ def scan(self, root_path: str) -> list[UnitDefinition]: dirnames.sort() filenames.sort() for filename in filenames: - if not filename.endswith(".py"): + is_python_file = filename.endswith(".py") + if not is_python_file: continue file_path = os.path.join(current_root, filename) units.extend(self.file_scanner.scan_file(file_path)) diff --git a/codra/file/__init__.py b/codra/file/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codra/file_path_collector.py b/codra/file/path_collector.py similarity index 83% rename from codra/file_path_collector.py rename to codra/file/path_collector.py index fae4211..007fe68 100644 --- a/codra/file_path_collector.py +++ b/codra/file/path_collector.py @@ -12,6 +12,7 @@ def collect(self, root_path: str) -> list[str]: dirnames.sort() filenames.sort() for filename in filenames: - if filename.endswith(".py"): + is_python_file = filename.endswith(".py") + if is_python_file: paths.append(os.path.join(current_root, filename)) return paths diff --git a/codra/function/__init__.py b/codra/function/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codra/function_definition_collector.py b/codra/function/definition_collector.py similarity index 73% rename from codra/function_definition_collector.py rename to codra/function/definition_collector.py index 875f066..e2c7958 100644 --- a/codra/function_definition_collector.py +++ b/codra/function/definition_collector.py @@ -1,32 +1,34 @@ from __future__ import annotations import ast +from ast import NodeVisitor from dataclasses import dataclass, field @dataclass -class FunctionDefinitionCollector(ast.NodeVisitor): +class FunctionDefinitionCollector(NodeVisitor): + """AST visitor collecting function definitions via NodeVisitor.""" names: set[str] = field(default_factory=set) class_stack: list[str] = field(default_factory=list) def collect(self, tree: ast.AST) -> set[str]: - self.visit(tree) + super().visit(tree) return set(self.names) def visit_ClassDef(self, node: ast.ClassDef) -> None: self.class_stack.append(node.name) for statement in node.body: - self.visit(statement) + super().visit(statement) self.class_stack.pop() def visit_FunctionDef(self, node: ast.FunctionDef) -> None: if not self.class_stack: self.names.add(node.name) for statement in node.body: - self.visit(statement) + super().visit(statement) def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: if not self.class_stack: self.names.add(node.name) for statement in node.body: - self.visit(statement) + super().visit(statement) diff --git a/codra/function_extractor.py b/codra/function/extractor.py similarity index 97% rename from codra/function_extractor.py rename to codra/function/extractor.py index 5ebf5df..6443fa5 100644 --- a/codra/function_extractor.py +++ b/codra/function/extractor.py @@ -3,7 +3,7 @@ import ast from dataclasses import dataclass, field -from .unit_definition import UnitDefinition +from ..unit.definition import UnitDefinition @dataclass diff --git a/codra/function/symbol_collector.py b/codra/function/symbol_collector.py new file mode 100644 index 0000000..4669872 --- /dev/null +++ b/codra/function/symbol_collector.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import ast +from ast import NodeVisitor +from dataclasses import dataclass, field + +from .symbol_usage import FunctionSymbolUsage + + +@dataclass +class FunctionSymbolCollector(NodeVisitor): + """AST visitor collecting symbol usage via NodeVisitor.""" + usage: FunctionSymbolUsage = field(default_factory=FunctionSymbolUsage) + + def collect(self, node: ast.AST) -> FunctionSymbolUsage: + self._add_arguments(node) + for statement in node.body: + super().visit(statement) + return self.usage + + def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + return None + + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: + return None + + def visit_ClassDef(self, node: ast.ClassDef) -> None: + return None + + def visit_Global(self, node: ast.Global) -> None: + self.usage.global_decls.update(node.names) + + def visit_Nonlocal(self, node: ast.Nonlocal) -> None: + self.usage.nonlocal_decls.update(node.names) + + def visit_Name(self, node: ast.Name) -> None: + context = node.ctx + name = node.id + is_load_context = self._is_load_context(context) + is_store_or_del_context = self._is_store_or_del_context(context) + if is_load_context: + self.usage.used_names.add(name) + elif is_store_or_del_context: + is_declared_nonlocal_or_global = self._is_declared_nonlocal_or_global(name) + if is_declared_nonlocal_or_global: + return + self.usage.locals.add(name) + + def visit_Attribute(self, node: ast.Attribute) -> None: + is_self_attribute_read = self._is_self_attribute_read(node) + if is_self_attribute_read: + self.usage.self_fields_read.add(node.attr) + super().generic_visit(node) + + def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None: + handler_name = node.name + if self._is_exception_name(handler_name): + self.usage.locals.add(handler_name) + elif handler_name is not None: + super().visit(handler_name) + for statement in node.body: + super().visit(statement) + + def _add_arguments(self, node: ast.AST) -> None: + arguments = node.args + for arg in arguments.posonlyargs: + self.usage.locals.add(arg.arg) + for arg in arguments.args: + self.usage.locals.add(arg.arg) + for arg in arguments.kwonlyargs: + self.usage.locals.add(arg.arg) + if arguments.vararg is not None: + self.usage.locals.add(arguments.vararg.arg) + if arguments.kwarg is not None: + self.usage.locals.add(arguments.kwarg.arg) + + def _is_load_context(self, context: ast.expr_context) -> bool: + return isinstance(context, ast.Load) + + def _is_store_or_del_context(self, context: ast.expr_context) -> bool: + return isinstance(context, (ast.Store, ast.Del)) + + def _is_declared_nonlocal_or_global(self, name: str) -> bool: + is_global = name in self.usage.global_decls + is_nonlocal = name in self.usage.nonlocal_decls + return is_global or is_nonlocal + + def _is_self_attribute_read(self, node: ast.Attribute) -> bool: + context = node.ctx + is_load_context = self._is_load_context(context) + if not is_load_context: + return False + value = node.value + return isinstance(value, ast.Name) and value.id == "self" + + def _is_exception_name(self, name: object) -> bool: + return isinstance(name, str) diff --git a/codra/function_symbol_usage.py b/codra/function/symbol_usage.py similarity index 100% rename from codra/function_symbol_usage.py rename to codra/function/symbol_usage.py diff --git a/codra/function_symbol_collector.py b/codra/function_symbol_collector.py deleted file mode 100644 index f765130..0000000 --- a/codra/function_symbol_collector.py +++ /dev/null @@ -1,69 +0,0 @@ -from __future__ import annotations - -import ast -from dataclasses import dataclass, field - -from .function_symbol_usage import FunctionSymbolUsage - - -@dataclass -class FunctionSymbolCollector(ast.NodeVisitor): - usage: FunctionSymbolUsage = field(default_factory=FunctionSymbolUsage) - - def collect(self, node: ast.AST) -> FunctionSymbolUsage: - self._add_arguments(node) - for statement in node.body: - self.visit(statement) - return self.usage - - def visit_FunctionDef(self, node: ast.FunctionDef) -> None: - return None - - def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: - return None - - def visit_ClassDef(self, node: ast.ClassDef) -> None: - return None - - def visit_Global(self, node: ast.Global) -> None: - self.usage.global_decls.update(node.names) - - def visit_Nonlocal(self, node: ast.Nonlocal) -> None: - self.usage.nonlocal_decls.update(node.names) - - def visit_Name(self, node: ast.Name) -> None: - if isinstance(node.ctx, ast.Load): - self.usage.used_names.add(node.id) - elif isinstance(node.ctx, (ast.Store, ast.Del)): - if node.id in self.usage.global_decls: - return - if node.id in self.usage.nonlocal_decls: - return - self.usage.locals.add(node.id) - - def visit_Attribute(self, node: ast.Attribute) -> None: - if isinstance(node.ctx, ast.Load): - if isinstance(node.value, ast.Name) and node.value.id == "self": - self.usage.self_fields_read.add(node.attr) - self.generic_visit(node) - - def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None: - if isinstance(node.name, str): - self.usage.locals.add(node.name) - elif node.name is not None: - self.visit(node.name) - for statement in node.body: - self.visit(statement) - - def _add_arguments(self, node: ast.AST) -> None: - arguments = node.args - for arg in arguments.posonlyargs: - self.usage.locals.add(arg.arg) - for arg in arguments.args: - self.usage.locals.add(arg.arg) - for arg in arguments.kwonlyargs: - self.usage.locals.add(arg.arg) - if arguments.vararg is not None: - self.usage.locals.add(arguments.vararg.arg) - if arguments.kwarg is not None: - self.usage.locals.add(arguments.kwarg.arg) diff --git a/codra/if_/__init__.py b/codra/if_/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codra/if_collector.py b/codra/if_/collector.py similarity index 75% rename from codra/if_collector.py rename to codra/if_/collector.py index 629bc31..f32748c 100644 --- a/codra/if_collector.py +++ b/codra/if_/collector.py @@ -1,16 +1,18 @@ from __future__ import annotations import ast +from ast import NodeVisitor from dataclasses import dataclass, field @dataclass -class IfCollector(ast.NodeVisitor): +class IfCollector(NodeVisitor): + """AST visitor collecting if statements via NodeVisitor.""" nodes: list[ast.If] = field(default_factory=list) def collect(self, node: ast.AST) -> list[ast.If]: for statement in node.body: - self.visit(statement) + super().visit(statement) return list(self.nodes) def visit_FunctionDef(self, node: ast.FunctionDef) -> None: @@ -24,4 +26,4 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: def visit_If(self, node: ast.If) -> None: self.nodes.append(node) - self.generic_visit(node) + super().generic_visit(node) diff --git a/codra/indirection/__init__.py b/codra/indirection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codra/indirection/analyzer.py b/codra/indirection/analyzer.py new file mode 100644 index 0000000..9d3ab47 --- /dev/null +++ b/codra/indirection/analyzer.py @@ -0,0 +1,296 @@ +from __future__ import annotations + +import ast +import builtins +import sys +from dataclasses import dataclass +from typing import Iterable, Mapping + +from ..alias.collector import AliasCollector +from ..call.collector import CallCollector +from ..function.definition_collector import FunctionDefinitionCollector +from ..module.symbol_collector import ModuleSymbolCollector +from ..unit.node import UnitNode +from ..unit.node_collector import UnitNodeCollector +from .result import IndirectionResult + + +def resolve_alias(name: str, aliases: Mapping[str, str]) -> str: + seen: set[str] = set() + current = name + while current in aliases and current not in seen: + seen.add(current) + current = aliases[current] + return current + + +def collect_calls(node: ast.AST) -> list[str]: + return CallCollector().collect(node) + + +def collect_calls_by_unit(unit_nodes: Iterable[UnitNode]) -> dict[str, list[str]]: + return { + unit.definition.qualified_id: collect_calls(unit.node) + for unit in unit_nodes + } + + +@dataclass +class IndirectionAnalyzer: + def analyze_file(self, file_path: str) -> list[IndirectionResult]: + tree = self._parse_file(file_path) + function_names = self._collect_function_definitions(tree) + class_methods = self._collect_class_methods(tree) + aliases = self._collect_aliases(tree) + module_symbols = ModuleSymbolCollector().collect(tree) + builtin_names = self._collect_builtin_names() + stdlib_modules = self._collect_stdlib_modules() + unit_nodes, call_map = self._collect_unit_calls(file_path, tree) + alias_cache: dict[str, str] = {} + call_graph = self._collect_call_graph( + function_names, class_methods, aliases, call_map, alias_cache + ) + return self._build_results( + unit_nodes=unit_nodes, + call_map=call_map, + class_methods=class_methods, + aliases=aliases, + function_names=function_names, + module_symbols=module_symbols, + builtin_names=builtin_names, + stdlib_modules=stdlib_modules, + alias_cache=alias_cache, + call_graph=call_graph, + ) + + def _parse_file(self, file_path: str) -> ast.AST: + with open(file_path, "r", encoding="utf-8") as handle: + source = handle.read() + return ast.parse(source, filename=file_path) + + def _collect_function_definitions(self, tree: ast.AST) -> set[str]: + return FunctionDefinitionCollector().collect(tree) + + def _collect_aliases(self, tree: ast.AST) -> dict[str, str]: + return AliasCollector().collect(tree) + + def _collect_class_methods(self, tree: ast.AST) -> dict[str, set[str]]: + class_methods: dict[str, set[str]] = {} + for node in tree.body: + if isinstance(node, ast.ClassDef): + methods = { + statement.name + for statement in node.body + if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)) + } + class_methods[node.name] = methods + return class_methods + + def _collect_call_graph( + self, + function_names: set[str], + class_methods: dict[str, set[str]], + aliases: dict[str, str], + call_map: dict[str, list[str]], + alias_cache: dict[str, str], + ) -> dict[str, list[str]]: + call_graph: dict[str, list[str]] = {name: [] for name in function_names} + for class_name, methods in class_methods.items(): + for method_name in methods: + call_graph[f"{class_name}.{method_name}"] = [] + for function_name in function_names: + call_names = call_map.get(function_name, []) + for name in call_names: + resolved = self._resolve_alias(name, aliases, alias_cache) + if resolved in function_names: + call_graph[function_name].append(resolved) + for class_name, methods in class_methods.items(): + for method_name in methods: + qualified_name = f"{class_name}.{method_name}" + call_names = call_map.get(qualified_name, []) + for name in call_names: + if name.startswith("self."): + callee_name = name.split(".", 1)[1] + if callee_name in methods: + call_graph[qualified_name].append( + f"{class_name}.{callee_name}" + ) + else: + resolved = self._resolve_alias(name, aliases, alias_cache) + if resolved in function_names: + call_graph[qualified_name].append(resolved) + return call_graph + + def _collect_builtin_names(self) -> set[str]: + return set(dir(builtins)) + + def _collect_stdlib_modules(self) -> set[str]: + return set(sys.stdlib_module_names) + + def _collect_unit_calls( + self, file_path: str, tree: ast.AST + ) -> tuple[list[UnitNode], dict[str, list[str]]]: + unit_collector = UnitNodeCollector(file_path=file_path) + unit_collector.visit(tree) + call_map = collect_calls_by_unit(unit_collector.units) + return unit_collector.units, call_map + + def _build_results( + self, + unit_nodes: list[UnitNode], + call_map: dict[str, list[str]], + class_methods: dict[str, set[str]], + aliases: dict[str, str], + function_names: set[str], + module_symbols: set[str], + builtin_names: set[str], + stdlib_modules: set[str], + alias_cache: dict[str, str], + call_graph: dict[str, list[str]], + ) -> list[IndirectionResult]: + depth_cache: dict[str, int] = {} + results: list[IndirectionResult] = [] + for unit_node in unit_nodes: + class_name, class_method_names = self._class_context( + unit_node, class_methods + ) + call_names = call_map.get(unit_node.definition.qualified_id, []) + resolved_calls, unresolved_calls = self._resolve_unit_calls( + call_names=call_names, + class_name=class_name, + class_method_names=class_method_names, + aliases=aliases, + alias_cache=alias_cache, + function_names=function_names, + module_symbols=module_symbols, + builtin_names=builtin_names, + stdlib_modules=stdlib_modules, + ) + id_max, id_avg = self._calculate_indirection_depths( + resolved_calls, call_graph, depth_cache + ) + results.append( + IndirectionResult( + unit=unit_node.definition, + id_max=id_max, + id_avg=id_avg, + unresolved_calls=sorted(unresolved_calls), + ) + ) + return results + + def _class_context( + self, unit_node: UnitNode, class_methods: dict[str, set[str]] + ) -> tuple[str | None, set[str]]: + if unit_node.definition.kind != "method": + return None, set() + class_name = unit_node.definition.qualified_id.split(".", 1)[0] + return class_name, class_methods.get(class_name, set()) + + def _resolve_unit_calls( + self, + call_names: list[str], + class_name: str | None, + class_method_names: set[str], + aliases: dict[str, str], + alias_cache: dict[str, str], + function_names: set[str], + module_symbols: set[str], + builtin_names: set[str], + stdlib_modules: set[str], + ) -> tuple[list[str], set[str]]: + resolved_calls: list[str] = [] + unresolved_calls: set[str] = set() + for name in call_names: + resolved_self = self._resolve_self_call( + name, class_name, class_method_names + ) + if resolved_self: + resolved_calls.append(resolved_self) + continue + if name.startswith("self."): + continue + resolved = self._resolve_alias(name, aliases, alias_cache) + if resolved in function_names: + resolved_calls.append(resolved) + continue + if self._is_ignorable_call( + resolved, module_symbols, builtin_names, stdlib_modules + ): + continue + unresolved_calls.add(name) + return resolved_calls, unresolved_calls + + def _resolve_self_call( + self, + name: str, + class_name: str | None, + class_method_names: set[str], + ) -> str | None: + if not name.startswith("self."): + return None + if not class_name: + return None + method_name = name.split(".", 1)[1] + if method_name not in class_method_names: + return None + return f"{class_name}.{method_name}" + + def _is_ignorable_call( + self, + resolved: str, + module_symbols: set[str], + builtin_names: set[str], + stdlib_modules: set[str], + ) -> bool: + return ( + resolved in module_symbols + or resolved in builtin_names + or resolved in stdlib_modules + ) + + def _resolve_alias( + self, name: str, aliases: dict[str, str], alias_cache: dict[str, str] + ) -> str: + if name in alias_cache: + return alias_cache[name] + resolved = resolve_alias(name, aliases) + alias_cache[name] = resolved + return resolved + + def _depth( + self, + name: str, + call_graph: dict[str, list[str]], + depth_cache: dict[str, int], + ) -> int: + if name in depth_cache: + return depth_cache[name] + if name not in call_graph: + depth_cache[name] = 0 + return 0 + if not call_graph[name]: + depth_cache[name] = 0 + return 0 + depth_cache[name] = -1 + depth = 0 + for callee in call_graph[name]: + if depth_cache.get(callee) == -1: + continue + depth = max(depth, 1 + self._depth(callee, call_graph, depth_cache)) + depth_cache[name] = depth + return depth + + def _calculate_indirection_depths( + self, + resolved_calls: list[str], + call_graph: dict[str, list[str]], + depth_cache: dict[str, int], + ) -> tuple[int, float]: + call_depths = [ + 1 + self._depth(call_name, call_graph, depth_cache) + for call_name in resolved_calls + ] + if not call_depths: + return 0, 0.0 + return max(call_depths), sum(call_depths) / len(call_depths) diff --git a/codra/indirection_result.py b/codra/indirection/result.py similarity index 82% rename from codra/indirection_result.py rename to codra/indirection/result.py index 5deaf47..4200b75 100644 --- a/codra/indirection_result.py +++ b/codra/indirection/result.py @@ -2,7 +2,7 @@ from dataclasses import dataclass -from .unit_definition import UnitDefinition +from ..unit.definition import UnitDefinition @dataclass(frozen=True) diff --git a/codra/indirection_analyzer.py b/codra/indirection_analyzer.py deleted file mode 100644 index 66974b0..0000000 --- a/codra/indirection_analyzer.py +++ /dev/null @@ -1,102 +0,0 @@ -from __future__ import annotations - -import ast -from dataclasses import dataclass - -from .alias_collector import AliasCollector -from .call_collector import CallCollector -from .function_definition_collector import FunctionDefinitionCollector -from .indirection_result import IndirectionResult -from .unit_node_collector import UnitNodeCollector - - -@dataclass -class IndirectionAnalyzer: - def analyze_file(self, file_path: str) -> list[IndirectionResult]: - with open(file_path, "r", encoding="utf-8") as handle: - source = handle.read() - tree = ast.parse(source, filename=file_path) - function_names = FunctionDefinitionCollector().collect(tree) - aliases = AliasCollector().collect(tree) - call_graph = self._build_call_graph(tree, function_names, aliases) - depth_cache: dict[str, int] = {} - results: list[IndirectionResult] = [] - unit_collector = UnitNodeCollector(file_path=file_path) - unit_collector.visit(tree) - for unit_node in unit_collector.units: - call_names = CallCollector().collect(unit_node.node) - resolved_calls: list[str] = [] - unresolved_calls: set[str] = set() - for name in call_names: - resolved = self._resolve_alias(name, aliases) - if resolved in function_names: - resolved_calls.append(resolved) - else: - unresolved_calls.add(name) - call_depths = [ - 1 + self._depth(call_name, call_graph, depth_cache) - for call_name in resolved_calls - ] - if call_depths: - id_max = max(call_depths) - id_avg = sum(call_depths) / len(call_depths) - else: - id_max = 0 - id_avg = 0.0 - results.append( - IndirectionResult( - unit=unit_node.definition, - id_max=id_max, - id_avg=id_avg, - unresolved_calls=sorted(unresolved_calls), - ) - ) - return results - - def _build_call_graph( - self, - tree: ast.AST, - function_names: set[str], - aliases: dict[str, str], - ) -> dict[str, list[str]]: - call_graph: dict[str, list[str]] = {name: [] for name in function_names} - for node in tree.body: - if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): - if node.name in function_names: - call_names = CallCollector().collect(node) - for name in call_names: - resolved = self._resolve_alias(name, aliases) - if resolved in function_names: - call_graph[node.name].append(resolved) - return call_graph - - def _resolve_alias(self, name: str, aliases: dict[str, str]) -> str: - seen: set[str] = set() - current = name - while current in aliases and current not in seen: - seen.add(current) - current = aliases[current] - return current - - def _depth( - self, - name: str, - call_graph: dict[str, list[str]], - depth_cache: dict[str, int], - ) -> int: - if name in depth_cache: - return depth_cache[name] - if name not in call_graph: - depth_cache[name] = 0 - return 0 - if not call_graph[name]: - depth_cache[name] = 0 - return 0 - depth_cache[name] = -1 - depth = 0 - for callee in call_graph[name]: - if depth_cache.get(callee) == -1: - continue - depth = max(depth, 1 + self._depth(callee, call_graph, depth_cache)) - depth_cache[name] = depth - return depth diff --git a/codra/module/__init__.py b/codra/module/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codra/module_symbol_collector.py b/codra/module/symbol_collector.py similarity index 73% rename from codra/module_symbol_collector.py rename to codra/module/symbol_collector.py index 5133a9f..5f5336d 100644 --- a/codra/module_symbol_collector.py +++ b/codra/module/symbol_collector.py @@ -1,15 +1,17 @@ from __future__ import annotations import ast +from ast import NodeVisitor from dataclasses import dataclass, field @dataclass -class ModuleSymbolCollector(ast.NodeVisitor): +class ModuleSymbolCollector(NodeVisitor): + """AST visitor collecting module symbols via NodeVisitor.""" symbols: set[str] = field(default_factory=set) def collect(self, tree: ast.AST) -> set[str]: - self.visit(tree) + super().visit(tree) return set(self.symbols) def visit_FunctionDef(self, node: ast.FunctionDef) -> None: @@ -34,46 +36,49 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None: def visit_Assign(self, node: ast.Assign) -> None: for target in node.targets: self.symbols.update(self._extract_target_names(target)) - self.generic_visit(node) + super().generic_visit(node) def visit_AnnAssign(self, node: ast.AnnAssign) -> None: self.symbols.update(self._extract_target_names(node.target)) - self.generic_visit(node) + super().generic_visit(node) def visit_AugAssign(self, node: ast.AugAssign) -> None: self.symbols.update(self._extract_target_names(node.target)) - self.generic_visit(node) + super().generic_visit(node) def visit_For(self, node: ast.For) -> None: self.symbols.update(self._extract_target_names(node.target)) - self.generic_visit(node) + super().generic_visit(node) def visit_AsyncFor(self, node: ast.AsyncFor) -> None: self.symbols.update(self._extract_target_names(node.target)) - self.generic_visit(node) + super().generic_visit(node) def visit_With(self, node: ast.With) -> None: for item in node.items: if item.optional_vars is not None: self.symbols.update(self._extract_target_names(item.optional_vars)) - self.generic_visit(node) + super().generic_visit(node) def visit_AsyncWith(self, node: ast.AsyncWith) -> None: for item in node.items: if item.optional_vars is not None: self.symbols.update(self._extract_target_names(item.optional_vars)) - self.generic_visit(node) + super().generic_visit(node) def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None: - if isinstance(node.name, str): - self.symbols.add(node.name) - elif node.name is not None: - self.symbols.update(self._extract_target_names(node.name)) - self.generic_visit(node) + handler_name = node.name + is_handler_name = isinstance(handler_name, str) + if is_handler_name: + self.symbols.add(handler_name) + elif handler_name is not None: + self.symbols.update(self._extract_target_names(handler_name)) + super().generic_visit(node) def _extract_target_names(self, node: ast.AST) -> set[str]: names: set[str] = set() for target in ast.walk(node): - if isinstance(target, ast.Name): + is_name = isinstance(target, ast.Name) + if is_name: names.add(target.id) return names diff --git a/codra/python/__init__.py b/codra/python/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codra/python_file_scanner.py b/codra/python/file_scanner.py similarity index 82% rename from codra/python_file_scanner.py rename to codra/python/file_scanner.py index a836078..c276682 100644 --- a/codra/python_file_scanner.py +++ b/codra/python/file_scanner.py @@ -3,8 +3,8 @@ import ast from dataclasses import dataclass -from .function_extractor import FunctionExtractor -from .unit_definition import UnitDefinition +from ..function.extractor import FunctionExtractor +from ..unit.definition import UnitDefinition @dataclass diff --git a/codra/report/__init__.py b/codra/report/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codra/report_builder.py b/codra/report/builder.py similarity index 51% rename from codra/report_builder.py rename to codra/report/builder.py index d3f1832..02216e1 100644 --- a/codra/report_builder.py +++ b/codra/report/builder.py @@ -1,19 +1,26 @@ from __future__ import annotations +import logging from dataclasses import dataclass -from .bps_analyzer import BpsAnalyzer -from .csa_analyzer import CsaAnalyzer -from .file_path_collector import FilePathCollector -from .file_report import FileReport -from .indirection_analyzer import IndirectionAnalyzer -from .report import Report -from .report_summary import ReportSummary -from .threshold_config import ThresholdConfig -from .unit_definition import UnitDefinition -from .unit_key import UnitKey -from .unit_metrics import UnitMetrics -from .unit_report import UnitReport +from ..bps.analyzer import BpsAnalyzer +from ..bps.result import BpsResult +from ..csa.analyzer import CsaAnalyzer +from ..csa.result import CsaResult +from ..file.path_collector import FilePathCollector +from ..indirection.analyzer import IndirectionAnalyzer +from ..indirection.result import IndirectionResult +from ..threshold.config import ThresholdConfig +from ..unit.definition import UnitDefinition +from ..unit.key import UnitKey +from ..unit.metrics import UnitMetrics +from ..unit.overview import UnitReport +from .file import FileReport +from .model import Report +from .summary import ReportSummary + + +logger = logging.getLogger(__name__) @dataclass @@ -24,30 +31,10 @@ class ReportBuilder: path_collector: FilePathCollector def build(self, root_path: str) -> Report: + logger.info("Collecting files from %s", root_path) file_paths = self.path_collector.collect(root_path) - files: list[FileReport] = [] - total_units = 0 - for file_path in file_paths: - csa_results = self.csa_analyzer.analyze_file(file_path) - indirection_results = self.indirection_analyzer.analyze_file(file_path) - bps_results = self.bps_analyzer.analyze_file(file_path) - csa_map = {self._key(result.unit): result for result in csa_results} - indirection_map = { - self._key(result.unit): result for result in indirection_results - } - bps_map = {self._key(result.unit): result for result in bps_results} - keys = sorted( - {**csa_map, **indirection_map, **bps_map}.keys(), - key=self._sort_key, - ) - units: list[UnitReport] = [] - for key in keys: - unit = self._resolve_unit(key, csa_map, indirection_map, bps_map) - metrics = self._resolve_metrics(key, csa_map, indirection_map, bps_map) - units.append(UnitReport(unit=unit, metrics=metrics)) - files.append(FileReport(file_path=file_path, units=units)) - total_units += len(units) - summary = ReportSummary(total_files=len(files), total_units=total_units) + files = self._build_file_reports(file_paths) + summary = self._build_summary(files) return Report( schema_version="1.0", language="python", @@ -56,6 +43,9 @@ def build(self, root_path: str) -> Report: ) def check_thresholds(self, report: Report, thresholds: ThresholdConfig) -> bool: + return self._apply_thresholds(report, thresholds) + + def _apply_thresholds(self, report: Report, thresholds: ThresholdConfig) -> bool: for file_report in report.files: for unit in file_report.units: metrics = unit.metrics @@ -82,6 +72,54 @@ def _key(self, unit: UnitDefinition) -> UnitKey: def _sort_key(self, key: UnitKey) -> tuple[str, int, int, str]: return (key.qualified_id, key.start_line, key.end_line, key.kind) + def _collect_file_analysis( + self, file_path: str + ) -> tuple[list[CsaResult], list[IndirectionResult], list[BpsResult]]: + csa_results = self.csa_analyzer.analyze_file(file_path) + indirection_results = self.indirection_analyzer.analyze_file(file_path) + bps_results = self.bps_analyzer.analyze_file(file_path) + return csa_results, indirection_results, bps_results + + def _build_file_reports(self, file_paths: list[str]) -> list[FileReport]: + files: list[FileReport] = [] + for file_path in file_paths: + logger.info("Analyzing %s", file_path) + csa_results, indirection_results, bps_results = self._collect_file_analysis( + file_path + ) + file_report, _ = self._build_file_report( + file_path, csa_results, indirection_results, bps_results + ) + files.append(file_report) + return files + + def _build_file_report( + self, + file_path: str, + csa_results: list[CsaResult], + indirection_results: list[IndirectionResult], + bps_results: list[BpsResult], + ) -> tuple[FileReport, int]: + csa_map = {self._key(result.unit): result for result in csa_results} + indirection_map = { + self._key(result.unit): result for result in indirection_results + } + bps_map = {self._key(result.unit): result for result in bps_results} + keys = sorted( + {**csa_map, **indirection_map, **bps_map}.keys(), + key=self._sort_key, + ) + units: list[UnitReport] = [] + for key in keys: + unit = self._resolve_unit(key, csa_map, indirection_map, bps_map) + metrics = self._resolve_metrics(key, csa_map, indirection_map, bps_map) + units.append(UnitReport(unit=unit, metrics=metrics)) + return FileReport(file_path=file_path, units=units), len(units) + + def _build_summary(self, files: list[FileReport]) -> ReportSummary: + total_units = sum(len(file_report.units) for file_report in files) + return ReportSummary(total_files=len(files), total_units=total_units) + def _resolve_unit( self, key: UnitKey, diff --git a/codra/file_report.py b/codra/report/file.py similarity index 80% rename from codra/file_report.py rename to codra/report/file.py index fc6717c..039496f 100644 --- a/codra/file_report.py +++ b/codra/report/file.py @@ -2,7 +2,7 @@ from dataclasses import dataclass -from .unit_report import UnitReport +from ..unit.overview import UnitReport @dataclass(frozen=True) diff --git a/codra/report.py b/codra/report/model.py similarity index 72% rename from codra/report.py rename to codra/report/model.py index d19d2a1..4c0701c 100644 --- a/codra/report.py +++ b/codra/report/model.py @@ -2,8 +2,8 @@ from dataclasses import dataclass -from .file_report import FileReport -from .report_summary import ReportSummary +from .file import FileReport +from .summary import ReportSummary @dataclass(frozen=True) diff --git a/codra/report_serializer.py b/codra/report/serializer.py similarity index 90% rename from codra/report_serializer.py rename to codra/report/serializer.py index 59d8673..ca458f3 100644 --- a/codra/report_serializer.py +++ b/codra/report/serializer.py @@ -4,7 +4,7 @@ from dataclasses import asdict from dataclasses import dataclass -from .report import Report +from .model import Report @dataclass diff --git a/codra/report_summary.py b/codra/report/summary.py similarity index 100% rename from codra/report_summary.py rename to codra/report/summary.py diff --git a/codra/threshold/__init__.py b/codra/threshold/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codra/threshold_config.py b/codra/threshold/config.py similarity index 100% rename from codra/threshold_config.py rename to codra/threshold/config.py diff --git a/codra/unit/__init__.py b/codra/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/codra/unit_definition.py b/codra/unit/definition.py similarity index 100% rename from codra/unit_definition.py rename to codra/unit/definition.py diff --git a/codra/unit_key.py b/codra/unit/key.py similarity index 100% rename from codra/unit_key.py rename to codra/unit/key.py diff --git a/codra/unit_metrics.py b/codra/unit/metrics.py similarity index 100% rename from codra/unit_metrics.py rename to codra/unit/metrics.py diff --git a/codra/unit_node.py b/codra/unit/node.py similarity index 79% rename from codra/unit_node.py rename to codra/unit/node.py index 0c38fef..b872c23 100644 --- a/codra/unit_node.py +++ b/codra/unit/node.py @@ -3,7 +3,7 @@ import ast from dataclasses import dataclass -from .unit_definition import UnitDefinition +from .definition import UnitDefinition @dataclass(frozen=True) diff --git a/codra/unit_node_collector.py b/codra/unit/node_collector.py similarity index 86% rename from codra/unit_node_collector.py rename to codra/unit/node_collector.py index 56e1b64..8aa1397 100644 --- a/codra/unit_node_collector.py +++ b/codra/unit/node_collector.py @@ -1,14 +1,16 @@ from __future__ import annotations import ast +from ast import NodeVisitor from dataclasses import dataclass, field -from .unit_definition import UnitDefinition -from .unit_node import UnitNode +from .definition import UnitDefinition +from .node import UnitNode @dataclass -class UnitNodeCollector(ast.NodeVisitor): +class UnitNodeCollector(NodeVisitor): + """AST visitor collecting unit nodes via NodeVisitor.""" file_path: str units: list[UnitNode] = field(default_factory=list) class_stack: list[str] = field(default_factory=list) @@ -16,7 +18,7 @@ class UnitNodeCollector(ast.NodeVisitor): def visit_ClassDef(self, node: ast.ClassDef) -> None: self.class_stack.append(node.name) - self.generic_visit(node) + super().generic_visit(node) self.class_stack.pop() def visit_FunctionDef(self, node: ast.FunctionDef) -> None: @@ -51,5 +53,5 @@ def _handle_function(self, node: ast.AST) -> None: ) self.units.append(UnitNode(definition=definition, node=node)) self.function_stack.append(name) - self.generic_visit(node) + super().generic_visit(node) self.function_stack.pop() diff --git a/codra/unit_report.py b/codra/unit/overview.py similarity index 66% rename from codra/unit_report.py rename to codra/unit/overview.py index 208e656..1143617 100644 --- a/codra/unit_report.py +++ b/codra/unit/overview.py @@ -2,8 +2,8 @@ from dataclasses import dataclass -from .unit_definition import UnitDefinition -from .unit_metrics import UnitMetrics +from .definition import UnitDefinition +from .metrics import UnitMetrics @dataclass(frozen=True) diff --git a/report.json b/report.json new file mode 100644 index 0000000..97398c1 --- /dev/null +++ b/report.json @@ -0,0 +1 @@ +{"files": [{"file_path": "/workspace/codra/setup.py", "units": []}, {"file_path": "/workspace/codra/codra/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/alias/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/alias/collector.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["aliases"], "unresolved_calls": []}, "unit": {"end_line": 16, "file_path": "/workspace/codra/codra/alias/collector.py", "kind": "method", "qualified_id": "AliasCollector.collect", "start_line": 14}}, {"metrics": {"bps": 0.26190476190476186, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["aliases", "depth"], "unresolved_calls": []}, "unit": {"end_line": 49, "file_path": "/workspace/codra/codra/alias/collector.py", "kind": "method", "qualified_id": "AliasCollector.visit_AnnAssign", "start_line": 42}}, {"metrics": {"bps": 0.26190476190476186, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["aliases", "depth"], "unresolved_calls": []}, "unit": {"end_line": 40, "file_path": "/workspace/codra/codra/alias/collector.py", "kind": "method", "qualified_id": "AliasCollector.visit_Assign", "start_line": 33}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 28, "file_path": "/workspace/codra/codra/alias/collector.py", "kind": "method", "qualified_id": "AliasCollector.visit_AsyncFunctionDef", "start_line": 27}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 31, "file_path": "/workspace/codra/codra/alias/collector.py", "kind": "method", "qualified_id": "AliasCollector.visit_ClassDef", "start_line": 30}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 25, "file_path": "/workspace/codra/codra/alias/collector.py", "kind": "method", "qualified_id": "AliasCollector.visit_FunctionDef", "start_line": 24}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 22, "file_path": "/workspace/codra/codra/alias/collector.py", "kind": "method", "qualified_id": "AliasCollector.visit_Module", "start_line": 18}}]}, {"file_path": "/workspace/codra/codra/bps/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/bps/analyzer.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["BpsResult"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 58, "file_path": "/workspace/codra/codra/bps/analyzer.py", "kind": "method", "qualified_id": "BpsAnalyzer._build_results", "start_line": 52}}, {"metrics": {"bps": 1.0, "csa_main": 3, "external_symbols": ["ConditionMetricsCollector", "FunctionSymbolCollector", "IfCollector"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 50, "file_path": "/workspace/codra/codra/bps/analyzer.py", "kind": "method", "qualified_id": "BpsAnalyzer._collect_bps", "start_line": 32}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["UnitNodeCollector"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 30, "file_path": "/workspace/codra/codra/bps/analyzer.py", "kind": "method", "qualified_id": "BpsAnalyzer._collect_units", "start_line": 27}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 25, "file_path": "/workspace/codra/codra/bps/analyzer.py", "kind": "method", "qualified_id": "BpsAnalyzer._parse_file", "start_line": 22}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_build_results", "_collect_bps", "_collect_units", "_parse_file"], "unresolved_calls": []}, "unit": {"end_line": 20, "file_path": "/workspace/codra/codra/bps/analyzer.py", "kind": "method", "qualified_id": "BpsAnalyzer.analyze_file", "start_line": 16}}]}, {"file_path": "/workspace/codra/codra/bps/result.py", "units": []}, {"file_path": "/workspace/codra/codra/call/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/call/collector.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["calls"], "unresolved_calls": []}, "unit": {"end_line": 16, "file_path": "/workspace/codra/codra/call/collector.py", "kind": "method", "qualified_id": "CallCollector.collect", "start_line": 13}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 22, "file_path": "/workspace/codra/codra/call/collector.py", "kind": "method", "qualified_id": "CallCollector.visit_AsyncFunctionDef", "start_line": 21}}, {"metrics": {"bps": 0.13227513227513227, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["calls"], "unresolved_calls": []}, "unit": {"end_line": 33, "file_path": "/workspace/codra/codra/call/collector.py", "kind": "method", "qualified_id": "CallCollector.visit_Call", "start_line": 27}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 25, "file_path": "/workspace/codra/codra/call/collector.py", "kind": "method", "qualified_id": "CallCollector.visit_ClassDef", "start_line": 24}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 19, "file_path": "/workspace/codra/codra/call/collector.py", "kind": "method", "qualified_id": "CallCollector.visit_FunctionDef", "start_line": 18}}]}, {"file_path": "/workspace/codra/codra/cli/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/cli/args.py", "units": []}, {"file_path": "/workspace/codra/codra/cli/dependencies.py", "units": []}, {"file_path": "/workspace/codra/codra/cli/main.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 7, "external_symbols": ["BpsAnalyzer", "CliDependencies", "CsaAnalyzer", "FilePathCollector", "IndirectionAnalyzer", "ReportBuilder", "ReportSerializer"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 63, "file_path": "/workspace/codra/codra/cli/main.py", "kind": "function", "qualified_id": "build_dependencies", "start_line": 56}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["ThresholdConfig"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 53, "file_path": "/workspace/codra/codra/cli/main.py", "kind": "function", "qualified_id": "build_thresholds", "start_line": 48}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 71, "file_path": "/workspace/codra/codra/cli/main.py", "kind": "function", "qualified_id": "configure_logging", "start_line": 66}}, {"metrics": {"bps": 1.0, "csa_main": 4, "external_symbols": ["build_dependencies", "configure_logging", "parse_args", "run_cli"], "id_avg": 1.25, "id_max": 2, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 93, "file_path": "/workspace/codra/codra/cli/main.py", "kind": "function", "qualified_id": "main", "start_line": 90}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["CliArgs"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 45, "file_path": "/workspace/codra/codra/cli/main.py", "kind": "function", "qualified_id": "parse_args", "start_line": 21}}, {"metrics": {"bps": 1.0, "csa_main": 2, "external_symbols": ["build_thresholds", "logger"], "id_avg": 1.0, "id_max": 1, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 87, "file_path": "/workspace/codra/codra/cli/main.py", "kind": "function", "qualified_id": "run_cli", "start_line": 74}}]}, {"file_path": "/workspace/codra/codra/condition/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/condition/collector.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["local_names"], "unresolved_calls": []}, "unit": {"end_line": 49, "file_path": "/workspace/codra/codra/condition/collector.py", "kind": "method", "qualified_id": "ConditionMetricsCollector._is_external_name", "start_line": 44}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["ConditionMetrics"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["bool_ops", "calls", "compare_ops", "external_refs"], "unresolved_calls": []}, "unit": {"end_line": 26, "file_path": "/workspace/codra/codra/condition/collector.py", "kind": "method", "qualified_id": "ConditionMetricsCollector.collect", "start_line": 19}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 30, "file_path": "/workspace/codra/codra/condition/collector.py", "kind": "method", "qualified_id": "ConditionMetricsCollector.visit_BoolOp", "start_line": 28}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 38, "file_path": "/workspace/codra/codra/condition/collector.py", "kind": "method", "qualified_id": "ConditionMetricsCollector.visit_Call", "start_line": 36}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 34, "file_path": "/workspace/codra/codra/condition/collector.py", "kind": "method", "qualified_id": "ConditionMetricsCollector.visit_Compare", "start_line": 32}}, {"metrics": {"bps": 0.3333333333333333, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_is_external_name"], "unresolved_calls": []}, "unit": {"end_line": 42, "file_path": "/workspace/codra/codra/condition/collector.py", "kind": "method", "qualified_id": "ConditionMetricsCollector.visit_Name", "start_line": 40}}]}, {"file_path": "/workspace/codra/codra/condition/metrics.py", "units": []}, {"file_path": "/workspace/codra/codra/csa/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/csa/analyzer.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 2, "external_symbols": ["CsaResult", "FunctionSymbolCollector"], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_extract_external_symbols"], "unresolved_calls": []}, "unit": {"end_line": 83, "file_path": "/workspace/codra/codra/csa/analyzer.py", "kind": "method", "qualified_id": "CsaAnalyzer._build_results", "start_line": 62}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["ModuleSymbolCollector"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 37, "file_path": "/workspace/codra/codra/csa/analyzer.py", "kind": "method", "qualified_id": "CsaAnalyzer._collect_module_symbols", "start_line": 36}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["UnitNodeCollector"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 42, "file_path": "/workspace/codra/codra/csa/analyzer.py", "kind": "method", "qualified_id": "CsaAnalyzer._collect_unit_nodes", "start_line": 39}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 60, "file_path": "/workspace/codra/codra/csa/analyzer.py", "kind": "method", "qualified_id": "CsaAnalyzer._extract_external_symbols", "start_line": 44}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 34, "file_path": "/workspace/codra/codra/csa/analyzer.py", "kind": "method", "qualified_id": "CsaAnalyzer._parse_file", "start_line": 31}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 1.25, "id_max": 2, "self_fields_read": ["_build_results", "_collect_module_symbols", "_collect_unit_nodes", "_parse_file"], "unresolved_calls": []}, "unit": {"end_line": 29, "file_path": "/workspace/codra/codra/csa/analyzer.py", "kind": "method", "qualified_id": "CsaAnalyzer.analyze_file", "start_line": 18}}]}, {"file_path": "/workspace/codra/codra/csa/result.py", "units": []}, {"file_path": "/workspace/codra/codra/directory/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/directory/scanner.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["UnitDefinition"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["file_scanner"], "unresolved_calls": []}, "unit": {"end_line": 25, "file_path": "/workspace/codra/codra/directory/scanner.py", "kind": "method", "qualified_id": "DirectoryScanner.scan", "start_line": 14}}]}, {"file_path": "/workspace/codra/codra/file/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/file/path_collector.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 18, "file_path": "/workspace/codra/codra/file/path_collector.py", "kind": "method", "qualified_id": "FilePathCollector.collect", "start_line": 9}}]}, {"file_path": "/workspace/codra/codra/function/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/function/definition_collector.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["names"], "unresolved_calls": []}, "unit": {"end_line": 16, "file_path": "/workspace/codra/codra/function/definition_collector.py", "kind": "method", "qualified_id": "FunctionDefinitionCollector.collect", "start_line": 14}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["class_stack", "names"], "unresolved_calls": []}, "unit": {"end_line": 34, "file_path": "/workspace/codra/codra/function/definition_collector.py", "kind": "method", "qualified_id": "FunctionDefinitionCollector.visit_AsyncFunctionDef", "start_line": 30}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["class_stack"], "unresolved_calls": []}, "unit": {"end_line": 22, "file_path": "/workspace/codra/codra/function/definition_collector.py", "kind": "method", "qualified_id": "FunctionDefinitionCollector.visit_ClassDef", "start_line": 18}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["class_stack", "names"], "unresolved_calls": []}, "unit": {"end_line": 28, "file_path": "/workspace/codra/codra/function/definition_collector.py", "kind": "method", "qualified_id": "FunctionDefinitionCollector.visit_FunctionDef", "start_line": 24}}]}, {"file_path": "/workspace/codra/codra/function/extractor.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["UnitDefinition"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["class_stack", "file_path", "function_stack", "generic_visit", "units"], "unresolved_calls": []}, "unit": {"end_line": 55, "file_path": "/workspace/codra/codra/function/extractor.py", "kind": "method", "qualified_id": "FunctionExtractor._handle_function", "start_line": 27}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_handle_function"], "unresolved_calls": []}, "unit": {"end_line": 25, "file_path": "/workspace/codra/codra/function/extractor.py", "kind": "method", "qualified_id": "FunctionExtractor.visit_AsyncFunctionDef", "start_line": 24}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["class_stack", "generic_visit"], "unresolved_calls": []}, "unit": {"end_line": 19, "file_path": "/workspace/codra/codra/function/extractor.py", "kind": "method", "qualified_id": "FunctionExtractor.visit_ClassDef", "start_line": 16}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_handle_function"], "unresolved_calls": []}, "unit": {"end_line": 22, "file_path": "/workspace/codra/codra/function/extractor.py", "kind": "method", "qualified_id": "FunctionExtractor.visit_FunctionDef", "start_line": 21}}]}, {"file_path": "/workspace/codra/codra/function/symbol_collector.py", "units": [{"metrics": {"bps": 0.5, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["usage"], "unresolved_calls": []}, "unit": {"end_line": 75, "file_path": "/workspace/codra/codra/function/symbol_collector.py", "kind": "method", "qualified_id": "FunctionSymbolCollector._add_arguments", "start_line": 64}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["usage"], "unresolved_calls": []}, "unit": {"end_line": 86, "file_path": "/workspace/codra/codra/function/symbol_collector.py", "kind": "method", "qualified_id": "FunctionSymbolCollector._is_declared_nonlocal_or_global", "start_line": 83}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 97, "file_path": "/workspace/codra/codra/function/symbol_collector.py", "kind": "method", "qualified_id": "FunctionSymbolCollector._is_exception_name", "start_line": 96}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 78, "file_path": "/workspace/codra/codra/function/symbol_collector.py", "kind": "method", "qualified_id": "FunctionSymbolCollector._is_load_context", "start_line": 77}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_is_load_context"], "unresolved_calls": []}, "unit": {"end_line": 94, "file_path": "/workspace/codra/codra/function/symbol_collector.py", "kind": "method", "qualified_id": "FunctionSymbolCollector._is_self_attribute_read", "start_line": 88}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 81, "file_path": "/workspace/codra/codra/function/symbol_collector.py", "kind": "method", "qualified_id": "FunctionSymbolCollector._is_store_or_del_context", "start_line": 80}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_add_arguments", "usage"], "unresolved_calls": []}, "unit": {"end_line": 19, "file_path": "/workspace/codra/codra/function/symbol_collector.py", "kind": "method", "qualified_id": "FunctionSymbolCollector.collect", "start_line": 15}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 25, "file_path": "/workspace/codra/codra/function/symbol_collector.py", "kind": "method", "qualified_id": "FunctionSymbolCollector.visit_AsyncFunctionDef", "start_line": 24}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 2.0, "id_max": 2, "self_fields_read": ["_is_self_attribute_read", "usage"], "unresolved_calls": []}, "unit": {"end_line": 53, "file_path": "/workspace/codra/codra/function/symbol_collector.py", "kind": "method", "qualified_id": "FunctionSymbolCollector.visit_Attribute", "start_line": 49}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 28, "file_path": "/workspace/codra/codra/function/symbol_collector.py", "kind": "method", "qualified_id": "FunctionSymbolCollector.visit_ClassDef", "start_line": 27}}, {"metrics": {"bps": 0.41666666666666663, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_is_exception_name", "usage"], "unresolved_calls": []}, "unit": {"end_line": 62, "file_path": "/workspace/codra/codra/function/symbol_collector.py", "kind": "method", "qualified_id": "FunctionSymbolCollector.visit_ExceptHandler", "start_line": 55}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 22, "file_path": "/workspace/codra/codra/function/symbol_collector.py", "kind": "method", "qualified_id": "FunctionSymbolCollector.visit_FunctionDef", "start_line": 21}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["usage"], "unresolved_calls": []}, "unit": {"end_line": 31, "file_path": "/workspace/codra/codra/function/symbol_collector.py", "kind": "method", "qualified_id": "FunctionSymbolCollector.visit_Global", "start_line": 30}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_is_declared_nonlocal_or_global", "_is_load_context", "_is_store_or_del_context", "usage"], "unresolved_calls": []}, "unit": {"end_line": 47, "file_path": "/workspace/codra/codra/function/symbol_collector.py", "kind": "method", "qualified_id": "FunctionSymbolCollector.visit_Name", "start_line": 36}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["usage"], "unresolved_calls": []}, "unit": {"end_line": 34, "file_path": "/workspace/codra/codra/function/symbol_collector.py", "kind": "method", "qualified_id": "FunctionSymbolCollector.visit_Nonlocal", "start_line": 33}}]}, {"file_path": "/workspace/codra/codra/function/symbol_usage.py", "units": []}, {"file_path": "/workspace/codra/codra/if_/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/if_/collector.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["nodes"], "unresolved_calls": []}, "unit": {"end_line": 16, "file_path": "/workspace/codra/codra/if_/collector.py", "kind": "method", "qualified_id": "IfCollector.collect", "start_line": 13}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 22, "file_path": "/workspace/codra/codra/if_/collector.py", "kind": "method", "qualified_id": "IfCollector.visit_AsyncFunctionDef", "start_line": 21}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 25, "file_path": "/workspace/codra/codra/if_/collector.py", "kind": "method", "qualified_id": "IfCollector.visit_ClassDef", "start_line": 24}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 19, "file_path": "/workspace/codra/codra/if_/collector.py", "kind": "method", "qualified_id": "IfCollector.visit_FunctionDef", "start_line": 18}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["nodes"], "unresolved_calls": []}, "unit": {"end_line": 29, "file_path": "/workspace/codra/codra/if_/collector.py", "kind": "method", "qualified_id": "IfCollector.visit_If", "start_line": 27}}]}, {"file_path": "/workspace/codra/codra/indirection/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/indirection/analyzer.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["IndirectionResult"], "id_avg": 2.0, "id_max": 3, "self_fields_read": ["_calculate_indirection_depths", "_class_context", "_resolve_unit_calls"], "unresolved_calls": []}, "unit": {"end_line": 180, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer._build_results", "start_line": 138}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_depth"], "unresolved_calls": []}, "unit": {"end_line": 296, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer._calculate_indirection_depths", "start_line": 284}}, {"metrics": {"bps": 0.5, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 188, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer._class_context", "start_line": 182}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["AliasCollector"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 75, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer._collect_aliases", "start_line": 74}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 125, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer._collect_builtin_names", "start_line": 124}}, {"metrics": {"bps": 0.4583333333333333, "csa_main": 0, "external_symbols": [], "id_avg": 2.0, "id_max": 2, "self_fields_read": ["_resolve_alias"], "unresolved_calls": []}, "unit": {"end_line": 122, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer._collect_call_graph", "start_line": 89}}, {"metrics": {"bps": 0.14285714285714285, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 87, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer._collect_class_methods", "start_line": 77}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["FunctionDefinitionCollector"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 72, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer._collect_function_definitions", "start_line": 71}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 128, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer._collect_stdlib_modules", "start_line": 127}}, {"metrics": {"bps": 1.0, "csa_main": 2, "external_symbols": ["UnitNodeCollector", "collect_calls_by_unit"], "id_avg": 2.0, "id_max": 2, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 136, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer._collect_unit_calls", "start_line": 130}}, {"metrics": {"bps": 0.5625, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_depth"], "unresolved_calls": []}, "unit": {"end_line": 282, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer._depth", "start_line": 261}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 250, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer._is_ignorable_call", "start_line": 239}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 69, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer._parse_file", "start_line": 66}}, {"metrics": {"bps": 0.5, "csa_main": 1, "external_symbols": ["resolve_alias"], "id_avg": 1.0, "id_max": 1, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 259, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer._resolve_alias", "start_line": 252}}, {"metrics": {"bps": 0.611111111111111, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 237, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer._resolve_self_call", "start_line": 224}}, {"metrics": {"bps": 0.5416666666666666, "csa_main": 0, "external_symbols": [], "id_avg": 1.3333333333333333, "id_max": 2, "self_fields_read": ["_is_ignorable_call", "_resolve_alias", "_resolve_self_call"], "unresolved_calls": []}, "unit": {"end_line": 222, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer._resolve_unit_calls", "start_line": 190}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["ModuleSymbolCollector"], "id_avg": 1.7777777777777777, "id_max": 4, "self_fields_read": ["_build_results", "_collect_aliases", "_collect_builtin_names", "_collect_call_graph", "_collect_class_methods", "_collect_function_definitions", "_collect_stdlib_modules", "_collect_unit_calls", "_parse_file"], "unresolved_calls": []}, "unit": {"end_line": 64, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "method", "qualified_id": "IndirectionAnalyzer.analyze_file", "start_line": 40}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["CallCollector"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 28, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "function", "qualified_id": "collect_calls", "start_line": 27}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["collect_calls"], "id_avg": 1.0, "id_max": 1, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 35, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "function", "qualified_id": "collect_calls_by_unit", "start_line": 31}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 24, "file_path": "/workspace/codra/codra/indirection/analyzer.py", "kind": "function", "qualified_id": "resolve_alias", "start_line": 18}}]}, {"file_path": "/workspace/codra/codra/indirection/result.py", "units": []}, {"file_path": "/workspace/codra/codra/module/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/module/symbol_collector.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 84, "file_path": "/workspace/codra/codra/module/symbol_collector.py", "kind": "method", "qualified_id": "ModuleSymbolCollector._extract_target_names", "start_line": 78}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["symbols"], "unresolved_calls": []}, "unit": {"end_line": 15, "file_path": "/workspace/codra/codra/module/symbol_collector.py", "kind": "method", "qualified_id": "ModuleSymbolCollector.collect", "start_line": 13}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_extract_target_names", "symbols"], "unresolved_calls": []}, "unit": {"end_line": 43, "file_path": "/workspace/codra/codra/module/symbol_collector.py", "kind": "method", "qualified_id": "ModuleSymbolCollector.visit_AnnAssign", "start_line": 41}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_extract_target_names", "symbols"], "unresolved_calls": []}, "unit": {"end_line": 39, "file_path": "/workspace/codra/codra/module/symbol_collector.py", "kind": "method", "qualified_id": "ModuleSymbolCollector.visit_Assign", "start_line": 36}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_extract_target_names", "symbols"], "unresolved_calls": []}, "unit": {"end_line": 55, "file_path": "/workspace/codra/codra/module/symbol_collector.py", "kind": "method", "qualified_id": "ModuleSymbolCollector.visit_AsyncFor", "start_line": 53}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["symbols"], "unresolved_calls": []}, "unit": {"end_line": 21, "file_path": "/workspace/codra/codra/module/symbol_collector.py", "kind": "method", "qualified_id": "ModuleSymbolCollector.visit_AsyncFunctionDef", "start_line": 20}}, {"metrics": {"bps": 0.5, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_extract_target_names", "symbols"], "unresolved_calls": []}, "unit": {"end_line": 67, "file_path": "/workspace/codra/codra/module/symbol_collector.py", "kind": "method", "qualified_id": "ModuleSymbolCollector.visit_AsyncWith", "start_line": 63}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_extract_target_names", "symbols"], "unresolved_calls": []}, "unit": {"end_line": 47, "file_path": "/workspace/codra/codra/module/symbol_collector.py", "kind": "method", "qualified_id": "ModuleSymbolCollector.visit_AugAssign", "start_line": 45}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["symbols"], "unresolved_calls": []}, "unit": {"end_line": 24, "file_path": "/workspace/codra/codra/module/symbol_collector.py", "kind": "method", "qualified_id": "ModuleSymbolCollector.visit_ClassDef", "start_line": 23}}, {"metrics": {"bps": 0.75, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_extract_target_names", "symbols"], "unresolved_calls": []}, "unit": {"end_line": 76, "file_path": "/workspace/codra/codra/module/symbol_collector.py", "kind": "method", "qualified_id": "ModuleSymbolCollector.visit_ExceptHandler", "start_line": 69}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_extract_target_names", "symbols"], "unresolved_calls": []}, "unit": {"end_line": 51, "file_path": "/workspace/codra/codra/module/symbol_collector.py", "kind": "method", "qualified_id": "ModuleSymbolCollector.visit_For", "start_line": 49}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["symbols"], "unresolved_calls": []}, "unit": {"end_line": 18, "file_path": "/workspace/codra/codra/module/symbol_collector.py", "kind": "method", "qualified_id": "ModuleSymbolCollector.visit_FunctionDef", "start_line": 17}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["symbols"], "unresolved_calls": []}, "unit": {"end_line": 29, "file_path": "/workspace/codra/codra/module/symbol_collector.py", "kind": "method", "qualified_id": "ModuleSymbolCollector.visit_Import", "start_line": 26}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["symbols"], "unresolved_calls": []}, "unit": {"end_line": 34, "file_path": "/workspace/codra/codra/module/symbol_collector.py", "kind": "method", "qualified_id": "ModuleSymbolCollector.visit_ImportFrom", "start_line": 31}}, {"metrics": {"bps": 0.5, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_extract_target_names", "symbols"], "unresolved_calls": []}, "unit": {"end_line": 61, "file_path": "/workspace/codra/codra/module/symbol_collector.py", "kind": "method", "qualified_id": "ModuleSymbolCollector.visit_With", "start_line": 57}}]}, {"file_path": "/workspace/codra/codra/python/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/python/file_scanner.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["FunctionExtractor"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 18, "file_path": "/workspace/codra/codra/python/file_scanner.py", "kind": "method", "qualified_id": "PythonFileScanner.scan_file", "start_line": 12}}]}, {"file_path": "/workspace/codra/codra/report/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/report/builder.py", "units": [{"metrics": {"bps": 0.25, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 61, "file_path": "/workspace/codra/codra/report/builder.py", "kind": "method", "qualified_id": "ReportBuilder._apply_thresholds", "start_line": 48}}, {"metrics": {"bps": 1.0, "csa_main": 2, "external_symbols": ["FileReport", "UnitReport"], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_key", "_resolve_metrics", "_resolve_unit", "_sort_key"], "unresolved_calls": []}, "unit": {"end_line": 117, "file_path": "/workspace/codra/codra/report/builder.py", "kind": "method", "qualified_id": "ReportBuilder._build_file_report", "start_line": 96}}, {"metrics": {"bps": 1.0, "csa_main": 2, "external_symbols": ["FileReport", "logger"], "id_avg": 1.5, "id_max": 2, "self_fields_read": ["_build_file_report", "_collect_file_analysis"], "unresolved_calls": []}, "unit": {"end_line": 94, "file_path": "/workspace/codra/codra/report/builder.py", "kind": "method", "qualified_id": "ReportBuilder._build_file_reports", "start_line": 83}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["ReportSummary"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 121, "file_path": "/workspace/codra/codra/report/builder.py", "kind": "method", "qualified_id": "ReportBuilder._build_summary", "start_line": 119}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["bps_analyzer", "csa_analyzer", "indirection_analyzer"], "unresolved_calls": []}, "unit": {"end_line": 81, "file_path": "/workspace/codra/codra/report/builder.py", "kind": "method", "qualified_id": "ReportBuilder._collect_file_analysis", "start_line": 75}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["UnitKey"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 70, "file_path": "/workspace/codra/codra/report/builder.py", "kind": "method", "qualified_id": "ReportBuilder._key", "start_line": 63}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["UnitMetrics"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 161, "file_path": "/workspace/codra/codra/report/builder.py", "kind": "method", "qualified_id": "ReportBuilder._resolve_metrics", "start_line": 141}}, {"metrics": {"bps": 0.5, "csa_main": 1, "external_symbols": ["UnitDefinition"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 139, "file_path": "/workspace/codra/codra/report/builder.py", "kind": "method", "qualified_id": "ReportBuilder._resolve_unit", "start_line": 123}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 73, "file_path": "/workspace/codra/codra/report/builder.py", "kind": "method", "qualified_id": "ReportBuilder._sort_key", "start_line": 72}}, {"metrics": {"bps": 1.0, "csa_main": 2, "external_symbols": ["Report", "logger"], "id_avg": 2.0, "id_max": 3, "self_fields_read": ["_build_file_reports", "_build_summary", "path_collector"], "unresolved_calls": []}, "unit": {"end_line": 43, "file_path": "/workspace/codra/codra/report/builder.py", "kind": "method", "qualified_id": "ReportBuilder.build", "start_line": 33}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_apply_thresholds"], "unresolved_calls": []}, "unit": {"end_line": 46, "file_path": "/workspace/codra/codra/report/builder.py", "kind": "method", "qualified_id": "ReportBuilder.check_thresholds", "start_line": 45}}]}, {"file_path": "/workspace/codra/codra/report/file.py", "units": []}, {"file_path": "/workspace/codra/codra/report/model.py", "units": []}, {"file_path": "/workspace/codra/codra/report/serializer.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["asdict"], "id_avg": 0.0, "id_max": 0, "self_fields_read": [], "unresolved_calls": []}, "unit": {"end_line": 13, "file_path": "/workspace/codra/codra/report/serializer.py", "kind": "method", "qualified_id": "ReportSerializer.to_json", "start_line": 12}}]}, {"file_path": "/workspace/codra/codra/report/summary.py", "units": []}, {"file_path": "/workspace/codra/codra/threshold/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/threshold/config.py", "units": []}, {"file_path": "/workspace/codra/codra/unit/__init__.py", "units": []}, {"file_path": "/workspace/codra/codra/unit/definition.py", "units": []}, {"file_path": "/workspace/codra/codra/unit/key.py", "units": []}, {"file_path": "/workspace/codra/codra/unit/metrics.py", "units": []}, {"file_path": "/workspace/codra/codra/unit/node.py", "units": []}, {"file_path": "/workspace/codra/codra/unit/node_collector.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 2, "external_symbols": ["UnitDefinition", "UnitNode"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["class_stack", "file_path", "function_stack", "units"], "unresolved_calls": []}, "unit": {"end_line": 57, "file_path": "/workspace/codra/codra/unit/node_collector.py", "kind": "method", "qualified_id": "UnitNodeCollector._handle_function", "start_line": 30}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_handle_function"], "unresolved_calls": []}, "unit": {"end_line": 28, "file_path": "/workspace/codra/codra/unit/node_collector.py", "kind": "method", "qualified_id": "UnitNodeCollector.visit_AsyncFunctionDef", "start_line": 27}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["class_stack"], "unresolved_calls": []}, "unit": {"end_line": 22, "file_path": "/workspace/codra/codra/unit/node_collector.py", "kind": "method", "qualified_id": "UnitNodeCollector.visit_ClassDef", "start_line": 19}}, {"metrics": {"bps": 1.0, "csa_main": 0, "external_symbols": [], "id_avg": 1.0, "id_max": 1, "self_fields_read": ["_handle_function"], "unresolved_calls": []}, "unit": {"end_line": 25, "file_path": "/workspace/codra/codra/unit/node_collector.py", "kind": "method", "qualified_id": "UnitNodeCollector.visit_FunctionDef", "start_line": 24}}]}, {"file_path": "/workspace/codra/codra/unit/overview.py", "units": []}, {"file_path": "/workspace/codra/tests/test_analyzers.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 2, "external_symbols": ["BpsAnalyzer", "BpsResult"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["assertAlmostEqual", "assertEqual"], "unresolved_calls": []}, "unit": {"end_line": 45, "file_path": "/workspace/codra/tests/test_analyzers.py", "kind": "method", "qualified_id": "BpsAnalyzerHelperTests.test_helpers_collect_bps_scores", "start_line": 25}}, {"metrics": {"bps": 1.0, "csa_main": 3, "external_symbols": ["CsaAnalyzer", "CsaResult", "FunctionSymbolCollector"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["assertEqual"], "unresolved_calls": []}, "unit": {"end_line": 93, "file_path": "/workspace/codra/tests/test_analyzers.py", "kind": "method", "qualified_id": "CsaAnalyzerHelperTests.test_helpers_extract_external_symbols", "start_line": 49}}, {"metrics": {"bps": 1.0, "csa_main": 3, "external_symbols": ["IndirectionAnalyzer", "UnitNodeCollector", "collect_calls_by_unit"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["assertEqual"], "unresolved_calls": []}, "unit": {"end_line": 135, "file_path": "/workspace/codra/tests/test_analyzers.py", "kind": "method", "qualified_id": "IndirectionAnalyzerHelperTests.test_helpers_collect_call_graph_and_depth", "start_line": 97}}, {"metrics": {"bps": 1.0, "csa_main": 4, "external_symbols": ["IndirectionAnalyzer", "UnitNodeCollector", "collect_calls_by_unit", "resolve_alias"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["assertEqual"], "unresolved_calls": []}, "unit": {"end_line": 181, "file_path": "/workspace/codra/tests/test_analyzers.py", "kind": "method", "qualified_id": "IndirectionAnalyzerHelperTests.test_helpers_resolve_alias_chain_and_class_methods", "start_line": 137}}, {"metrics": {"bps": 1.0, "csa_main": 9, "external_symbols": ["BpsAnalyzer", "BpsResult", "CsaAnalyzer", "CsaResult", "IndirectionAnalyzer", "IndirectionResult", "ReportBuilder", "UnitDefinition", "UnitReport"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["assertEqual", "assertIsInstance"], "unresolved_calls": []}, "unit": {"end_line": 235, "file_path": "/workspace/codra/tests/test_analyzers.py", "kind": "method", "qualified_id": "ReportBuilderHelperTests.test_helpers_build_file_report_and_summary", "start_line": 185}}]}, {"file_path": "/workspace/codra/tests/test_collectors.py", "units": [{"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["AliasCollector"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["assertEqual"], "unresolved_calls": []}, "unit": {"end_line": 29, "file_path": "/workspace/codra/tests/test_collectors.py", "kind": "method", "qualified_id": "CollectorVisitTests.test_alias_collector_tracks_top_level_aliases", "start_line": 15}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["CallCollector"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["assertEqual"], "unresolved_calls": []}, "unit": {"end_line": 46, "file_path": "/workspace/codra/tests/test_collectors.py", "kind": "method", "qualified_id": "CollectorVisitTests.test_call_collector_tracks_module_calls_only", "start_line": 31}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["ConditionMetricsCollector"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["assertEqual"], "unresolved_calls": []}, "unit": {"end_line": 63, "file_path": "/workspace/codra/tests/test_collectors.py", "kind": "method", "qualified_id": "CollectorVisitTests.test_condition_metrics_collector_counts_operations", "start_line": 48}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["FunctionDefinitionCollector"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["assertEqual"], "unresolved_calls": []}, "unit": {"end_line": 95, "file_path": "/workspace/codra/tests/test_collectors.py", "kind": "method", "qualified_id": "CollectorVisitTests.test_function_definition_collector_skips_methods", "start_line": 81}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["FunctionSymbolCollector"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["assertIn"], "unresolved_calls": []}, "unit": {"end_line": 117, "file_path": "/workspace/codra/tests/test_collectors.py", "kind": "method", "qualified_id": "CollectorVisitTests.test_function_symbol_collector_tracks_usage", "start_line": 97}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["IfCollector"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["assertEqual", "assertIsInstance"], "unresolved_calls": []}, "unit": {"end_line": 79, "file_path": "/workspace/codra/tests/test_collectors.py", "kind": "method", "qualified_id": "CollectorVisitTests.test_if_collector_tracks_module_if_statements", "start_line": 65}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["ModuleSymbolCollector"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["assertTrue"], "unresolved_calls": []}, "unit": {"end_line": 151, "file_path": "/workspace/codra/tests/test_collectors.py", "kind": "method", "qualified_id": "CollectorVisitTests.test_module_symbol_collector_gathers_symbols", "start_line": 119}}, {"metrics": {"bps": 1.0, "csa_main": 1, "external_symbols": ["UnitNodeCollector"], "id_avg": 0.0, "id_max": 0, "self_fields_read": ["assertEqual"], "unresolved_calls": []}, "unit": {"end_line": 172, "file_path": "/workspace/codra/tests/test_collectors.py", "kind": "method", "qualified_id": "CollectorVisitTests.test_unit_node_collector_builds_qualified_ids", "start_line": 153}}]}], "language": "python", "schema_version": "1.0", "summary": {"total_files": 54, "total_units": 130}} diff --git a/tests/test_analyzers.py b/tests/test_analyzers.py new file mode 100644 index 0000000..1527a02 --- /dev/null +++ b/tests/test_analyzers.py @@ -0,0 +1,239 @@ +import builtins +import sys +import tempfile +import textwrap +import unittest + +from codra.bps.analyzer import BpsAnalyzer +from codra.bps.result import BpsResult +from codra.csa.analyzer import CsaAnalyzer +from codra.csa.result import CsaResult +from codra.function.symbol_collector import FunctionSymbolCollector +from codra.indirection.analyzer import ( + IndirectionAnalyzer, + collect_calls_by_unit, + resolve_alias, +) +from codra.indirection.result import IndirectionResult +from codra.report.builder import ReportBuilder +from codra.unit.definition import UnitDefinition +from codra.unit.node_collector import UnitNodeCollector +from codra.unit.overview import UnitReport + + +class BpsAnalyzerHelperTests(unittest.TestCase): + def test_helpers_collect_bps_scores(self) -> None: + source = textwrap.dedent( + """ + def foo(x): + if x and y: + return 1 + return 2 + """ + ) + with tempfile.TemporaryDirectory() as tmpdir: + file_path = f"{tmpdir}/sample.py" + with open(file_path, "w", encoding="utf-8") as handle: + handle.write(source) + analyzer = BpsAnalyzer() + tree = analyzer._parse_file(file_path) + unit_nodes = analyzer._collect_units(tree, file_path) + self.assertEqual(len(unit_nodes), 1) + bps = analyzer._collect_bps(unit_nodes[0]) + self.assertAlmostEqual(bps, 0.25) + results = analyzer._build_results(unit_nodes, [bps]) + self.assertEqual(results, [BpsResult(unit=unit_nodes[0].definition, bps=bps)]) + + +class CsaAnalyzerHelperTests(unittest.TestCase): + def test_helpers_extract_external_symbols(self) -> None: + source = textwrap.dedent( + """ + import os + from math import sqrt + + CONST = 10 + + def foo(self, x): + return self.attr + sqrt(x) + CONST + y + """ + ) + with tempfile.TemporaryDirectory() as tmpdir: + file_path = f"{tmpdir}/sample.py" + with open(file_path, "w", encoding="utf-8") as handle: + handle.write(source) + analyzer = CsaAnalyzer() + tree = analyzer._parse_file(file_path) + module_symbols = analyzer._collect_module_symbols(tree) + unit_nodes = analyzer._collect_unit_nodes(tree, file_path) + usage = FunctionSymbolCollector().collect(unit_nodes[0].node) + external_symbols = analyzer._extract_external_symbols( + usage, + module_symbols, + set(dir(builtins)), + set(sys.stdlib_module_names), + ) + self.assertEqual(external_symbols, {"CONST", "sqrt", "y"}) + results = analyzer._build_results( + unit_nodes, + module_symbols, + set(dir(builtins)), + set(sys.stdlib_module_names), + ) + self.assertEqual( + results, + [ + CsaResult( + unit=unit_nodes[0].definition, + csa_main=3, + external_symbols=["CONST", "sqrt", "y"], + self_fields_read=["attr"], + ) + ], + ) + + +class IndirectionAnalyzerHelperTests(unittest.TestCase): + def test_helpers_collect_call_graph_and_depth(self) -> None: + source = textwrap.dedent( + """ + def bar(): + pass + + def foo(): + bar() + + alias = bar + + def baz(): + alias() + """ + ) + with tempfile.TemporaryDirectory() as tmpdir: + file_path = f"{tmpdir}/sample.py" + with open(file_path, "w", encoding="utf-8") as handle: + handle.write(source) + analyzer = IndirectionAnalyzer() + tree = analyzer._parse_file(file_path) + function_names = analyzer._collect_function_definitions(tree) + self.assertEqual(function_names, {"bar", "foo", "baz"}) + aliases = analyzer._collect_aliases(tree) + self.assertEqual(aliases, {"alias": "bar"}) + class_methods = analyzer._collect_class_methods(tree) + unit_collector = UnitNodeCollector(file_path=file_path) + unit_collector.visit(tree) + call_map = collect_calls_by_unit(unit_collector.units) + call_graph = analyzer._collect_call_graph( + function_names, class_methods, aliases, call_map, {} + ) + self.assertEqual(call_graph["foo"], ["bar"]) + self.assertEqual(call_graph["baz"], ["bar"]) + id_max, id_avg = analyzer._calculate_indirection_depths( + ["bar"], call_graph, {} + ) + self.assertEqual(id_max, 1) + self.assertEqual(id_avg, 1.0) + + def test_helpers_resolve_alias_chain_and_class_methods(self) -> None: + source = textwrap.dedent( + """ + def helper(): + pass + + alias = helper + alias2 = alias + + def uses_alias(): + alias2() + + class Foo: + def ping(self): + self.pong() + alias2() + + def pong(self): + alias() + """ + ) + with tempfile.TemporaryDirectory() as tmpdir: + file_path = f"{tmpdir}/sample.py" + with open(file_path, "w", encoding="utf-8") as handle: + handle.write(source) + analyzer = IndirectionAnalyzer() + tree = analyzer._parse_file(file_path) + aliases = analyzer._collect_aliases(tree) + self.assertEqual(resolve_alias("alias2", aliases), "helper") + function_names = analyzer._collect_function_definitions(tree) + class_methods = analyzer._collect_class_methods(tree) + unit_collector = UnitNodeCollector(file_path=file_path) + unit_collector.visit(tree) + call_map = collect_calls_by_unit(unit_collector.units) + call_graph = analyzer._collect_call_graph( + function_names, class_methods, aliases, call_map, {} + ) + self.assertEqual(call_graph["uses_alias"], ["helper"]) + self.assertEqual( + call_graph["Foo.ping"], ["Foo.pong", "helper"] + ) + results = analyzer.analyze_file(file_path) + results_by_unit = {result.unit.qualified_id: result for result in results} + self.assertEqual(results_by_unit["uses_alias"].id_max, 1) + self.assertEqual(results_by_unit["Foo.ping"].id_max, 2) + + +class ReportBuilderHelperTests(unittest.TestCase): + def test_helpers_build_file_report_and_summary(self) -> None: + builder = ReportBuilder( + csa_analyzer=CsaAnalyzer(), + indirection_analyzer=IndirectionAnalyzer(), + bps_analyzer=BpsAnalyzer(), + path_collector=None, + ) + file_path = "sample.py" + unit_a = UnitDefinition( + file_path=file_path, + qualified_id="a", + kind="function", + start_line=1, + end_line=2, + ) + unit_b = UnitDefinition( + file_path=file_path, + qualified_id="b", + kind="function", + start_line=3, + end_line=4, + ) + csa_results = [ + CsaResult( + unit=unit_b, + csa_main=1, + external_symbols=["z"], + self_fields_read=[], + ) + ] + indirection_results = [ + IndirectionResult( + unit=unit_a, id_max=2, id_avg=1.5, unresolved_calls=["x"] + ) + ] + bps_results = [BpsResult(unit=unit_a, bps=0.5), BpsResult(unit=unit_b, bps=1.0)] + file_report, unit_count = builder._build_file_report( + file_path, csa_results, indirection_results, bps_results + ) + self.assertEqual(unit_count, 2) + self.assertEqual(file_report.file_path, file_path) + self.assertEqual( + [unit.unit.qualified_id for unit in file_report.units], ["a", "b"] + ) + unit_a_report, unit_b_report = file_report.units + self.assertIsInstance(unit_a_report, UnitReport) + self.assertEqual(unit_a_report.metrics.id_max, 2) + self.assertEqual(unit_b_report.metrics.csa_main, 1) + summary = builder._build_summary([file_report], unit_count) + self.assertEqual(summary.total_files, 1) + self.assertEqual(summary.total_units, 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_collectors.py b/tests/test_collectors.py new file mode 100644 index 0000000..e43e4bd --- /dev/null +++ b/tests/test_collectors.py @@ -0,0 +1,176 @@ +import ast +import unittest + +from codra.alias.collector import AliasCollector +from codra.call.collector import CallCollector +from codra.condition.collector import ConditionMetricsCollector +from codra.function.definition_collector import FunctionDefinitionCollector +from codra.function.symbol_collector import FunctionSymbolCollector +from codra.if_.collector import IfCollector +from codra.module.symbol_collector import ModuleSymbolCollector +from codra.unit.node_collector import UnitNodeCollector + + +class CollectorVisitTests(unittest.TestCase): + def test_alias_collector_tracks_top_level_aliases(self) -> None: + tree = ast.parse( + """ +alias = original +other: int = alias + +def inner(): + shadow = original +""" + ) + collector = AliasCollector() + self.assertEqual( + collector.collect(tree), + {"alias": "original", "other": "alias"}, + ) + + def test_call_collector_tracks_module_calls_only(self) -> None: + tree = ast.parse( + """ +foo() +self.bar() + +def inner(): + baz() + +class Thing: + def method(self): + qux() +""" + ) + collector = CallCollector() + self.assertEqual(collector.collect(tree), ["foo", "self.bar"]) + + def test_condition_metrics_collector_counts_operations(self) -> None: + tree = ast.parse( + """ +if a and b and c and foo(x) and y < z: + pass +""" + ) + if_node = tree.body[0] + collector = ConditionMetricsCollector( + local_names={"a", "b", "c", "x", "y", "z"} + ) + metrics = collector.collect(if_node.test) + self.assertEqual(metrics.bool_ops, 4) + self.assertEqual(metrics.compare_ops, 1) + self.assertEqual(metrics.calls, 1) + self.assertEqual(metrics.external_refs, 1) + + def test_if_collector_tracks_module_if_statements(self) -> None: + tree = ast.parse( + """ +if ready: + pass + +def inner(): + if nested: + pass +""" + ) + collector = IfCollector() + nodes = collector.collect(tree) + self.assertEqual(len(nodes), 1) + self.assertIsInstance(nodes[0], ast.If) + + def test_function_definition_collector_skips_methods(self) -> None: + tree = ast.parse( + """ +def top(): + def inner(): + return 1 + return inner() + +class Thing: + def method(self): + return 2 +""" + ) + collector = FunctionDefinitionCollector() + self.assertEqual(collector.collect(tree), {"top", "inner"}) + + def test_function_symbol_collector_tracks_usage(self) -> None: + tree = ast.parse( + """ +def foo(arg): + global g + x = arg + self.attr + return g + x +""" + ) + function_node = next( + node for node in tree.body if isinstance(node, ast.FunctionDef) + ) + collector = FunctionSymbolCollector() + usage = collector.collect(function_node) + self.assertIn("arg", usage.locals) + self.assertIn("x", usage.locals) + self.assertIn("g", usage.used_names) + self.assertIn("arg", usage.used_names) + self.assertIn("attr", usage.self_fields_read) + self.assertIn("g", usage.global_decls) + + def test_module_symbol_collector_gathers_symbols(self) -> None: + tree = ast.parse( + """ +import os as operating +from sys import path as sys_path + +class Foo: + pass + +def bar(): + pass + +baz = 1 +count: int = 2 + +for i in range(3): + pass + +with open("a") as f: + pass + +try: + pass +except Exception as exc: + pass +""" + ) + collector = ModuleSymbolCollector() + symbols = collector.collect(tree) + self.assertTrue( + {"operating", "sys_path", "Foo", "bar", "baz", "count", "i", "f", "exc"} + <= symbols + ) + + def test_unit_node_collector_builds_qualified_ids(self) -> None: + tree = ast.parse( + """ +def top(): + def inner(): + return 1 + return inner() + +class Thing: + def method(self): + return 2 +""" + ) + collector = UnitNodeCollector(file_path="module.py") + collector.visit(tree) + qualified_ids = {unit.definition.qualified_id for unit in collector.units} + kinds = {unit.definition.qualified_id: unit.definition.kind for unit in collector.units} + self.assertEqual(qualified_ids, {"top", "top.inner", "Thing.method"}) + self.assertEqual(kinds["Thing.method"], "method") + self.assertEqual(kinds["top"], "function") + + +if __name__ == "__main__": + unittest.main() diff --git a/viewer/package-lock.json b/viewer/package-lock.json new file mode 100644 index 0000000..58a95f4 --- /dev/null +++ b/viewer/package-lock.json @@ -0,0 +1,1731 @@ +{ + "name": "codra-viewer", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "codra-viewer", + "version": "0.1.0", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.21", + "@types/react-dom": "^18.2.7", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.3.3", + "vite": "^5.0.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/viewer/src/App.tsx b/viewer/src/App.tsx index c60d7c3..7a18998 100644 --- a/viewer/src/App.tsx +++ b/viewer/src/App.tsx @@ -1,11 +1,14 @@ -import { useState } from "react" +import { useMemo, useState } from "react" import type { Report } from "./report_types" import { ReportTable } from "./report_table" +import { evaluateMetrics, type StatusLevel } from "./report_utils" export function App() { const [report, setReport] = useState(null) const [error, setError] = useState(null) const [filterText, setFilterText] = useState("") + const [statusFilter, setStatusFilter] = useState("all") + const [onlyIssues, setOnlyIssues] = useState(false) const handleFile = (event: React.ChangeEvent) => { const file = event.target.files?.[0] @@ -30,6 +33,27 @@ export function App() { reader.readAsText(file) } + const globalStats = useMemo(() => { + if (!report) { + return null + } + const stats = { + ok: 0, + warning: 0, + critical: 0 + } + for (const file of report.files) { + for (const unit of file.units) { + const evaluation = evaluateMetrics(unit.metrics) + stats[evaluation.level] += 1 + } + } + const issues = stats.warning + stats.critical + const overall: StatusLevel = + stats.critical > 0 ? "critical" : stats.warning > 0 ? "warning" : "ok" + return { ...stats, issues, overall } + }, [report]) + return (
@@ -47,8 +71,30 @@ export function App() { type="text" value={filterText} onChange={(event) => setFilterText(event.target.value)} - placeholder="Search by file, unit, or kind" + placeholder="Search by file, unit, kind, or issue" + /> + + + {error ?
{error}
: null} @@ -71,8 +117,36 @@ export function App() { Units {report.summary.total_units}
+
+ Critical + {globalStats?.critical ?? 0} +
+
+ Warning + {globalStats?.warning ?? 0} +
+
+ OK + {globalStats?.ok ?? 0} +
+
+ Issues + {globalStats?.issues ?? 0} +
+
+ Health + + + {globalStats?.overall ?? "ok"} + +
- + ) : (
diff --git a/viewer/src/report_table.tsx b/viewer/src/report_table.tsx index 907ab64..d2eddf2 100644 --- a/viewer/src/report_table.tsx +++ b/viewer/src/report_table.tsx @@ -1,24 +1,35 @@ import { useMemo, useState } from "react" import type { FileReport, UnitReport } from "./report_types" +import { evaluateMetrics, type StatusLevel } from "./report_utils" -export type SortKey = "file" | "unit" | "kind" | "csa" | "id" | "bps" +export type SortKey = "file" | "unit" | "kind" | "status" | "csa" | "id" | "bps" type ReportTableProps = { files: FileReport[] filterText: string + statusFilter: StatusLevel | "all" + onlyIssues: boolean } type Row = { file: string unit: string kind: string + status: StatusLevel + statusScore: number + issues: string[] csa: number idMax: number bps: number raw: UnitReport } -export function ReportTable({ files, filterText }: ReportTableProps) { +export function ReportTable({ + files, + filterText, + statusFilter, + onlyIssues +}: ReportTableProps) { const [sortKey, setSortKey] = useState("file") const [direction, setDirection] = useState<"asc" | "desc">("asc") @@ -26,10 +37,14 @@ export function ReportTable({ files, filterText }: ReportTableProps) { const entries: Row[] = [] for (const file of files) { for (const unit of file.units) { + const evaluation = evaluateMetrics(unit.metrics) entries.push({ file: file.file_path, unit: unit.unit.qualified_id, kind: unit.unit.kind, + status: evaluation.level, + statusScore: evaluation.score, + issues: evaluation.issues.map((issue) => issue.label), csa: unit.metrics.csa_main, idMax: unit.metrics.id_max, bps: unit.metrics.bps, @@ -38,14 +53,18 @@ export function ReportTable({ files, filterText }: ReportTableProps) { } } const needle = filterText.trim().toLowerCase() - const filtered = needle - ? entries.filter((row) => - [row.file, row.unit, row.kind] + const filtered = entries.filter((row) => { + const matchesText = needle + ? [row.file, row.unit, row.kind, row.issues.join(" ")] .join(" ") .toLowerCase() .includes(needle) - ) - : entries + : true + const matchesStatus = + statusFilter === "all" ? true : row.status === statusFilter + const matchesIssues = onlyIssues ? row.status !== "ok" : true + return matchesText && matchesStatus && matchesIssues + }) const sorted = [...filtered].sort((left, right) => { const factor = direction === "asc" ? 1 : -1 if (sortKey === "file") { @@ -57,6 +76,9 @@ export function ReportTable({ files, filterText }: ReportTableProps) { if (sortKey === "kind") { return left.kind.localeCompare(right.kind) * factor } + if (sortKey === "status") { + return (left.statusScore - right.statusScore) * factor + } if (sortKey === "csa") { return (left.csa - right.csa) * factor } @@ -96,6 +118,11 @@ export function ReportTable({ files, filterText }: ReportTableProps) { Kind + + + + Issues @@ -119,9 +147,28 @@ export function ReportTable({ files, filterText }: ReportTableProps) { {row.file} {row.unit} {row.kind} + + + + {row.status} + + {row.csa} {row.idMax} {row.bps.toFixed(2)} + +
+ {row.issues.length ? ( + row.issues.map((issue) => ( + + {issue} + + )) + ) : ( + OK + )} +
+ ))} diff --git a/viewer/src/report_utils.ts b/viewer/src/report_utils.ts new file mode 100644 index 0000000..e95c254 --- /dev/null +++ b/viewer/src/report_utils.ts @@ -0,0 +1,72 @@ +import type { UnitMetrics } from "./report_types" + +export type StatusLevel = "ok" | "warning" | "critical" + +type Issue = { + label: string + level: StatusLevel +} + +const thresholds = { + csa: { + warning: 5, + critical: 10 + }, + idMax: { + warning: 3, + critical: 5 + }, + bps: { + warning: 0.7, + critical: 0.5 + } +} + +const levelScore: Record = { + ok: 0, + warning: 1, + critical: 2 +} + +export function evaluateMetrics(metrics: UnitMetrics): { + level: StatusLevel + score: number + issues: Issue[] +} { + const issues: Issue[] = [] + + if (metrics.csa_main >= thresholds.csa.critical) { + issues.push({ label: "CSA >= 10", level: "critical" }) + } else if (metrics.csa_main >= thresholds.csa.warning) { + issues.push({ label: "CSA >= 5", level: "warning" }) + } + + if (metrics.id_max >= thresholds.idMax.critical) { + issues.push({ label: "ID max >= 5", level: "critical" }) + } else if (metrics.id_max >= thresholds.idMax.warning) { + issues.push({ label: "ID max >= 3", level: "warning" }) + } + + if (metrics.bps <= thresholds.bps.critical) { + issues.push({ label: "BPS <= 0.5", level: "critical" }) + } else if (metrics.bps <= thresholds.bps.warning) { + issues.push({ label: "BPS <= 0.7", level: "warning" }) + } + + if (metrics.unresolved_calls.length > 0) { + issues.push({ label: "Chiamate non risolte", level: "warning" }) + } + + let level: StatusLevel = "ok" + for (const issue of issues) { + if (levelScore[issue.level] > levelScore[level]) { + level = issue.level + } + } + + return { + level, + score: levelScore[level], + issues + } +} diff --git a/viewer/src/styles.css b/viewer/src/styles.css index 7af25cd..f302822 100644 --- a/viewer/src/styles.css +++ b/viewer/src/styles.css @@ -42,13 +42,27 @@ header p { } .controls input[type="text"], -.controls input[type="file"] { +.controls input[type="file"], +.controls select { padding: 8px 10px; border-radius: 6px; border: 1px solid #cbd5f5; background: #ffffff; } +.checkbox-input { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + margin-bottom: 2px; +} + +.checkbox-input input { + width: 16px; + height: 16px; +} + .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); @@ -72,6 +86,58 @@ header p { color: #64748b; } +.summary-status span { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.status { + display: inline-flex; + align-items: center; + gap: 8px; + text-transform: capitalize; + font-weight: 600; +} + +.status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: #22c55e; + box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.15); +} + +.status-warning .status-dot { + background: #f59e0b; + box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.2); +} + +.status-critical .status-dot { + background: #ef4444; + box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.2); +} + +.issue-list { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.issue-pill { + padding: 2px 8px; + border-radius: 999px; + background: #fef3c7; + color: #92400e; + font-size: 12px; + font-weight: 600; +} + +.issue-pill.issue-ok { + background: #dcfce7; + color: #166534; +} + .report-table { width: 100%; border-collapse: collapse;