Skip to content

Analyzer

analyzer

High-level package analysis engine.

This module builds on the tree-sitter parser to provide package-wide analysis: module discovery, dependency graphs, public API extraction, and semantic search.

Example

from pathlib import Path from axm_ast.core.analyzer import analyze_package pkg = analyze_package(Path("src/mylib")) [m.path.name for m in pkg.modules] ['__init__.py', 'core.py', 'utils.py']

analyze_package(path)

Analyze a Python package directory.

Discovers all .py files, parses them with tree-sitter, and builds a complete PackageInfo with dependency edges.

Parameters:

Name Type Description Default
path Path

Path to the package root directory.

required

Returns:

Type Description
PackageInfo

PackageInfo with all modules and dependency edges.

Raises:

Type Description
ValueError

If path is not a directory.

Example

pkg = analyze_package(Path("src/mylib")) pkg.name 'mylib'

Source code in packages/axm-ast/src/axm_ast/core/analyzer.py
Python
def analyze_package(path: Path) -> PackageInfo:
    """Analyze a Python package directory.

    Discovers all ``.py`` files, parses them with tree-sitter, and
    builds a complete ``PackageInfo`` with dependency edges.

    Args:
        path: Path to the package root directory.

    Returns:
        PackageInfo with all modules and dependency edges.

    Raises:
        ValueError: If path is not a directory.

    Example:
        >>> pkg = analyze_package(Path("src/mylib"))
        >>> pkg.name
        'mylib'
    """
    path = Path(path).resolve()
    if not path.is_dir():
        msg = f"{path} is not a directory"
        raise ValueError(msg)

    # Detect src-layout: src/<pkg>/__init__.py
    src_dir = path / "src"
    if src_dir.is_dir():
        pkg_dirs = [
            child
            for child in src_dir.iterdir()
            if child.is_dir() and (child / "__init__.py").exists()
        ]
        if pkg_dirs:
            path = pkg_dirs[0]

    t0 = time.perf_counter()

    # Discover all .py files, skipping virtual envs and caches
    py_files = sorted(_discover_py_files(path))
    modules: list[ModuleInfo] = []
    for py_file in py_files:
        modules.append(extract_module_info(py_file))

    # Build dependency edges from internal imports
    dep_edges = _build_edges(modules, path)

    pkg = PackageInfo(
        name=path.name,
        root=path,
        modules=modules,
        dependency_edges=dep_edges,
    )

    elapsed = time.perf_counter() - t0
    logger.debug("Analyzed %s in %.2fs (%d modules)", path.name, elapsed, len(modules))

    return pkg

build_import_graph(pkg)

Build an adjacency-list import graph from package info.

Parameters:

Name Type Description Default
pkg PackageInfo

Analyzed package info.

required

Returns:

Type Description
dict[str, list[str]]

Dict mapping module name to list of modules it imports.

Example

graph = build_import_graph(pkg) graph["cli"] ['core', 'models']

Source code in packages/axm-ast/src/axm_ast/core/analyzer.py
Python
def build_import_graph(pkg: PackageInfo) -> dict[str, list[str]]:
    """Build an adjacency-list import graph from package info.

    Args:
        pkg: Analyzed package info.

    Returns:
        Dict mapping module name to list of modules it imports.

    Example:
        >>> graph = build_import_graph(pkg)
        >>> graph["cli"]
        `['core', 'models']`
    """
    graph: dict[str, list[str]] = {}
    for src, target in pkg.dependency_edges:
        graph.setdefault(src, []).append(target)
    return graph

find_module_for_symbol(pkg, symbol)

Find the module containing a symbol.

Supports two lookup modes:

  • Object (FunctionInfo / ClassInfo): identity-first match, then name fallback.
  • String: name-based search across functions, methods, and classes.

Parameters:

Name Type Description Default
pkg PackageInfo

Analyzed package info.

required
symbol str | FunctionInfo | ClassInfo | VariableInfo

Symbol name or object to locate.

required

Returns:

Type Description
ModuleInfo | None

The ModuleInfo containing the symbol, or None.

Source code in packages/axm-ast/src/axm_ast/core/analyzer.py
Python
def find_module_for_symbol(
    pkg: PackageInfo,
    symbol: str | FunctionInfo | ClassInfo | VariableInfo,
) -> ModuleInfo | None:
    """Find the module containing a symbol.

    Supports two lookup modes:

    - **Object** (``FunctionInfo`` / ``ClassInfo``): identity-first match,
      then name fallback.
    - **String**: name-based search across functions, methods, and classes.

    Args:
        pkg: Analyzed package info.
        symbol: Symbol name or object to locate.

    Returns:
        The ``ModuleInfo`` containing the symbol, or ``None``.
    """
    if not isinstance(symbol, str):
        # Identity match (when passed an object)
        result = _find_module_by_identity(pkg, symbol)
        if result is not None:
            return result
        # Fallback to name-based search
        symbol = symbol.name

    return _find_module_by_name(pkg, symbol)

module_dotted_name(mod_path, root)

Convert a module file path to a dotted name relative to root.

Parameters:

Name Type Description Default
mod_path Path

Absolute path to a .py file.

required
root Path

Package root directory.

required

Returns:

Type Description
str

Dotted module name (e.g. "core.parser").

Source code in packages/axm-ast/src/axm_ast/core/analyzer.py
Python
def module_dotted_name(mod_path: Path, root: Path) -> str:
    """Convert a module file path to a dotted name relative to root.

    Args:
        mod_path: Absolute path to a ``.py`` file.
        root: Package root directory.

    Returns:
        Dotted module name (e.g. ``"core.parser"``).
    """
    try:
        rel = mod_path.relative_to(root)
    except ValueError:
        return mod_path.stem
    parts = list(rel.with_suffix("").parts)
    if parts and parts[-1] == "__init__":
        parts = parts[:-1]
    if parts and parts[0] == "src" and len(parts) > 1:
        parts = parts[1:]
    return ".".join(parts) if parts else root.name

search_symbols(pkg, *, name=None, returns=None, kind=None, inherits=None)

Search for symbols across a package with filters.

All filters are AND-combined. A symbol must match all provided filters to be included in results.

Parameters:

Name Type Description Default
pkg PackageInfo

Analyzed package info.

required
name str | None

Filter by symbol name (substring match).

None
returns str | None

Filter functions by return type (substring match).

None
kind SymbolKind | None

Filter by SymbolKind (function, method, property, classmethod, staticmethod, abstract, class, variable).

None
inherits str | None

Filter classes by base class name.

None

Returns:

Type Description
list[tuple[str, FunctionInfo | ClassInfo | VariableInfo]]

List of (module_name, symbol) tuples for matching symbols.

Example

results = search_symbols(pkg, returns="str") [sym.name for _, sym in results] ['greet', 'version']

Source code in packages/axm-ast/src/axm_ast/core/analyzer.py
Python
def search_symbols(
    pkg: PackageInfo,
    *,
    name: str | None = None,
    returns: str | None = None,
    kind: SymbolKind | None = None,
    inherits: str | None = None,
) -> list[tuple[str, FunctionInfo | ClassInfo | VariableInfo]]:
    """Search for symbols across a package with filters.

    All filters are AND-combined. A symbol must match all provided
    filters to be included in results.

    Args:
        pkg: Analyzed package info.
        name: Filter by symbol name (substring match).
        returns: Filter functions by return type (substring match).
        kind: Filter by SymbolKind (function, method, property,
            classmethod, staticmethod, abstract, class, variable).
        inherits: Filter classes by base class name.

    Returns:
        List of (module_name, symbol) tuples for matching symbols.

    Example:
        >>> results = search_symbols(pkg, returns="str")
        >>> [sym.name for _, sym in results]
        `['greet', 'version']`
    """
    results: list[tuple[str, FunctionInfo | ClassInfo | VariableInfo]] = []

    for mod in pkg.modules:
        mod_dotted = mod.name or module_dotted_name(mod.path, pkg.root)
        for sym in _search_module(
            mod,
            name=name,
            returns=returns,
            kind=kind,
            inherits=inherits,
        ):
            results.append((mod_dotted, sym))

    return results