Open-Source Wikis

/

Ruff

/

Crates

/

`ruff_python_semantic`

astral-sh/ruff

ruff_python_semantic

Name resolution, scope graph, and the SemanticModel that lint rules query. Source: crates/ruff_python_semantic/.

Purpose

This crate answers questions like "what does this name refer to?", "which scope does this binding belong to?", and "is this expression statically a typing.TYPE_CHECKING import?". It's the layer between the raw AST and the lint rules.

Directory layout

crates/ruff_python_semantic/src/
├── lib.rs
├── analyze/       # high-level helpers (typing, branch detection, function arguments)
├── binding.rs     # Binding, BindingKind, scoping
├── scope.rs       # Scope hierarchy
├── model.rs       # SemanticModel (the public façade)
├── definition.rs
├── reference.rs
├── globals.rs
├── nodes.rs
└── ...

Key abstractions

Type Purpose
SemanticModel Façade exposed to lint rules. Knows current scope, all bindings, references, branch state.
Binding, BindingKind Records of names introduced into a scope (import, assignment, function, class, …).
Scope, ScopeKind Module / function / class / lambda / type-param / comprehension scopes.
Reference A use-site of a name, linked back to its binding.
analyze::typing Helpers like is_typing_extension, match_typing_call, is_known_unsubscriptable.
analyze::branch_detection "Are we inside an if False: block?" style queries.

How it integrates with the linter

The AST checker (crates/ruff_linter/src/checkers/ast.rs) instantiates a SemanticModel, walks the AST, and updates the model in place: pushing scopes, recording bindings as it sees them, and then offering checker.semantic() to each rule.

Most rules look like:

fn rule_handler(checker: &mut Checker, expr: &Expr) {
    let semantic = checker.semantic();
    if let Some(binding) = semantic.resolve_name(...) {
        // emit diagnostic
    }
}

This pattern is repeated hundreds of times. Look at crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs for a representative example.

Typing helpers

The analyze::typing module knows about:

  • typing and typing_extensions re-exports
  • TYPE_CHECKING blocks
  • PEP 604 union syntax
  • PEP 695 type aliases
  • Annotated, Final, ClassVar, Literal, TypeGuard, TypeIs, TypedDict, NamedTuple
  • dataclasses, enum, pydantic, attr patterns (where the rule needs to recognize them)

Rules that need type-aware behavior (e.g. flake8-type-checking, flake8-pyi, pyupgrade) lean heavily on this module.

Branch detection

SemanticModel tracks whether the current statement is inside a branch that's statically reachable. analyze::branch_detection lets a rule decide whether a redefinition is a real shadow or just an if/else defensive pattern.

Integration points

  • Used by ruff_linter. Some rules also pull from this crate directly (without going through Checker).
  • Used by ty_python_semantic — ty consumes the same scope/binding primitives, layered with type information.

Modifying

  • Adding a new "is this a typing X?" helper goes in analyze/typing.rs. Add unit tests next to it.
  • The semantic model's public API is consumed by hundreds of rules, so be careful with breaking changes; prefer adding methods over removing or renaming.
  • For new scope kinds (e.g. PEP 695 type-param scopes), update scope.rs and the AST checker.

For consumer patterns, see ruff_linter.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

`ruff_python_semantic` – Ruff wiki | Factory