AST matchers

The matcher catalog for styleguide rules. A matcher is a predicate over abstract syntax tree (AST) nodes. It wraps a (node, parents) -> bool test, so M.calls("zip") asks “is this AST node a call to zip?” Matchers compose with boolean algebra (&, |, ~), refine with .where(...), and terminate with .over(tree), .violations(tree), or .exists(tree). Import as from captain_hook.style import matchers as M.

Node types

Matcher Matches
M.module The file root (ast.Module)
M.cls class definitions
M.func def / async def
M.definition Classes or functions (M.cls \| M.func)
M.imports import / from … import
M.call Call expressions
M.assignment x = … and x: T = …
M.control_flow if / for / while / with / try / except (and async)
M.type_checking if TYPE_CHECKING: blocks
M.future_annotations Modules with from __future__ import annotations
M.kind(*types, label=None) Any of the given ast node types

Calls & references

Matcher Matches
M.calls(name) A call to the bare-name function name
M.kwarg(name) A call passing keyword argument name
M.ref(name) A bare-name reference (e.g. Any inside an annotation)

Names

Matcher Matches
M.named(pattern) A class/func/assignment/param whose name matches the regex
M.private Single-leading-underscore names (not dunder)
M.dunder __dunder__ names
M.constant SCREAMING_SNAKE names

Annotations

Matcher Matches
M.annotated(inner=None) An annotated variable, parameter, or return (excludes *args/**kwargs); inner constrains the annotation
M.forward_ref String forward references inside annotations

Position

Matcher Matches
M.under(other) A node with any ancestor matching other
M.child_of(other) A node whose immediate parent matches other
M.following(boundary) A body statement after the first sibling matching boundary

Combinators & terminals

Operator Behavior
a & b Both must match
a \| b Either matches
~a Must not match
a.where(predicate) Refine with a node-local predicate
m.over(tree) Yield every matching node
m.violations(tree, label=None) Yield a Violation per match
m.diff(pre, post, key=ast.unparse, label=None) Yield matches present in post but absent in pre (for diff rules)
m.exists(tree) Whether any node matches
m.matches(node) Whether a single node matches; node-local matchers only, structural matchers raise

Examples

Block imports nested inside control flow, but allow if TYPE_CHECKING::

from captain_hook.style import StyleRule, matchers as M

class NoNestedImports(StyleRule):
    """Lazy imports go at the top of the function body, not inside if/for/try."""
    match = M.imports & M.child_of(M.control_flow) & ~M.under(M.type_checking)

Flag only edits that newly widen a typed slot to Any (a diff rule):

from captain_hook.style import StyleDiffRule, matchers as M

class NoWeakeningToAny(StyleDiffRule):
    """Don't widen a typed slot to Any to silence the type checker."""
    def check(self, pre, post):
        yield from M.annotated(M.ref("Any")).diff(pre, post)