Skip to content

ColonTargets

ColonTargets constructs alignment members at every : context the alignment and singleton rules consume. The distinct contexts in the Python grammar that carry a colon worth aligning are listed below, and the walker that finds them is identical across rules, so the walker lives once in ColonTargets and each consuming rule supplies a receiver that handles the discovered members.

Public Surface

ColonTargets lives at crate/src/primitives/colon_targets.rs and is pub(crate). Two consumers use it today:

(which aligns multi-item groups in every context) and (which strips pre-colon padding from groups that have no column to align to). The downstream-visible consequence is the rewrites both rules emit through the diagnostic stream.

At 1.0 the trait promotes to pub, so a downstream can implement a :-context rule of its own.

The Contexts

  1. Dict items. {key: value, key: value} literals, where each key: value pair contributes a member.
  2. Annotated assignments. Statements of the form target: Annotation = default in any scope (module, function, or class body), where each annotation colon contributes a member.
  3. Annotated function parameters. def f(param: T, param: T) signatures, where each annotated parameter contributes a member.
  4. Google-style docstring sections. Every Args:, Returns:, Raises:, or other Title-case-headed section, where each name: description entry line contributes a member and each section aligns independently.
  5. Match-arm cases. match x: case Pattern: ..., where each case's pattern-to-body colon contributes a member.

Each context resolves a ColonMember, pairing the pre-colon alignment member (its width the display-column width of the left-hand side, its gap the whitespace immediately before the colon) with an optional post-colon value_gap an aligned or stripped row rewrites to one space.

Internal Surface

The receiver trait carries the per-context handlers. rule and handle are the required methods, rule naming the consuming rule so the group builders can hold its skip-suppressed rows out of alignment, while match_arms carries a default a consuming rule overrides for match-arm-specific handling:

rust
pub(crate) trait ColonEmitter {
    fn handle(&mut self, members: &[ColonMember]);

    fn match_arms(&mut self, members: &[ColonMember]) {
        self.handle(members);
    }

    fn rule(&self) -> RuleId;

    fn walk(&mut self, source: &Source) where Self: Sized { /* provided */ }
}

handle is the catch-all for annotated assignments, docstring entries, dict entries, and parameters. match_arms is split out so a rule can opt out of match-arm alignment by overriding it to a no-op (which is what

does, since owns the match-arm context), with its default delegating to handle for any rule that wants the unified callback.

walk(source) is the provided driver across source's module body, recursing into nested classes, functions, matches, and expressions so a single call covers the whole tree. A consuming rule never overrides walk, because calling the provided method is enough to drive the receiver across every relevant context.

match_case(source, case) -> Option<aligner::Member> is exposed pub(crate) alongside the trait for

, which builds members one match arm at a time rather than through the receiver shape. New rules whose grouping logic is contiguous-line-shaped should reach for the trait, whereas rules that emit one-member-per-construct should reach for match_case directly.

Build Pattern

A rule implementing ColonEmitter carries a single accumulator (typically Vec<Vec<ColonMember>> for grouped members) and pushes into it from each handler. After walk(source) returns, the accumulator carries every group the rule cares about, and the rule emits Vec<Edit> by calling Aligner's emit_group against each group.

How Grouping Works

Each context defines its own grouping shape, because what counts as "adjacent" inside a dict literal differs from what counts as "adjacent" across class-body statements:

  1. Dict items group by line-adjacency between one key's end and the next item's start. A trailing comment rides with its entry and a **spread entry skips the colon scan, neither breaking the run, so the rest of the dict aligns around them.
  2. Annotated assignments group via line_adjacent_groups over each scope's statements, treating any non-target: T statement as a divider.
  3. Annotated function parameters group via parameter_split_groups, splitting at the first parameter that does not qualify (an un-annotated argument, a *args or **kwargs, a / or * separator).
  4. Match arms group one per match statement, with every arm's colon contributing a member. Patterns may span multiple lines, so the alignment column is per-match rather than per-line-run.
  5. Docstring sections group one per Google-style section, with the structured-section parser invoked inline to find each section's entries, so a section's entries align without reaching across the section break.

Each group is handed to the receiver as one &[ColonMember] slice, so the consumer aligns within the group without seeing cross-group state. The docstring-args context borrows Docstring's body_docstring to find a body's leading docstring literal, then runs its own line scan for each entry's : position, because the two primitives surface different shapes. Docstring yields entry names with the byte range a reorder carries along, whereas the colon walker yields each line's colon anchor for the aligner's padding math.

Re-Using This Primitive

A new :-context rule implements ColonEmitter, overrides the handlers for the contexts it cares about, and calls walk(source) from inside its apply method. The shared walker, the same-indentation grouping, and the per-context member construction come for free.

  • Aligner is the math the produced Member lists feed into.
  • aligns multi-item groups across every context.
  • strips padding from singleton groups.
  • owns the match-arm context exclusively.
  • The =-context sibling builds its members in equal_targets, described under Aligner.