use ruff_db::files::{File, FilePath}; use ruff_db::source::{line_index, source_text}; use ruff_python_ast::{self as ast, ExprStringLiteral, ModExpression}; use ruff_python_ast::{Expr, ExprRef, HasNodeIndex, name::Name}; use ruff_python_parser::Parsed; use ruff_source_file::LineIndex; use rustc_hash::FxHashMap; use crate::Db; use crate::module_name::ModuleName; use crate::module_resolver::{KnownModule, Module, list_modules, resolve_module}; use crate::semantic_index::definition::Definition; use crate::semantic_index::scope::FileScopeId; use crate::semantic_index::semantic_index; use crate::types::ide_support::{Member, all_declarations_and_bindings, all_members}; use crate::types::{Type, binding_type, infer_scope_types}; /// The primary interface the LSP should use for querying semantic information about a [`File`]. /// /// Although you can in principle freely construct this type given a `db` and `file`, you should /// try to construct this at the start of your analysis and thread the same instance through /// the full analysis. /// /// The primary reason for this is that it manages traversing into the sub-ASTs of string /// annotations (see [`Self::enter_string_annotation`]). When you do this you will be handling /// AST nodes that don't belong to the file's AST (or *any* file's AST). These kinds of nodes /// will result in panics and confusing results if handed to the wrong subsystem. `SemanticModel` /// methods will automatically handle using the string literal's AST node when necessary. pub struct SemanticModel<'db> { db: &'db dyn Db, file: File, /// If `Some` then this `SemanticModel` is for analyzing the sub-AST of a string annotation. /// This expression will be used as a witness to the scope/location we're analyzing. in_string_annotation_expr: Option>, } impl<'db> SemanticModel<'db> { pub fn new(db: &'db dyn Db, file: File) -> Self { Self { db, file, in_string_annotation_expr: None, } } pub fn db(&self) -> &'db dyn Db { self.db } pub fn file(&self) -> File { self.file } pub fn file_path(&self) -> &FilePath { self.file.path(self.db) } pub fn line_index(&self) -> LineIndex { line_index(self.db, self.file) } /// Returns a map from symbol name to that symbol's /// type and definition site (if available). /// /// The symbols are the symbols in scope at the given /// AST node. pub fn members_in_scope_at( &self, node: ast::AnyNodeRef<'_>, ) -> FxHashMap> { let index = semantic_index(self.db, self.file); let mut members = FxHashMap::default(); let Some(file_scope) = self.scope(node) else { return members; }; for (file_scope, _) in index.ancestor_scopes(file_scope) { for memberdef in all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file)) { members.insert( memberdef.member.name, MemberDefinition { ty: memberdef.member.ty, definition: memberdef.definition, }, ); } } members } /// Resolve the given import made in this file to a Type pub fn resolve_module_type(&self, module: Option<&str>, level: u32) -> Option> { let module = self.resolve_module(module, level)?; Some(Type::module_literal(self.db, self.file, module)) } /// Resolve the given import made in this file to a Module pub fn resolve_module(&self, module: Option<&str>, level: u32) -> Option> { let module_name = ModuleName::from_identifier_parts(self.db, self.file, module, level).ok()?; resolve_module(self.db, self.file, &module_name) } /// Returns completions for symbols available in a `import ` context. pub fn import_completions(&self) -> Vec> { list_modules(self.db) .into_iter() .map(|module| { let builtin = module.is_known(self.db, KnownModule::Builtins); let ty = Type::module_literal(self.db, self.file, module); Completion { name: Name::new(module.name(self.db).as_str()), ty: Some(ty), builtin, } }) .collect() } /// Returns completions for symbols available in a `from module import ` context. pub fn from_import_completions(&self, import: &ast::StmtImportFrom) -> Vec> { let module_name = match ModuleName::from_import_statement(self.db, self.file, import) { Ok(module_name) => module_name, Err(err) => { tracing::debug!( "Could not extract module name from `{module:?}` with level {level}: {err:?}", module = import.module, level = import.level, ); return vec![]; } }; self.module_completions(&module_name) } /// Returns submodule-only completions for the given module. pub fn import_submodule_completions_for_name( &self, module_name: &ModuleName, ) -> Vec> { let Some(module) = resolve_module(self.db, self.file, module_name) else { tracing::debug!("Could not resolve module from `{module_name:?}`"); return vec![]; }; self.submodule_completions(&module) } /// Returns completions for symbols available in the given module as if /// it were imported by this model's `File`. fn module_completions(&self, module_name: &ModuleName) -> Vec> { let Some(module) = resolve_module(self.db, self.file, module_name) else { tracing::debug!("Could not resolve module from `{module_name:?}`"); return vec![]; }; let ty = Type::module_literal(self.db, self.file, module); let builtin = module.is_known(self.db, KnownModule::Builtins); let mut completions = vec![]; for Member { name, ty } in all_members(self.db, ty) { completions.push(Completion { name, ty: Some(ty), builtin, }); } completions.extend(self.submodule_completions(&module)); completions } /// Returns completions for submodules of the given module. fn submodule_completions(&self, module: &Module<'db>) -> Vec> { let builtin = module.is_known(self.db, KnownModule::Builtins); let mut completions = vec![]; for submodule in module.all_submodules(self.db) { let ty = Type::module_literal(self.db, self.file, *submodule); let Some(base) = submodule.name(self.db).components().next_back() else { continue; }; completions.push(Completion { name: Name::new(base), ty: Some(ty), builtin, }); } completions } /// Returns completions for symbols available in a `object.` context. pub fn attribute_completions(&self, node: &ast::ExprAttribute) -> Vec> { let ty = node.value.inferred_type(self); all_members(self.db, ty) .into_iter() .map(|member| Completion { name: member.name, ty: Some(member.ty), builtin: false, }) .collect() } /// Returns completions for symbols available in the scope containing the /// given expression. /// /// If a scope could not be determined, then completions for the global /// scope of this model's `File` are returned. pub fn scoped_completions(&self, node: ast::AnyNodeRef<'_>) -> Vec> { let index = semantic_index(self.db, self.file); let Some(file_scope) = self.scope(node) else { return vec![]; }; let mut completions = vec![]; for (file_scope, _) in index.ancestor_scopes(file_scope) { completions.extend( all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file)) .map(|memberdef| Completion { name: memberdef.member.name, ty: Some(memberdef.member.ty), builtin: false, }), ); } // Builtins are available in all scopes. let builtins = ModuleName::new("builtins").expect("valid module name"); completions.extend(self.module_completions(&builtins)); completions } /// Get the scope of the given node (handles string annotations) pub fn scope(&self, node: ast::AnyNodeRef<'_>) -> Option { let index = semantic_index(self.db, self.file); match self.node_in_ast(node) { ast::AnyNodeRef::Identifier(identifier) => index.try_expression_scope_id(identifier), node => match node.as_expr_ref() { // If we couldn't identify a specific // expression that we're in, then just // fall back to the global scope. None => Some(FileScopeId::global()), Some(expr) => index.try_expression_scope_id(&expr), }, } } /// Get a "safe" [`ast::AnyNodeRef`] to use for referring to the given (sub-)AST node. /// /// If we're analyzing a string annotation, it will return the string literal's node. /// Otherwise it will return the input. pub fn node_in_ast<'a>(&'a self, node: ast::AnyNodeRef<'a>) -> ast::AnyNodeRef<'a> { if let Some(string_annotation) = &self.in_string_annotation_expr { (&**string_annotation).into() } else { node } } /// Get a "safe" [`Expr`] to use for referring to the given (sub-)expression. /// /// If we're analyzing a string annotation, it will return the string literal's expression. /// Otherwise it will return the input. pub fn expr_in_ast<'a>(&'a self, expr: &'a Expr) -> &'a Expr { if let Some(string_annotation) = &self.in_string_annotation_expr { string_annotation } else { expr } } /// Get a "safe" [`ExprRef`] to use for referring to the given (sub-)expression. /// /// If we're analyzing a string annotation, it will return the string literal's expression. /// Otherwise it will return the input. pub fn expr_ref_in_ast<'a>(&'a self, expr: ExprRef<'a>) -> ExprRef<'a> { if let Some(string_annotation) = &self.in_string_annotation_expr { ExprRef::from(string_annotation) } else { expr } } /// Given a string expression, determine if it's a string annotation, and if it is, /// yield the parsed sub-AST and a sub-model that knows it's analyzing a sub-AST. /// /// Analysis of the sub-AST should only be done with the sub-model, or else things /// may return nonsense results or even panic! pub fn enter_string_annotation( &self, string_expr: &ExprStringLiteral, ) -> Option<(Parsed, Self)> { // String annotations can't contain string annotations if self.in_string_annotation_expr.is_some() { return None; } // Ask the inference engine whether this is actually a string annotation let expr = ExprRef::StringLiteral(string_expr); let index = semantic_index(self.db, self.file); let file_scope = index.expression_scope_id(&expr); let scope = file_scope.to_scope_id(self.db, self.file); if !infer_scope_types(self.db, scope).is_string_annotation(expr) { return None; } // Parse the sub-AST and create a semantic model that knows it's in a sub-AST // // The string_annotation will be used as the expr/node for any query that needs // to look up a node in the AST to prevent panics, because these sub-AST nodes // are not in the File's AST! let source = source_text(self.db, self.file); let string_literal = string_expr.as_single_part_string()?; let ast = ruff_python_parser::parse_string_annotation(source.as_str(), string_literal).ok()?; let model = Self { db: self.db, file: self.file, in_string_annotation_expr: Some(Box::new(Expr::StringLiteral(string_expr.clone()))), }; Some((ast, model)) } } /// The type and definition (if available) of a symbol. #[derive(Clone, Debug)] pub struct MemberDefinition<'db> { pub ty: Type<'db>, pub definition: Option>, } /// A classification of symbol names. /// /// The ordering here is used for sorting completions. /// /// This sorts "normal" names first, then dunder names and finally /// single-underscore names. This matches the order of the variants defined for /// this enum, which is in turn picked up by the derived trait implementation /// for `Ord`. #[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] pub enum NameKind { Normal, Dunder, Sunder, } impl NameKind { pub fn classify(name: &Name) -> NameKind { // Dunder needs a prefix and suffix double underscore. // When there's only a prefix double underscore, this // results in explicit name mangling. We let that be // classified as-if they were single underscore names. // // Ref: if name.starts_with("__") && name.ends_with("__") { NameKind::Dunder } else if name.starts_with('_') { NameKind::Sunder } else { NameKind::Normal } } } /// A suggestion for code completion. #[derive(Clone, Debug)] pub struct Completion<'db> { /// The label shown to the user for this suggestion. pub name: Name, /// The type of this completion, if available. /// /// Generally speaking, this is always available /// *unless* this was a completion corresponding to /// an unimported symbol. In that case, computing the /// type of all such symbols could be quite expensive. pub ty: Option>, /// Whether this suggestion came from builtins or not. /// /// At time of writing (2025-06-26), this information /// doesn't make it into the LSP response. Instead, we /// use it mainly in tests so that we can write less /// noisy tests. pub builtin: bool, } impl<'db> Completion<'db> { pub fn is_type_check_only(&self, db: &'db dyn Db) -> bool { self.ty.is_some_and(|ty| ty.is_type_check_only(db)) } } pub trait HasType { /// Returns the inferred type of `self`. /// /// ## Panics /// May panic if `self` is from another file than `model`. fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db>; } pub trait HasDefinition { /// Returns the inferred type of `self`. /// /// ## Panics /// May panic if `self` is from another file than `model`. fn definition<'db>(&self, model: &SemanticModel<'db>) -> Definition<'db>; } impl HasType for ast::ExprRef<'_> { fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { let index = semantic_index(model.db, model.file); // TODO(#1637): semantic tokens is making this crash even with // `try_expr_ref_in_ast` guarding this, for now just use `try_expression_scope_id`. // The problematic input is `x: "float` (with a dangling quote). I imagine the issue // is we're too eagerly setting `is_string_annotation` in inference. let Some(file_scope) = index.try_expression_scope_id(&model.expr_ref_in_ast(*self)) else { return Type::unknown(); }; let scope = file_scope.to_scope_id(model.db, model.file); infer_scope_types(model.db, scope).expression_type(*self) } } macro_rules! impl_expression_has_type { ($ty: ty) => { impl HasType for $ty { #[inline] fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { let expression_ref = ExprRef::from(self); expression_ref.inferred_type(model) } } }; } impl_expression_has_type!(ast::ExprBoolOp); impl_expression_has_type!(ast::ExprNamed); impl_expression_has_type!(ast::ExprBinOp); impl_expression_has_type!(ast::ExprUnaryOp); impl_expression_has_type!(ast::ExprLambda); impl_expression_has_type!(ast::ExprIf); impl_expression_has_type!(ast::ExprDict); impl_expression_has_type!(ast::ExprSet); impl_expression_has_type!(ast::ExprListComp); impl_expression_has_type!(ast::ExprSetComp); impl_expression_has_type!(ast::ExprDictComp); impl_expression_has_type!(ast::ExprGenerator); impl_expression_has_type!(ast::ExprAwait); impl_expression_has_type!(ast::ExprYield); impl_expression_has_type!(ast::ExprYieldFrom); impl_expression_has_type!(ast::ExprCompare); impl_expression_has_type!(ast::ExprCall); impl_expression_has_type!(ast::ExprFString); impl_expression_has_type!(ast::ExprTString); impl_expression_has_type!(ast::ExprStringLiteral); impl_expression_has_type!(ast::ExprBytesLiteral); impl_expression_has_type!(ast::ExprNumberLiteral); impl_expression_has_type!(ast::ExprBooleanLiteral); impl_expression_has_type!(ast::ExprNoneLiteral); impl_expression_has_type!(ast::ExprEllipsisLiteral); impl_expression_has_type!(ast::ExprAttribute); impl_expression_has_type!(ast::ExprSubscript); impl_expression_has_type!(ast::ExprStarred); impl_expression_has_type!(ast::ExprName); impl_expression_has_type!(ast::ExprList); impl_expression_has_type!(ast::ExprTuple); impl_expression_has_type!(ast::ExprSlice); impl_expression_has_type!(ast::ExprIpyEscapeCommand); impl HasType for ast::Expr { fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { match self { Expr::BoolOp(inner) => inner.inferred_type(model), Expr::Named(inner) => inner.inferred_type(model), Expr::BinOp(inner) => inner.inferred_type(model), Expr::UnaryOp(inner) => inner.inferred_type(model), Expr::Lambda(inner) => inner.inferred_type(model), Expr::If(inner) => inner.inferred_type(model), Expr::Dict(inner) => inner.inferred_type(model), Expr::Set(inner) => inner.inferred_type(model), Expr::ListComp(inner) => inner.inferred_type(model), Expr::SetComp(inner) => inner.inferred_type(model), Expr::DictComp(inner) => inner.inferred_type(model), Expr::Generator(inner) => inner.inferred_type(model), Expr::Await(inner) => inner.inferred_type(model), Expr::Yield(inner) => inner.inferred_type(model), Expr::YieldFrom(inner) => inner.inferred_type(model), Expr::Compare(inner) => inner.inferred_type(model), Expr::Call(inner) => inner.inferred_type(model), Expr::FString(inner) => inner.inferred_type(model), Expr::TString(inner) => inner.inferred_type(model), Expr::StringLiteral(inner) => inner.inferred_type(model), Expr::BytesLiteral(inner) => inner.inferred_type(model), Expr::NumberLiteral(inner) => inner.inferred_type(model), Expr::BooleanLiteral(inner) => inner.inferred_type(model), Expr::NoneLiteral(inner) => inner.inferred_type(model), Expr::EllipsisLiteral(inner) => inner.inferred_type(model), Expr::Attribute(inner) => inner.inferred_type(model), Expr::Subscript(inner) => inner.inferred_type(model), Expr::Starred(inner) => inner.inferred_type(model), Expr::Name(inner) => inner.inferred_type(model), Expr::List(inner) => inner.inferred_type(model), Expr::Tuple(inner) => inner.inferred_type(model), Expr::Slice(inner) => inner.inferred_type(model), Expr::IpyEscapeCommand(inner) => inner.inferred_type(model), } } } macro_rules! impl_binding_has_ty_def { ($ty: ty) => { impl HasDefinition for $ty { #[inline] fn definition<'db>(&self, model: &SemanticModel<'db>) -> Definition<'db> { let index = semantic_index(model.db, model.file); index.expect_single_definition(self) } } impl HasType for $ty { #[inline] fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { let binding = HasDefinition::definition(self, model); binding_type(model.db, binding) } } }; } impl_binding_has_ty_def!(ast::StmtFunctionDef); impl_binding_has_ty_def!(ast::StmtClassDef); impl_binding_has_ty_def!(ast::Parameter); impl_binding_has_ty_def!(ast::ParameterWithDefault); impl_binding_has_ty_def!(ast::ExceptHandlerExceptHandler); impl_binding_has_ty_def!(ast::TypeParamTypeVar); impl HasType for ast::Alias { fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> { if &self.name == "*" { return Type::Never; } let index = semantic_index(model.db, model.file); binding_type(model.db, index.expect_single_definition(self)) } } /// Implemented by types for which the semantic index tracks their scope. pub(crate) trait HasTrackedScope: HasNodeIndex {} impl HasTrackedScope for ast::Expr {} impl HasTrackedScope for ast::ExprRef<'_> {} impl HasTrackedScope for &ast::ExprRef<'_> {} // We never explicitly register the scope of an `Identifier`. // However, `ExpressionsScopeMap` stores the text ranges of each scope. // That allows us to look up the identifier's scope for as long as it's // inside an expression (because the ranges overlap). impl HasTrackedScope for ast::Identifier {} #[cfg(test)] mod tests { use ruff_db::files::system_path_to_file; use ruff_db::parsed::parsed_module; use crate::db::tests::TestDbBuilder; use crate::{HasType, SemanticModel}; #[test] fn function_type() -> anyhow::Result<()> { let db = TestDbBuilder::new() .with_file("/src/foo.py", "def test(): pass") .build()?; let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); let ast = parsed_module(&db, foo).load(&db); let function = ast.suite()[0].as_function_def_stmt().unwrap(); let model = SemanticModel::new(&db, foo); let ty = function.inferred_type(&model); assert!(ty.is_function_literal()); Ok(()) } #[test] fn class_type() -> anyhow::Result<()> { let db = TestDbBuilder::new() .with_file("/src/foo.py", "class Test: pass") .build()?; let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); let ast = parsed_module(&db, foo).load(&db); let class = ast.suite()[0].as_class_def_stmt().unwrap(); let model = SemanticModel::new(&db, foo); let ty = class.inferred_type(&model); assert!(ty.is_class_literal()); Ok(()) } #[test] fn alias_type() -> anyhow::Result<()> { let db = TestDbBuilder::new() .with_file("/src/foo.py", "class Test: pass") .with_file("/src/bar.py", "from foo import Test") .build()?; let bar = system_path_to_file(&db, "/src/bar.py").unwrap(); let ast = parsed_module(&db, bar).load(&db); let import = ast.suite()[0].as_import_from_stmt().unwrap(); let alias = &import.names[0]; let model = SemanticModel::new(&db, bar); let ty = alias.inferred_type(&model); assert!(ty.is_class_literal()); Ok(()) } }