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:
typingandtyping_extensionsre-exportsTYPE_CHECKINGblocks- PEP 604 union syntax
- PEP 695 type aliases
Annotated,Final,ClassVar,Literal,TypeGuard,TypeIs,TypedDict,NamedTupledataclasses,enum,pydantic,attrpatterns (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 throughChecker). - 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.rsand 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.