# Static Analysis

URL: /kida/docs/advanced/analysis/
Section: advanced
Description: Analyze templates for dependencies, purity, and caching potential

---

> For a complete page index, fetch /kida/llms.txt.

Kida can statically analyze compiled templates to extract dependency information,
determine output purity, and recommend caching strategies — all without rendering.
No other Python template engine provides this capability.

## What Analysis Provides

Every compiled template exposes these analysis results:

| Capability | Method | Returns |
|---|---|---|
| **Context dependencies** | `template.required_context()` | Top-level variable names the template accesses |
| **Full dependency paths** | `template.depends_on()` | Dotted paths like `page.title`, `site.pages` |
| **Block metadata** | `template.block_metadata()` | Per-block purity, dependencies, cache scope |
| **Full metadata** | `template.template_metadata()` | Complete analysis including inheritance info |
| **Cache check** | `template.is_cacheable("nav")` | Whether a block can be safely cached |
| **Context validation** | `template.validate_context(ctx)` | Missing variable names before rendering |

## Quick Start

```python
from kida import Environment, DictLoader

env = Environment(loader=DictLoader({
    "page.html": """
        {% extends "base.html" %}
        {% block title %}{{ page.title }}{% end %}
        {% block nav %}
            <nav>{% for item in site.menu %}<a href="{{ item.url }}">{{ item.label }}</a>{% end %}</nav>
        {% end %}
        {% block content %}{{ page.content }}{% end %}
    """,
    "base.html": """
        <html>
        <head><title>{% block title %}{% end %}</title></head>
        <body>{% block nav %}{% end %}{% block content %}{% end %}</body>
        </html>
    """,
}))

template = env.get_template("page.html")
```

### Check What Variables a Template Needs

```python
>>> template.required_context()
frozenset({'page', 'site'})
```

### Validate Context Before Rendering

```python
>>> template.validate_context({"page": page_obj})
['site']  # 'site' is missing

>>> template.validate_context({"page": page_obj, "site": site_obj})
[]  # all required variables present
```

### Inspect Block-Level Metadata

```python
>>> meta = template.block_metadata()
>>> nav = meta["nav"]
>>> nav.depends_on
frozenset({'site.menu'})
>>> nav.is_pure
'pure'
>>> nav.cache_scope
'site'
>>> nav.is_cacheable()
True
```

### Determine Caching Strategy

```python
>>> template.is_cacheable("nav")    # site-wide cache
True
>>> template.is_cacheable("content")  # page-specific, still cacheable
True
>>> template.is_cacheable()          # all blocks cacheable?
True
```

## Analysis Concepts

### Dependencies

The dependency walker extracts every context variable path a template accesses.
Results are *conservative*: they may include paths not taken at runtime (e.g.,
both branches of an `{% if %}`) but never miss a path that is used.

```python
>>> template.depends_on()
frozenset({'page.title', 'page.content', 'site.menu'})
```

`required_context()` extracts just the top-level names:

```python
>>> template.required_context()
frozenset({'page', 'site'})
```

### Purity

A block is **pure** if its output is deterministic given the same inputs.
Pure blocks can be cached safely. Impure blocks (using `random`, `shuffle`,
or similar) must be re-rendered each time.

| Purity | Meaning | Cacheable? |
|---|---|---|
| `"pure"` | Deterministic output | Yes |
| `"impure"` | Uses non-deterministic functions | No |
| `"unknown"` | Cannot determine statically | Treat as impure |

### Cache Scope

Cache scope tells you *how broadly* a block's output can be reused:

| Scope | Meaning | Example |
|---|---|---|
| `"site"` | Same output for every page | Navigation, footer |
| `"page"` | Varies per page but stable for a given page | Content, title |
| `"none"` | Cannot cache (impure block) | Random quotes |
| `"unknown"` | Cannot determine | Mixed dependencies |

### Landmarks

Kida detects HTML5 landmark elements (`<nav>`, `<main>`, `<header>`, `<footer>`,
`<aside>`) in block output and uses them to infer the block's role:

```python
>>> meta["nav"].emits_landmarks
frozenset({'nav'})
>>> meta["nav"].inferred_role
'navigation'
```

## Context Validation

`validate_context()` is designed for build systems, SSR frameworks, and testing
pipelines that need to catch missing variables before rendering:

```python
template = env.get_template("email.html")
missing = template.validate_context(user_context)
if missing:
    raise ValueError(f"Missing template variables: {missing}")
result = template.render(**user_context)
```

This runs dependency analysis (cached after first call) and compares required
top-level variable names against the provided context keys plus environment
globals. It returns a sorted list of missing names, or an empty list if
everything is present.

### Dotted Context Contracts

Frameworks that know more than top-level keys can compare a route or handler
contract against Kida's dotted dependency paths:

```python
from kida.analysis import check_context_contract

issues = check_context_contract(
    template,
    provided={"page.title", "page.author.name"},
    globals={"csrf_token"},
    optional={"flash.message"},
)

for issue in issues:
    print(issue.code, issue.path, issue.message)
```

`provided`, `globals`, and `optional` can be dotted-path iterables or nested
mappings. This checker is deliberately route-agnostic: frameworks supply the
contract shape, and Kida reports only whether template dependency paths are
covered. Enable `check_extra=True` when a narrow contract should also warn about
provided paths the template does not read.

## Literal Attribute Extraction

Framework adapters and CI reports can inspect literal HTML attributes without
parsing Kida's AST themselves:

```python
from kida.analysis import extract_literal_attributes

attrs = extract_literal_attributes(
    template,
    prefixes=("data-", "hx-"),
)

for attr in attrs:
    print(attr.tag, attr.name, attr.value, attr.lineno)
```

This reports only attributes visible in static template text. Dynamic attributes
such as `{{ attrs }}`, `xmlattr`, or helper-generated markup are intentionally
not inferred. Kida provides the generic facts; frameworks decide whether a
literal `data-*`, `hx-*`, `id`, or other attribute has route-specific meaning.

## Escape Audit

Use `audit_escaping()` to inventory where a template outputs escaped values,
where autoescape is disabled, and where filters intentionally produce trusted
markup:

```python
from kida.analysis import audit_escaping

for finding in audit_escaping(template):
    print(finding.code, finding.kind, finding.expression, finding.message)
```

Findings are static diagnostics only. They do not change rendering and do not
prove user input was sanitized. `| safe` findings include the optional
`reason=` text when present; missing reasons get a suggestion so code review and
CI reports can point at explicit trust boundaries.

## Privacy Lint

Use `lint_privacy()` to catch likely private data exposure before templates or
report fixtures are published:

```python
from kida.analysis import lint_privacy

for finding in lint_privacy(template):
    print(finding.code, finding.kind, finding.path, finding.message)
```

The first version is intentionally narrow. It reports sensitive-looking context
paths, secret-like string literals without echoing their values, `| safe` on
sensitive-looking values, broad debug context output, and dynamic template names
that a framework policy cannot statically allowlist.

## Call-Site Validation

Kida can validate `{% def %}` call sites at compile time, catching parameter errors
before any template is rendered:

```python
from kida import Environment

env = Environment(validate_calls=True)

template = env.from_string("""
    {% def button(text: str, url: str, style="primary") %}
        <a href="{{ url }}" class="btn btn-{{ style }}">{{ text }}</a>
    {% end %}

    {{ button(text="Save", urll="/save") }}
""")
# UserWarning: Call to 'button' at <string>:6 — unknown params: urll; missing required: url
```

### What It Checks

| Issue | Example |
|-------|---------|
| **Unknown params** | Calling `button(labl="X")` when param is `label` |
| **Missing required** | Calling `button()` when `text` has no default |
| **`*args` / `**kwargs` relaxation** | Definitions with `*args` or `**kwargs` suppress unknown-param warnings |

### Programmatic API

For build systems and CI pipelines, use the `BlockAnalyzer` directly:

```python
from kida import Environment, DictLoader
from kida.analysis import BlockAnalyzer

env = Environment(
    loader=DictLoader({"page.html": src}),
    preserve_ast=True,
)
template = env.get_template("page.html")

analyzer = BlockAnalyzer()
issues = analyzer.validate_calls(template._optimized_ast)

for issue in issues:
    if not issue.is_valid:
        print(f"{issue.def_name} at line {issue.lineno}: "
              f"unknown={issue.unknown_params}, "
              f"missing={issue.missing_required}")
```

### CallValidation

| Field | Type | Description |
|-------|------|-------------|
| `def_name` | `str` | Name of the called `{% def %}` |
| `lineno` | `int` | Line number of the call site |
| `col_offset` | `int` | Column offset of the call site |
| `unknown_params` | `tuple[str, ...]` | Keyword args not in the definition |
| `missing_required` | `tuple[str, ...]` | Required params not provided |
| `duplicate_params` | `tuple[str, ...]` | Params passed more than once; parser-produced ASTs reject these before validation |

| Property | Description |
|----------|-------------|
| `is_valid` | `True` if no issues were found |

---

## Configuration

Use `AnalysisConfig` to customize analysis behavior for your framework:

```python
from kida import AnalysisConfig
from kida.analysis import BlockAnalyzer

config = AnalysisConfig(
    # Variables indicating page-specific scope
    page_prefixes=frozenset({"post.", "post", "article.", "article"}),
    # Variables indicating site-wide scope
    site_prefixes=frozenset({"settings.", "settings", "global."}),
    # Additional functions your framework guarantees are pure
    extra_pure_functions=frozenset({"asset_url", "t", "current_lang"}),
    # Filters that produce non-deterministic output
    extra_impure_filters=frozenset({"random_choice"}),
)

analyzer = BlockAnalyzer(config=config)
```

Kida ships with a default config (`DEFAULT_CONFIG`) that includes common SSG
pure functions like `asset_url`, `t`, `canonical_url`, etc.

## Case Study: Bengal Static Site Generator

[Bengal](https://github.com/bengal-ssg/bengal) uses Kida's analysis API to
implement smart incremental builds:

:::{steps}
:::{step} Compile all templates

Enable AST preservation when compiling.

:::{/step}
:::{step} Analyze each template

Get block metadata for each compiled template.

:::{/step}
:::{step} Identify site-cacheable blocks

Find nav, footer, sidebar using `cache_scope == "site"`.

:::{/step}
:::{step} Cache site-scoped blocks

Cache once per build, reuse across all pages.

:::{/step}
:::{step} Re-render page-scoped blocks only

Only when page content changes.

:::{/step}
:::{step} Track dependencies

Invalidate caches when upstream data changes.

:::{/step}
:::{/steps}

This reduces full-site rebuild time by 40-60% for sites with shared navigation
and footer blocks.

## Case Study: Chirp Web Framework

[Chirp](https://github.com/lbliii/chirp) uses Kida's introspection and block APIs for dynamic web apps:

:::{steps}
:::{step} Composition planning

Use `template_metadata()` to discover blocks and inheritance before rendering.

:::{/step}
:::{step} Block validation

Call `validate_block_exists(env, template, block)` before `render_block()` to avoid KeyError.

:::{/step}
:::{step} Fragment rendering

Use `render_block()` for HTMX partial responses and Turbo Stream updates.

:::{/step}
:::{step} Layout assembly

Use `render_with_blocks()` to inject pre-rendered content into layout templates.

:::{/step}
:::{step} Adapter pattern

`KidaAdapter` implements Chirp's `TemplateAdapter` interface, wrapping all Kida APIs.

:::{/step}
:::{/steps}

See [Framework Integration](/docs/usage/framework-integration/) for the full adapter pattern and API usage.

## API Reference

### Template Methods

| Method | Signature | Description |
|---|---|---|
| `required_context()` | `() -> frozenset[str]` | Top-level variable names needed |
| `depends_on()` | `() -> frozenset[str]` | All dotted dependency paths |
| `validate_context()` | `(context: dict) -> list[str]` | Missing variable names |
| `check_context_contract()` | `(template, provided, ...) -> list[ContextContractIssue]` | Dotted context contract diagnostics |
| `extract_literal_attributes()` | `(template_or_ast, names=..., prefixes=...) -> list[LiteralAttribute]` | Literal HTML attributes with source locations |
| `audit_escaping()` | `(template_or_ast, include_output_sites=True) -> list[EscapeAuditFinding]` | Static escape and trusted-markup findings |
| `lint_privacy()` | `(template_or_ast) -> list[PrivacyFinding]` | Sensitive path, secret literal, and broad-output findings |
| `block_metadata()` | `() -> dict[str, BlockMetadata]` | Per-block analysis results |
| `template_metadata()` | `() -> TemplateMetadata \| None` | Full template analysis |
| `is_cacheable()` | `(block_name: str \| None) -> bool` | Cache safety check |
| `list_blocks()` | `() -> list[str]` | Block names in template |

### BlockMetadata

| Field | Type | Description |
|---|---|---|
| `name` | `str` | Block identifier |
| `depends_on` | `frozenset[str]` | Context paths accessed |
| `is_pure` | `"pure" \| "impure" \| "unknown"` | Determinism classification |
| `cache_scope` | `"site" \| "page" \| "none" \| "unknown"` | Recommended cache level |
| `emits_html` | `bool` | Whether block produces output |
| `emits_landmarks` | `frozenset[str]` | HTML5 landmarks detected |
| `inferred_role` | `str` | Heuristic role classification |
| `is_region` | `bool` | True if block is a `{% region %}` |
| `region_params` | `tuple[str, ...]` | Parameter names (regions only) |

### TemplateMetadata

| Field | Type | Description |
|---|---|---|
| `name` | `str \| None` | Template identifier |
| `extends` | `str \| None` | Parent template name |
| `blocks` | `dict[str, BlockMetadata]` | All block metadata |
| `top_level_depends_on` | `frozenset[str]` | Dependencies outside blocks |

| Method | Description |
|---|---|
| `all_dependencies()` | Union of all block and top-level dependencies |
| `get_block(name)` | Get metadata for a specific block |
| `regions()` | Return only region-typed blocks (for OOB discovery) |
| `cacheable_blocks()` | List of blocks where `is_cacheable()` is True |
| `site_cacheable_blocks()` | List of blocks with `cache_scope == "site"` |

### CallValidation

| Field | Type | Description |
|---|---|---|
| `def_name` | `str` | Name of the called `{% def %}` |
| `lineno` | `int` | Line number of the call site |
| `col_offset` | `int` | Column offset of the call site |
| `unknown_params` | `tuple[str, ...]` | Keyword args not in the definition |
| `missing_required` | `tuple[str, ...]` | Required params not provided |
| `duplicate_params` | `tuple[str, ...]` | Params passed more than once |

| Property | Type | Description |
|---|---|---|
| `is_valid` | `bool` | `True` if no issues were found |

### AnalysisConfig

| Field | Type | Default | Description |
|---|---|---|---|
| `page_prefixes` | `frozenset[str]` | `{"page.", "page", ...}` | Page-scope variable prefixes |
| `site_prefixes` | `frozenset[str]` | `{"site.", "site", ...}` | Site-scope variable prefixes |
| `extra_pure_functions` | `frozenset[str]` | `frozenset()` | Additional pure function names |
| `extra_impure_filters` | `frozenset[str]` | `frozenset()` | Additional impure filter names |

## Integration with Frameworks

Frameworks like [Chirp](https://github.com/lbliii/chirp) use Kida templates and run contract validation (`chirp check`) to verify that `hx-post`, `hx-get`, and `action` URLs in templates match registered routes. Dynamic URLs built with `~` or `{{ var }}` are correctly skipped — only literal URLs are validated.
