# AST matchers

The matcher catalog for [styleguide rules](../../docs/guide/primitives.md#style-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](../../reference/files-commands.md#captain_hook.Call.name) |
| `M.kwarg(name)` | A call passing keyword argument [name](../../reference/files-commands.md#captain_hook.Call.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](../../reference/primitives.md#captain_hook.style.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:`:

``` python
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):

``` python
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)
```
