Skip to content

BindingAnalysis

BindingAnalysis walks the module once during Source construction and records, for every name introduced or shadowed in a lexical scope, the offsets of every write and read. Several rules read from this table to ask binding-shaped questions, and the single-walk-per-source guarantee is what makes adding new binding-aware rules cheap.

Public Surface

The BindingAnalysis type itself is pub and re-exported at the crate root as prose::BindingAnalysis, so a downstream consumer can hold a reference to one through Source::binding_analysis. The accessor methods on the type are pub(crate) today, so the in-process API is reachable from within the Prose crate but not from a downstream Rust caller.

A downstream consumer in 0.2.x can:

  • Pass a Source into Pipeline::run and read diagnostics emitted by binding-aware rules like .
  • Observe that the BindingAnalysis type exists and is reachable through source.binding_analysis().

A downstream consumer in 0.2.x cannot:

  • Call assignment_count, usage_count, binding_kinds, binding_name, bindings_in_scope, first_write_offset, is_defined_before, or module_function_reads on the returned reference. Every reader is pub(crate).
  • Implement a custom rule that consumes the binding table. The Rule trait is pub(crate).

The methods stabilize toward 1.0, where every reader becomes pub and the Rule trait opens so downstream consumers can implement project-specific binding-aware rules.

Internal Surface

For consumers reading this from within the Prose crate (or for readers curious about the surface that will widen at 1.0), the table indexes per binding:

  • assignment_count(binding: BindingId) -> usize counts every write site, including the introducing assignment.
  • usage_count(binding: BindingId) -> usize counts every read site.
  • binding_kinds(binding: BindingId) -> &[BindingKind] returns each kind that produced this binding (a single binding may carry several kinds when shadowing or augmented assignment is involved).
  • binding_name(binding: BindingId) -> &str returns the bound name.
  • bindings_in_scope(stmt: &Stmt) -> impl Iterator<Item = BindingId> lists every binding introduced in the lexical scope that contains the statement.
  • first_write_offset(binding: BindingId) -> TextSize returns the offset of the first write.
  • is_defined_before(name: &str, offset: TextSize) -> bool is the inverse-lookup convenience used by when checking that every name appearing in an annotation resolves to a binding introduced earlier.
  • module_function_reads(name: &str) -> Option<&[TextSize]> returns the read offsets of a module-scope name bound exactly once as a function definition, which 's call-site rewrite uses to resolve a reordered function's in-module call sites.

The supporting types BindingId, ScopeId, BindingKind, ScopeKind, Binding, and Scope are also pub(crate) in 0.2.x. BindingKind enumerates the categories of write event the table records: Assignment, AugAssign, ClassDef, Comprehension, ExceptHandler, For, FunctionDef, Import, Parameter, Walrus, With. ScopeKind covers Class, Comprehension, Function, Module, matching Python's lexical-scope categories.

Build Pattern

BindingAnalysis::new(module: &ModModule) runs the resolution pass once during Source construction. The pass walks the AST in source order, tracks every introduction and shadow per lexical scope, and indexes writes and reads by offset. The result is owned by the enclosing Source and handed to consuming rules as &BindingAnalysis.

A fresh analysis is built each time Source is constructed or reparsed, so the offsets a rule reads always match the Source it's running against. Inside one rule's apply the table is immutable, and across rules the pipeline reparses, which rebuilds the analysis against the new text, so a rule that depends on a previous rule's edits sees a fresh table reflecting the rewritten source.

Re-Using This Primitive

is the first rule to consume the table, counting writes and reads per binding to surface candidates for inlining. Future rules with binding-shaped questions (unused imports, shadowing detection, ahead-of-use references, dead-store analysis) reach for the same primitive without re-walking. The single-walk-per-source guarantee is what makes adding new binding-shaped rules cheap.

The Cargo dependency line (prose = { git = "...", tag = "<version>" }) lives on the Source page. In 0.2.x the consumption path runs indirectly through diagnostics emitted by binding-aware rules rather than through direct method calls, and at 1.0 the readers open up so a downstream rule can query the table itself.

  • Source is the input the analysis builds against, with every binding's offset landing inside the source's text.
  • is the canonical consumer.
  • Edit is the output shape binding-aware rules emit, with each edit's range named against an offset the analysis indexes.
  • Pipeline drives the rule run that calls into the analysis.
  • RuleId is the handle each rule registers under in the pipeline's ordering.

For the underlying rules catalog, the Rules page walks every shipped rule across categories, including the binding-aware rules that read from this table.