Static Analysis

Analyze templates for dependencies, purity, and caching potential

10 min read 2092 words

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 likepage.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

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

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

Validate Context Before Rendering

>>> 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

>>> 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

>>> 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.

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

required_context()extracts just the top-level names:

>>> 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 (usingrandom, 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:

>>> 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:

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:

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 optionalcan 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. Enablecheck_extra=Truewhen 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:

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 literaldata-*, hx-*, id, or other attribute has route-specific meaning.

Escape Audit

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

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.| safefindings 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

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

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,| safeon 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:

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 Callingbutton(labl="X") when param is label
Missing required Callingbutton() when texthas no default
*args / **kwargsrelaxation Definitions with*args or **kwargssuppress unknown-param warnings

Programmatic API

For build systems and CI pipelines, use theBlockAnalyzerdirectly:

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 Trueif no issues were found

Configuration

UseAnalysisConfigto customize analysis behavior for your framework:

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 likeasset_url, t, canonical_url, etc.

Case Study: Bengal Static Site Generator

Bengal uses Kida's analysis API to implement smart incremental builds:

  1. 1

    Compile all templates

    Enable AST preservation when compiling.

  2. 2

    Analyze each template

    Get block metadata for each compiled template.

  3. 3

    Identify site-cacheable blocks

    Find nav, footer, sidebar usingcache_scope == "site".

  4. 4

    Cache site-scoped blocks

    Cache once per build, reuse across all pages.

  5. 5

    Re-render page-scoped blocks only

    Only when page content changes.

  6. 6

    Track dependencies

    Invalidate caches when upstream data changes.

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

Case Study: Chirp Web Framework

Chirp uses Kida's introspection and block APIs for dynamic web apps:

  1. 1

    Composition planning

    Usetemplate_metadata()to discover blocks and inheritance before rendering.

  2. 2

    Block validation

    Callvalidate_block_exists(env, template, block) before render_block()to avoid KeyError.

  3. 3

    Fragment rendering

    Userender_block()for HTMX partial responses and Turbo Stream updates.

  4. 4

    Layout assembly

    Userender_with_blocks()to inject pre-rendered content into layout templates.

  5. 5

    Adapter pattern

    KidaAdapter implements Chirp's TemplateAdapterinterface, wrapping all Kida APIs.

See 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 whereis_cacheable()is True
site_cacheable_blocks() List of blocks withcache_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 Trueif 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 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.