"""Shared utilities for all native detector modules."""
from __future__ import annotations

import ast
import functools
from pathlib import Path


@functools.lru_cache(maxsize=1024)
def _cached_parse(text: str) -> ast.Module:
    """Parse source text, memoized by the text itself.

    During one scan every per-file detector and every cross-file detector parses
    the same files; without caching that is ~18 ast.parse calls per file. The key
    is the source text (identical text always yields the same AST), so the cache
    is correctness-safe and self-invalidating. Callers treat the returned tree as
    read-only (detectors only walk it / build separate parent maps)."""
    return ast.parse(text)


def _relative_path(path: Path, root: Path) -> str:
    try:
        return path.relative_to(root).as_posix()
    except ValueError:
        return path.as_posix()


def _line_number_from_index(text: str, index: int) -> int:
    return text.count("\n", 0, index) + 1


def _line_excerpt(text: str, line_number: int) -> str:
    lines = text.splitlines()
    if 1 <= line_number <= len(lines):
        return lines[line_number - 1].strip()
    return ""


def _build_parent_map(tree: ast.AST) -> dict[ast.AST, ast.AST]:
    parents: dict[ast.AST, ast.AST] = {}
    for parent in ast.walk(tree):
        for child in ast.iter_child_nodes(parent):
            parents[child] = parent
    return parents
