From 78dff3b0d189186610b65bd7168895c6652d2cb5 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Thu, 11 Sep 2025 13:16:47 +0100 Subject: [PATCH 01/21] Inlay hint call argument location --- crates/ty_ide/src/inlay_hints.rs | 42 ++++++-- crates/ty_ide/src/signature_help.rs | 16 +-- .../src/types/ide_support.rs | 40 ++++++- .../src/server/api/requests/inlay_hints.rs | 26 +++-- crates/ty_server/tests/e2e/inlay_hints.rs | 15 ++- crates/ty_wasm/src/lib.rs | 70 +++++++++--- playground/ty/src/Editor/Editor.tsx | 101 ++++++++++-------- 7 files changed, 218 insertions(+), 92 deletions(-) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 2ab77f2f1f4b0..1206bb5a3f512 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -30,8 +30,15 @@ impl InlayHint { } } - fn call_argument_name(position: TextSize, name: &str) -> Self { - let label_parts = vec![InlayHintLabelPart::new(name), "=".into()]; + fn call_argument_name( + position: TextSize, + name: &str, + navigation_target: Option, + ) -> Self { + let label_parts = vec![ + InlayHintLabelPart::new(name).with_target(navigation_target), + "=".into(), + ]; Self { position, @@ -97,6 +104,13 @@ impl InlayHintLabelPart { pub fn target(&self) -> Option<&crate::NavigationTarget> { self.target.as_ref() } + + pub fn with_target(self, target: Option) -> Self { + Self { + text: self.text, + target, + } + } } impl From for InlayHintLabelPart { @@ -171,6 +185,7 @@ impl Default for InlayHintSettings { struct InlayHintVisitor<'a, 'db> { db: &'db dyn Db, + file: File, model: SemanticModel<'db>, hints: Vec, in_assignment: bool, @@ -182,6 +197,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { fn new(db: &'db dyn Db, file: File, range: TextRange, settings: &'a InlayHintSettings) -> Self { Self { db, + file, model: SemanticModel::new(db, file), hints: Vec::new(), in_assignment: false, @@ -198,7 +214,12 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { .push(InlayHint::variable_type(position, ty, self.db)); } - fn add_call_argument_name(&mut self, position: TextSize, name: &str) { + fn add_call_argument_name( + &mut self, + position: TextSize, + name: &str, + parameter_label_offset: Option, + ) { if !self.settings.call_argument_names { return; } @@ -206,9 +227,12 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { if name.starts_with('_') { return; } + let navigation_target = + parameter_label_offset.map(|offset| crate::NavigationTarget::new(self.file, offset)); - self.hints - .push(InlayHint::call_argument_name(position, name)); + let inlay_hint = InlayHint::call_argument_name(position, name, navigation_target); + + self.hints.push(inlay_hint); } } @@ -273,8 +297,12 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { self.visit_expr(&call.func); for (index, arg_or_keyword) in call.arguments.arguments_source_order().enumerate() { - if let Some(name) = argument_names.get(&index) { - self.add_call_argument_name(arg_or_keyword.range().start(), name); + if let Some((name, parameter_label_offset)) = argument_names.get(&index) { + self.add_call_argument_name( + arg_or_keyword.range().start(), + name, + *parameter_label_offset, + ); } self.visit_expr(arg_or_keyword.value()); } diff --git a/crates/ty_ide/src/signature_help.rs b/crates/ty_ide/src/signature_help.rs index bbcb676c69034..f851f864094b3 100644 --- a/crates/ty_ide/src/signature_help.rs +++ b/crates/ty_ide/src/signature_help.rs @@ -382,7 +382,7 @@ mod tests { f = func_a else: f = func_b - + f( "#, ); @@ -427,10 +427,10 @@ mod tests { @overload def process(value: int) -> str: ... - + @overload def process(value: str) -> int: ... - + def process(value): if isinstance(value, int): return str(value) @@ -827,10 +827,10 @@ def ab(a: int, *, c: int): r#" class Point: """A simple point class representing a 2D coordinate.""" - + def __init__(self, x: int, y: int): """Initialize a point with x and y coordinates. - + Args: x: The x-coordinate y: The y-coordinate @@ -962,12 +962,12 @@ def ab(a: int, *, c: int): r#" from typing import overload - @overload + @overload def process(value: int) -> str: ... - + @overload def process(value: str, flag: bool) -> int: ... - + def process(value, flag=None): if isinstance(value, int): return str(value) diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 17b0c72f5216a..c013006b1e5cf 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -15,7 +15,7 @@ use crate::types::signatures::Signature; use crate::types::{ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type}; use crate::{Db, HasType, NameKind, SemanticModel}; use ruff_db::files::{File, FileRange}; -use ruff_db::parsed::parsed_module; +use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast}; use ruff_text_size::{Ranged, TextRange}; @@ -791,6 +791,11 @@ pub struct CallSignatureDetails<'db> { /// within the full signature string. pub parameter_label_offsets: Vec, + /// Offsets for each parameter in the signature definition. + /// Each range specifies the start position and length of a parameter definition + /// within the file containing the signature definition. + pub definition_parameter_offsets: Option>, + /// The names of the parameters in the signature, in order. /// This provides easy access to parameter names for documentation lookup. pub parameter_names: Vec, @@ -804,6 +809,22 @@ pub struct CallSignatureDetails<'db> { pub argument_to_parameter_mapping: Vec>, } +fn definition_parameter_offsets( + definition_kind: &DefinitionKind, + module_ref: &ParsedModuleRef, +) -> Option> { + match definition_kind { + DefinitionKind::Function(node) => Some( + node.node(module_ref) + .parameters + .iter() + .map(|param| param.name().range()) + .collect(), + ), + _ => None, + } +} + /// Extract signature details from a function call expression. /// This function analyzes the callable being invoked and returns zero or more /// `CallSignatureDetails` objects, each representing one possible signature @@ -834,11 +855,16 @@ pub fn call_signature_details<'db>( let display_details = signature.display(db).to_string_parts(); let parameter_label_offsets = display_details.parameter_ranges.clone(); let parameter_names = display_details.parameter_names.clone(); + let definition_parameter_offsets = signature.definition().and_then(|definition| { + let module_ref = parsed_module(db, definition.file(db)).load(db); + definition_parameter_offsets(definition.kind(db), &module_ref) + }); CallSignatureDetails { signature: signature.clone(), label: display_details.label, parameter_label_offsets, + definition_parameter_offsets, parameter_names, definition: signature.definition(), argument_to_parameter_mapping: binding.argument_matches().to_vec(), @@ -894,7 +920,7 @@ pub fn find_active_signature_from_details( #[derive(Default)] pub struct InlayHintFunctionArgumentDetails { - pub argument_names: HashMap, + pub argument_names: HashMap)>, } pub fn inlay_hint_function_argument_details<'db>( @@ -913,6 +939,7 @@ pub fn inlay_hint_function_argument_details<'db>( let call_signature_details = signature_details.get(active_signature_index)?; let parameters = call_signature_details.signature.parameters(); + let definition_parameter_offsets = &call_signature_details.definition_parameter_offsets; let mut argument_names = HashMap::new(); for arg_index in 0..call_expr.arguments.args.len() { @@ -935,12 +962,19 @@ pub fn inlay_hint_function_argument_details<'db>( continue; }; + let parameter_label_offset = definition_parameter_offsets + .as_ref() + .and_then(|offsets| offsets.get(*param_index)); + // Only add hints for parameters that can be specified by name if !param.is_positional_only() && !param.is_variadic() && !param.is_keyword_variadic() { let Some(name) = param.name() else { continue; }; - argument_names.insert(arg_index, name.to_string()); + argument_names.insert( + arg_index, + (name.to_string(), parameter_label_offset.copied()), + ); } } diff --git a/crates/ty_server/src/server/api/requests/inlay_hints.rs b/crates/ty_server/src/server/api/requests/inlay_hints.rs index ec8464fc6b187..e9b45a51cc83b 100644 --- a/crates/ty_server/src/server/api/requests/inlay_hints.rs +++ b/crates/ty_server/src/server/api/requests/inlay_hints.rs @@ -1,17 +1,17 @@ use std::borrow::Cow; -use crate::document::{RangeExt, TextSizeExt}; -use crate::server::api::traits::{ - BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, -}; -use crate::session::DocumentSnapshot; -use crate::session::client::Client; use lsp_types::request::InlayHintRequest; use lsp_types::{InlayHintParams, Url}; use ruff_db::source::{line_index, source_text}; use ty_ide::{InlayHintKind, InlayHintLabel, inlay_hints}; use ty_project::ProjectDatabase; +use crate::document::{RangeExt, TextSizeExt, ToLink}; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::session::{DocumentSnapshot, client::Client}; + pub(crate) struct InlayHintRequestHandler; impl RequestHandler for InlayHintRequestHandler { @@ -55,7 +55,7 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler { position: hint .position .to_position(&source, &index, snapshot.encoding()), - label: inlay_hint_label(&hint.label), + label: inlay_hint_label(&hint.label, db, snapshot), kind: Some(inlay_hint_kind(&hint.kind)), tooltip: None, padding_left: None, @@ -78,12 +78,20 @@ fn inlay_hint_kind(inlay_hint_kind: &InlayHintKind) -> lsp_types::InlayHintKind } } -fn inlay_hint_label(inlay_hint_label: &InlayHintLabel) -> lsp_types::InlayHintLabel { +fn inlay_hint_label( + inlay_hint_label: &InlayHintLabel, + db: &ProjectDatabase, + snapshot: &DocumentSnapshot, +) -> lsp_types::InlayHintLabel { let mut label_parts = Vec::new(); for part in inlay_hint_label.parts() { + let location = part + .target() + .and_then(|target| target.to_location(db, snapshot.encoding())); + label_parts.push(lsp_types::InlayHintLabelPart { value: part.text().into(), - location: None, + location, tooltip: None, command: None, }); diff --git a/crates/ty_server/tests/e2e/inlay_hints.rs b/crates/ty_server/tests/e2e/inlay_hints.rs index 3dbbbee994e23..26ae592da107d 100644 --- a/crates/ty_server/tests/e2e/inlay_hints.rs +++ b/crates/ty_server/tests/e2e/inlay_hints.rs @@ -59,7 +59,20 @@ foo(1) }, "label": [ { - "value": "a" + "value": "a", + "location": { + "uri": "file:///src/foo.py", + "range": { + "start": { + "line": 2, + "character": 8 + }, + "end": { + "line": 2, + "character": 9 + } + } + } }, { "value": "=" diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index 4bf3d19249cf3..dc091d680ca82 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -19,7 +19,7 @@ use ty_ide::{ InlayHintSettings, MarkupKind, RangedValue, document_highlights, goto_declaration, goto_definition, goto_references, goto_type_definition, hover, inlay_hints, }; -use ty_ide::{NavigationTargets, signature_help}; +use ty_ide::{NavigationTarget, NavigationTargets, signature_help}; use ty_project::metadata::options::Options; use ty_project::metadata::value::ValueSource; use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; @@ -456,7 +456,22 @@ impl Workspace { Ok(result .into_iter() .map(|hint| InlayHint { - markdown: hint.display().to_string(), + label: hint + .label + .parts() + .iter() + .map(|part| InlayHintLabelPart { + label: part.text().to_string(), + location: part.target().map(|target| { + location_link_from_navigation_target( + target, + &self.db, + self.position_encoding, + None, + ) + }), + }) + .collect(), position: Position::from_text_size( hint.position, &index, @@ -626,19 +641,8 @@ fn map_targets_to_links( targets .into_iter() - .map(|target| LocationLink { - path: target.file().path(db).to_string(), - full_range: Range::from_file_range( - db, - FileRange::new(target.file(), target.full_range()), - position_encoding, - ), - selection_range: Some(Range::from_file_range( - db, - FileRange::new(target.file(), target.focus_range()), - position_encoding, - )), - origin_selection_range: Some(source_range), + .map(|target| { + location_link_from_navigation_target(&target, db, position_encoding, Some(source_range)) }) .collect() } @@ -892,6 +896,7 @@ impl From for ruff_source_file::PositionEncoding { } #[wasm_bindgen] +#[derive(Clone)] pub struct LocationLink { /// The target file path #[wasm_bindgen(getter_with_clone)] @@ -905,6 +910,28 @@ pub struct LocationLink { pub origin_selection_range: Option, } +fn location_link_from_navigation_target( + target: &NavigationTarget, + db: &dyn Db, + position_encoding: PositionEncoding, + source_range: Option, +) -> LocationLink { + LocationLink { + path: target.file().path(db).to_string(), + full_range: Range::from_file_range( + db, + FileRange::new(target.file(), target.full_range()), + position_encoding, + ), + selection_range: Some(Range::from_file_range( + db, + FileRange::new(target.file(), target.focus_range()), + position_encoding, + )), + origin_selection_range: source_range, + } +} + #[wasm_bindgen] #[derive(Debug, Clone, PartialEq, Eq)] pub struct Hover { @@ -1005,16 +1032,25 @@ impl From for InlayHintKind { } #[wasm_bindgen] -#[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHint { #[wasm_bindgen(getter_with_clone)] - pub markdown: String, + pub label: Vec, pub position: Position, pub kind: InlayHintKind, } +#[wasm_bindgen] +#[derive(Clone)] +pub struct InlayHintLabelPart { + #[wasm_bindgen(getter_with_clone)] + pub label: String, + + #[wasm_bindgen(getter_with_clone)] + pub location: Option, +} + #[wasm_bindgen] #[derive(Debug, Clone, PartialEq, Eq)] pub struct SemanticToken { diff --git a/playground/ty/src/Editor/Editor.tsx b/playground/ty/src/Editor/Editor.tsx index 75bcf9d119468..8ce92600bc9a2 100644 --- a/playground/ty/src/Editor/Editor.tsx +++ b/playground/ty/src/Editor/Editor.tsx @@ -28,6 +28,7 @@ import { DocumentHighlight, DocumentHighlightKind, InlayHintKind, + LocationLink, } from "ty_wasm"; import { FileId, ReadonlyFiles } from "../Playground"; import { isPythonFile } from "./Files"; @@ -419,7 +420,13 @@ class PlaygroundServer return { dispose: () => {}, hints: inlayHints.map((hint) => ({ - label: hint.markdown, + label: hint.label.map((part) => ({ + label: part.label, + location: + part.location !== undefined + ? this.mapNavigationTarget(part.location) + : undefined, + })), position: { lineNumber: hint.position.line, column: hint.position.column, @@ -736,57 +743,57 @@ class PlaygroundServer return null; } - private mapNavigationTargets(links: any[]): languages.LocationLink[] { - const result = links.map((link) => { - const uri = Uri.parse(link.path); - - // Pre-create models to ensure peek definition works - if (this.monaco.editor.getModel(uri) == null) { - if (uri.scheme === "vendored") { - // Handle vendored files - const vendoredPath = this.getVendoredPath(uri); - const fileHandle = this.getOrCreateVendoredFileHandle(vendoredPath); - const content = this.props.workspace.sourceText(fileHandle); - this.monaco.editor.createModel(content, "python", uri); - } else { - // Handle regular files - const fileId = this.props.files.index.find((file) => { - return Uri.file(file.name).toString() === uri.toString(); - })?.id; - - if (fileId != null) { - const handle = this.props.files.handles[fileId]; - if (handle != null) { - const language = isPythonFile(handle) ? "python" : undefined; - this.monaco.editor.createModel( - this.props.files.contents[fileId], - language, - uri, - ); - } + private mapNavigationTarget(link: LocationLink): languages.LocationLink { + const uri = Uri.parse(link.path); + + // Pre-create models to ensure peek definition works + if (this.monaco.editor.getModel(uri) == null) { + if (uri.scheme === "vendored") { + // Handle vendored files + const vendoredPath = this.getVendoredPath(uri); + const fileHandle = this.getOrCreateVendoredFileHandle(vendoredPath); + const content = this.props.workspace.sourceText(fileHandle); + this.monaco.editor.createModel(content, "python", uri); + } else { + // Handle regular files + const fileId = this.props.files.index.find((file) => { + return Uri.file(file.name).toString() === uri.toString(); + })?.id; + + if (fileId != null) { + const handle = this.props.files.handles[fileId]; + if (handle != null) { + const language = isPythonFile(handle) ? "python" : undefined; + this.monaco.editor.createModel( + this.props.files.contents[fileId], + language, + uri, + ); } } } + } - const targetSelection = - link.selection_range == null - ? undefined - : tyRangeToMonacoRange(link.selection_range); - - const originSelection = - link.origin_selection_range == null - ? undefined - : tyRangeToMonacoRange(link.origin_selection_range); - - return { - uri: uri, - range: tyRangeToMonacoRange(link.full_range), - targetSelectionRange: targetSelection, - originSelectionRange: originSelection, - } as languages.LocationLink; - }); + const targetSelection = + link.selection_range == null + ? undefined + : tyRangeToMonacoRange(link.selection_range); - return result; + const originSelection = + link.origin_selection_range == null + ? undefined + : tyRangeToMonacoRange(link.origin_selection_range); + + return { + uri: uri, + range: tyRangeToMonacoRange(link.full_range), + targetSelectionRange: targetSelection, + originSelectionRange: originSelection, + } as languages.LocationLink; + } + + private mapNavigationTargets(links: any[]): languages.LocationLink[] { + return links.map(this.mapNavigationTarget); } dispose() { From 9f0bba6d123b502e770a0dedac2422b251874c8f Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Thu, 11 Sep 2025 14:06:42 +0100 Subject: [PATCH 02/21] Fix playground --- playground/ty/src/Editor/Editor.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/playground/ty/src/Editor/Editor.tsx b/playground/ty/src/Editor/Editor.tsx index 8ce92600bc9a2..b92cee158e14b 100644 --- a/playground/ty/src/Editor/Editor.tsx +++ b/playground/ty/src/Editor/Editor.tsx @@ -792,8 +792,10 @@ class PlaygroundServer } as languages.LocationLink; } - private mapNavigationTargets(links: any[]): languages.LocationLink[] { - return links.map(this.mapNavigationTarget); + private mapNavigationTargets( + links: LocationLink[], + ): languages.LocationLink[] { + return links.map((link) => this.mapNavigationTarget(link)); } dispose() { From de80e73d3096cb1ec40e0d25e7a9e436c510d32a Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Thu, 11 Sep 2025 17:07:03 +0100 Subject: [PATCH 03/21] Add tests for inlay hint label locations --- crates/ty_ide/src/inlay_hints.rs | 555 +++++++++++++++++- .../src/types/ide_support.rs | 28 +- 2 files changed, 561 insertions(+), 22 deletions(-) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 1206bb5a3f512..aa5a376834e00 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -1,6 +1,6 @@ use std::{fmt, vec}; -use crate::Db; +use crate::{Db, NavigationTarget}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal}; @@ -185,7 +185,6 @@ impl Default for InlayHintSettings { struct InlayHintVisitor<'a, 'db> { db: &'db dyn Db, - file: File, model: SemanticModel<'db>, hints: Vec, in_assignment: bool, @@ -197,7 +196,6 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { fn new(db: &'db dyn Db, file: File, range: TextRange, settings: &'a InlayHintSettings) -> Self { Self { db, - file, model: SemanticModel::new(db, file), hints: Vec::new(), in_assignment: false, @@ -218,7 +216,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { &mut self, position: TextSize, name: &str, - parameter_label_offset: Option, + navigation_target: Option, ) { if !self.settings.call_argument_names { return; @@ -227,8 +225,6 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { if name.starts_with('_') { return; } - let navigation_target = - parameter_label_offset.map(|offset| crate::NavigationTarget::new(self.file, offset)); let inlay_hint = InlayHint::call_argument_name(position, name, navigation_target); @@ -289,19 +285,30 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { source_order::walk_expr(self, expr); } Expr::Call(call) => { - let argument_names = + let inlay_hint_function_argument_details = inlay_hint_function_argument_details(self.db, &self.model, call) - .map(|details| details.argument_names) .unwrap_or_default(); self.visit_expr(&call.func); for (index, arg_or_keyword) in call.arguments.arguments_source_order().enumerate() { - if let Some((name, parameter_label_offset)) = argument_names.get(&index) { + if let Some((name, parameter_label_offset)) = + inlay_hint_function_argument_details + .argument_names + .get(&index) + { + let navigation_target = parameter_label_offset + .map(|offset| { + inlay_hint_function_argument_details + .target_signature_file + .map(|file| crate::NavigationTarget::new(file, offset)) + }) + .flatten(); + self.add_call_argument_name( arg_or_keyword.range().start(), name, - *parameter_label_offset, + navigation_target, ); } self.visit_expr(arg_or_keyword.value()); @@ -318,10 +325,16 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { mod tests { use super::*; + use crate::NavigationTarget; + use crate::tests::IntoDiagnostic; use insta::assert_snapshot; use ruff_db::{ Db as _, - files::{File, system_path_to_file}, + diagnostic::{ + Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, + LintName, Severity, Span, + }, + files::{File, FileRange, system_path_to_file}, source::source_text, }; use ruff_python_trivia::textwrap::dedent; @@ -405,13 +418,63 @@ mod tests { let mut buf = source_text(&self.db, self.file).as_str().to_string(); + let mut diagnostics = Vec::new(); + let mut offset = 0; for hint in hints { + let mut hint_str = "[".to_string(); + let end_position = (hint.position.to_u32() as usize) + offset; - let hint_str = format!("[{}]", hint.display()); - buf.insert_str(end_position, &hint_str); + + for part in hint.label.parts() { + hint_str.push_str(part.text()); + + let label_length = part.text().len(); + + if let Some(target) = part.target() { + println!("Target: {:?}", target); + let label_range = TextRange::new( + TextSize::try_from(end_position).unwrap(), + TextSize::try_from(end_position + label_length).unwrap(), + ); + + diagnostics.push(InlayHintLocationDiagnostic::new( + part.text().to_string(), + label_range, + target, + )); + } + } + + hint_str.push(']'); + offset += hint_str.len(); + + buf.insert_str(end_position, &hint_str); + } + + let rendered_diagnostics = self.render_diagnostics(diagnostics); + + format!("{buf}\n\n{rendered_diagnostics}") + } + + fn render_diagnostics(&self, diagnostics: I) -> String + where + I: IntoIterator, + D: IntoDiagnostic, + { + use std::fmt::Write; + + let mut buf = String::new(); + + let config = DisplayDiagnosticConfig::default() + .color(false) + .format(DiagnosticFormat::Full); + + for diagnostic in diagnostics { + let diag = diagnostic.into_diagnostic(); + write!(buf, "{}", diag.display(&self.db, &config)).unwrap(); } buf @@ -490,6 +553,15 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(x: int): pass foo([x=]1) + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int): pass + | ^ + 3 | foo(1) + | + info: For inlay hint label 'x' at 27..28 "); } @@ -560,6 +632,15 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(x: int, /, y: int): pass foo(1, [y=]2) + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:20 + | + 2 | def foo(x: int, /, y: int): pass + | ^ + 3 | foo(1, 2) + | + info: For inlay hint label 'y' at 41..42 "); } @@ -606,6 +687,28 @@ mod tests { def __init__(self, x: int): pass Foo([x=]1) f[: Foo] = Foo([x=]1) + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:24 + | + 2 | class Foo: + 3 | def __init__(self, x: int): pass + | ^ + 4 | Foo(1) + 5 | f = Foo(1) + | + info: For inlay hint label 'x' at 53..54 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:24 + | + 2 | class Foo: + 3 | def __init__(self, x: int): pass + | ^ + 4 | Foo(1) + 5 | f = Foo(1) + | + info: For inlay hint label 'x' at 75..76 "); } @@ -624,6 +727,28 @@ mod tests { def __new__(cls, x: int): pass Foo([x=]1) f[: Foo] = Foo([x=]1) + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:22 + | + 2 | class Foo: + 3 | def __new__(cls, x: int): pass + | ^ + 4 | Foo(1) + 5 | f = Foo(1) + | + info: For inlay hint label 'x' at 51..52 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:22 + | + 2 | class Foo: + 3 | def __new__(cls, x: int): pass + | ^ + 4 | Foo(1) + 5 | f = Foo(1) + | + info: For inlay hint label 'x' at 73..74 "); } @@ -644,6 +769,17 @@ mod tests { class Foo(metaclass=MetaFoo): pass Foo([x=]1) + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:24 + | + 2 | class MetaFoo: + 3 | def __call__(self, x: int): pass + | ^ + 4 | class Foo(metaclass=MetaFoo): + 5 | pass + | + info: For inlay hint label 'x' at 96..97 "); } @@ -676,6 +812,16 @@ mod tests { class Foo: def bar(self, y: int): pass Foo().bar([y=]2) + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:19 + | + 2 | class Foo: + 3 | def bar(self, y: int): pass + | ^ + 4 | Foo().bar(2) + | + info: For inlay hint label 'y' at 54..55 "); } @@ -694,6 +840,17 @@ mod tests { @classmethod def bar(cls, y: int): pass Foo.bar([y=]2) + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:4:18 + | + 2 | class Foo: + 3 | @classmethod + 4 | def bar(cls, y: int): pass + | ^ + 5 | Foo.bar(2) + | + info: For inlay hint label 'y' at 68..69 "); } @@ -712,6 +869,17 @@ mod tests { @staticmethod def bar(y: int): pass Foo.bar([y=]2) + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:4:13 + | + 2 | class Foo: + 3 | @staticmethod + 4 | def bar(y: int): pass + | ^ + 5 | Foo.bar(2) + | + info: For inlay hint label 'y' at 64..65 "); } @@ -728,6 +896,26 @@ mod tests { def foo(x: int | str): pass foo([x=]1) foo([x=]'abc') + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int | str): pass + | ^ + 3 | foo(1) + 4 | foo('abc') + | + info: For inlay hint label 'x' at 33..34 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int | str): pass + | ^ + 3 | foo(1) + 4 | foo('abc') + | + info: For inlay hint label 'x' at 44..45 "); } @@ -742,6 +930,33 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(x: int, y: str, z: bool): pass foo([x=]1, [y=]'hello', [z=]True) + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int, y: str, z: bool): pass + | ^ + 3 | foo(1, 'hello', True) + | + info: For inlay hint label 'x' at 44..45 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:17 + | + 2 | def foo(x: int, y: str, z: bool): pass + | ^ + 3 | foo(1, 'hello', True) + | + info: For inlay hint label 'y' at 51..52 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:25 + | + 2 | def foo(x: int, y: str, z: bool): pass + | ^ + 3 | foo(1, 'hello', True) + | + info: For inlay hint label 'z' at 64..65 "); } @@ -756,6 +971,15 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(x: int, y: str, z: bool): pass foo([x=]1, z=True, y='hello') + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int, y: str, z: bool): pass + | ^ + 3 | foo(1, z=True, y='hello') + | + info: For inlay hint label 'x' at 44..45 "); } @@ -774,6 +998,66 @@ mod tests { foo([x=]1) foo([x=]1, [y=]'custom') foo([x=]1, [y=]'custom', [z=]True) + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + | ^ + 3 | foo(1) + 4 | foo(1, 'custom') + | + info: For inlay hint label 'x' at 64..65 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + | ^ + 3 | foo(1) + 4 | foo(1, 'custom') + | + info: For inlay hint label 'x' at 75..76 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:17 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + | ^ + 3 | foo(1) + 4 | foo(1, 'custom') + | + info: For inlay hint label 'y' at 82..83 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + | ^ + 3 | foo(1) + 4 | foo(1, 'custom') + | + info: For inlay hint label 'x' at 100..101 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:17 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + | ^ + 3 | foo(1) + 4 | foo(1, 'custom') + | + info: For inlay hint label 'y' at 107..108 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:37 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + | ^ + 3 | foo(1) + 4 | foo(1, 'custom') + | + info: For inlay hint label 'z' at 121..122 "); } @@ -802,6 +1086,73 @@ mod tests { def baz(a: int, b: str, c: bool): pass baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True) + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:8:9 + | + 6 | return y + 7 | + 8 | def baz(a: int, b: str, c: bool): pass + | ^ + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | + info: For inlay hint label 'a' at 125..126 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int) -> int: + | ^ + 3 | return x * 2 + | + info: For inlay hint label 'x' at 133..134 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:8:17 + | + 6 | return y + 7 | + 8 | def baz(a: int, b: str, c: bool): pass + | ^ + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | + info: For inlay hint label 'b' at 141..142 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:5:9 + | + 3 | return x * 2 + 4 | + 5 | def bar(y: str) -> str: + | ^ + 6 | return y + | + info: For inlay hint label 'y' at 149..150 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:5:9 + | + 3 | return x * 2 + 4 | + 5 | def bar(y: str) -> str: + | ^ + 6 | return y + | + info: For inlay hint label 'y' at 157..158 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:8:25 + | + 6 | return y + 7 | + 8 | def baz(a: int, b: str, c: bool): pass + | ^ + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | + info: For inlay hint label 'c' at 171..172 "); } @@ -826,6 +1177,29 @@ mod tests { return self def baz(self): pass A().foo([value=]42).bar([name=]'test').baz() + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:19 + | + 2 | class A: + 3 | def foo(self, value: int) -> 'A': + | ^^^^^ + 4 | return self + 5 | def bar(self, name: str) -> 'A': + | + info: For inlay hint label 'value' at 157..162 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:5:19 + | + 3 | def foo(self, value: int) -> 'A': + 4 | return self + 5 | def bar(self, name: str) -> 'A': + | ^^^^ + 6 | return self + 7 | def baz(self): pass + | + info: For inlay hint label 'name' at 173..177 "); } @@ -845,6 +1219,17 @@ mod tests { return x def bar(y: int): pass bar(y=foo([x=]'test')) + + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: str) -> str: + | ^ + 3 | return x + 4 | def bar(y: int): pass + | + info: For inlay hint label 'x' at 70..71 "); } @@ -879,6 +1264,36 @@ mod tests { def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass foo(1, 'pos', [c=]3.14, [d=]False, e=42) foo(1, 'pos', [c=]3.14, e=42, f='custom') + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:28 + | + 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass + | ^ + 3 | foo(1, 'pos', 3.14, False, e=42) + 4 | foo(1, 'pos', 3.14, e=42, f='custom') + | + info: For inlay hint label 'c' at 105..106 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:38 + | + 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass + | ^ + 3 | foo(1, 'pos', 3.14, False, e=42) + 4 | foo(1, 'pos', 3.14, e=42, f='custom') + | + info: For inlay hint label 'd' at 115..116 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:28 + | + 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass + | ^ + 3 | foo(1, 'pos', 3.14, False, e=42) + 4 | foo(1, 'pos', 3.14, e=42, f='custom') + | + info: For inlay hint label 'c' at 146..147 "); } @@ -897,7 +1312,7 @@ mod tests { identity('hello')", ); - assert_snapshot!(test.inlay_hints(), @r###" + assert_snapshot!(test.inlay_hints(), @r" from typing import TypeVar, Generic T[: typing.TypeVar] = TypeVar([name=]'T') @@ -907,7 +1322,41 @@ mod tests { identity([x=]42) identity([x=]'hello') - "###); + + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/typing.pyi:278:13 + | + 276 | def __new__( + 277 | cls, + 278 | name: str, + | ^^^^ + 279 | *constraints: Any, # AnnotationForm + 280 | bound: Any | None = None, # AnnotationForm + | + info: For inlay hint label 'name' at 68..72 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:6:14 + | + 4 | T = TypeVar('T') + 5 | + 6 | def identity(x: T) -> T: + | ^ + 7 | return x + | + info: For inlay hint label 'x' at 129..130 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:6:14 + | + 4 | T = TypeVar('T') + 5 | + 6 | def identity(x: T) -> T: + | ^ + 7 | return x + | + info: For inlay hint label 'x' at 146..147 + "); } #[test] @@ -939,6 +1388,28 @@ mod tests { foo([x=]42) foo([x=]'hello') + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:5:9 + | + 4 | @overload + 5 | def foo(x: int) -> str: ... + | ^ + 6 | @overload + 7 | def foo(x: str) -> int: ... + | + info: For inlay hint label 'x' at 136..137 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:5:9 + | + 4 | @overload + 5 | def foo(x: int) -> str: ... + | ^ + 6 | @overload + 7 | def foo(x: str) -> int: ... + | + info: For inlay hint label 'x' at 148..149 "); } @@ -974,6 +1445,16 @@ mod tests { def bar(y: int): pass foo([x=]1) bar(2) + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:9 + | + 2 | def foo(x: int): pass + | ^ + 3 | def bar(y: int): pass + 4 | foo(1) + | + info: For inlay hint label 'x' at 49..50 "); } @@ -988,6 +1469,50 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(_x: int, y: int): pass foo(1, [y=]2) + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:18 + | + 2 | def foo(_x: int, y: int): pass + | ^ + 3 | foo(1, 2) + | + info: For inlay hint label 'y' at 39..40 "); } + + struct InlayHintLocationDiagnostic { + source_text: String, + source_range: TextRange, + target: FileRange, + } + + impl InlayHintLocationDiagnostic { + fn new(source_text: String, source_range: TextRange, target: &NavigationTarget) -> Self { + Self { + source_text, + source_range, + target: FileRange::new(target.file(), target.focus_range()), + } + } + } + + impl IntoDiagnostic for InlayHintLocationDiagnostic { + fn into_diagnostic(self) -> Diagnostic { + let mut main = Diagnostic::new( + DiagnosticId::Lint(LintName::of("inlay-hint-location")), + Severity::Info, + "Inlay Hint Target".to_string(), + ); + main.annotate(Annotation::primary( + Span::from(self.target.file()).with_range(self.target.range()), + )); + main.info(format!( + "For inlay hint label '{}' at {:?}", + self.source_text, self.source_range + )); + + main + } + } } diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index c013006b1e5cf..f155c5c1053dd 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -794,7 +794,7 @@ pub struct CallSignatureDetails<'db> { /// Offsets for each parameter in the signature definition. /// Each range specifies the start position and length of a parameter definition /// within the file containing the signature definition. - pub definition_parameter_offsets: Option>, + pub definition_parameter_offsets: Option>, /// The names of the parameters in the signature, in order. /// This provides easy access to parameter names for documentation lookup. @@ -812,13 +812,13 @@ pub struct CallSignatureDetails<'db> { fn definition_parameter_offsets( definition_kind: &DefinitionKind, module_ref: &ParsedModuleRef, -) -> Option> { +) -> Option> { match definition_kind { DefinitionKind::Function(node) => Some( node.node(module_ref) .parameters .iter() - .map(|param| param.name().range()) + .map(|param| (param.name().to_string(), param.name().range())) .collect(), ), _ => None, @@ -920,6 +920,7 @@ pub fn find_active_signature_from_details( #[derive(Default)] pub struct InlayHintFunctionArgumentDetails { + pub target_signature_file: Option, pub argument_names: HashMap)>, } @@ -939,6 +940,12 @@ pub fn inlay_hint_function_argument_details<'db>( let call_signature_details = signature_details.get(active_signature_index)?; let parameters = call_signature_details.signature.parameters(); + + let target_signature_file = call_signature_details + .signature + .definition + .map(|definition| definition.file(db)); + let definition_parameter_offsets = &call_signature_details.definition_parameter_offsets; let mut argument_names = HashMap::new(); @@ -962,9 +969,13 @@ pub fn inlay_hint_function_argument_details<'db>( continue; }; - let parameter_label_offset = definition_parameter_offsets - .as_ref() - .and_then(|offsets| offsets.get(*param_index)); + let parameter_label_offset = + definition_parameter_offsets + .as_ref() + .and_then(|offsets| match param.name() { + Some(name) => offsets.get(&name.to_string()), + None => None, + }); // Only add hints for parameters that can be specified by name if !param.is_positional_only() && !param.is_variadic() && !param.is_keyword_variadic() { @@ -978,7 +989,10 @@ pub fn inlay_hint_function_argument_details<'db>( } } - Some(InlayHintFunctionArgumentDetails { argument_names }) + Some(InlayHintFunctionArgumentDetails { + target_signature_file, + argument_names, + }) } /// Find the text range of a specific parameter in function parameters by name. From e1896cd051344415acabb0aaf130bc5b2d463fde Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Thu, 11 Sep 2025 17:12:21 +0100 Subject: [PATCH 04/21] Comments --- crates/ty_ide/src/inlay_hints.rs | 1 - crates/ty_python_semantic/src/types/ide_support.rs | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index aa5a376834e00..dc1fffca5a929 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -433,7 +433,6 @@ mod tests { let label_length = part.text().len(); if let Some(target) = part.target() { - println!("Target: {:?}", target); let label_range = TextRange::new( TextSize::try_from(end_position).unwrap(), TextSize::try_from(end_position + label_length).unwrap(), diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index f155c5c1053dd..30611a849cafc 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -821,6 +821,7 @@ fn definition_parameter_offsets( .map(|param| (param.name().to_string(), param.name().range())) .collect(), ), + // Todo: lambda functions _ => None, } } @@ -920,7 +921,13 @@ pub fn find_active_signature_from_details( #[derive(Default)] pub struct InlayHintFunctionArgumentDetails { + /// The file containing the signature derived from the [`ast::ExprCall`]. + /// + /// This is [`None`] if the signature does not have a definition. + /// This should only happen if [`definition_parameter_offsets`] is incomplete. pub target_signature_file: Option, + + /// The position of the arguments mapped to their name and the range of the argument definition in the signature. pub argument_names: HashMap)>, } From 4a142eb70c79f1c2c5e6dbcaa3f659dd258775db Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Thu, 11 Sep 2025 17:18:08 +0100 Subject: [PATCH 05/21] Update snapshots output --- crates/ty_ide/src/inlay_hints.rs | 72 ++++++++++++++++---------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index dc1fffca5a929..923074c7a8d8e 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -453,9 +453,17 @@ mod tests { buf.insert_str(end_position, &hint_str); } - let rendered_diagnostics = self.render_diagnostics(diagnostics); + let mut rendered_diagnostics = self.render_diagnostics(diagnostics); + + if !rendered_diagnostics.is_empty() { + rendered_diagnostics = format!( + "{}{}", + crate::MarkupKind::PlainText.horizontal_line(), + rendered_diagnostics + ); + } - format!("{buf}\n\n{rendered_diagnostics}") + format!("{buf}{rendered_diagnostics}",) } fn render_diagnostics(&self, diagnostics: I) -> String @@ -484,36 +492,28 @@ mod tests { fn test_assign_statement() { let test = inlay_hint_test("x = 1"); - assert_snapshot!(test.inlay_hints(), @r" - x[: Literal[1]] = 1 - "); + assert_snapshot!(test.inlay_hints(), @"x[: Literal[1]] = 1"); } #[test] fn test_tuple_assignment() { let test = inlay_hint_test("x, y = (1, 'abc')"); - assert_snapshot!(test.inlay_hints(), @r#" - x[: Literal[1]], y[: Literal["abc"]] = (1, 'abc') - "#); + assert_snapshot!(test.inlay_hints(), @r#"x[: Literal[1]], y[: Literal["abc"]] = (1, 'abc')"#); } #[test] fn test_nested_tuple_assignment() { let test = inlay_hint_test("x, (y, z) = (1, ('abc', 2))"); - assert_snapshot!(test.inlay_hints(), @r#" - x[: Literal[1]], (y[: Literal["abc"]], z[: Literal[2]]) = (1, ('abc', 2)) - "#); + assert_snapshot!(test.inlay_hints(), @r#"x[: Literal[1]], (y[: Literal["abc"]], z[: Literal[2]]) = (1, ('abc', 2))"#); } #[test] fn test_assign_statement_with_type_annotation() { let test = inlay_hint_test("x: int = 1"); - assert_snapshot!(test.inlay_hints(), @r" - x: int = 1 - "); + assert_snapshot!(test.inlay_hints(), @"x: int = 1"); } #[test] @@ -535,9 +535,7 @@ mod tests { variable_types: false, ..Default::default() }), - @r" - x = 1 - " + @"x = 1" ); } @@ -552,7 +550,7 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(x: int): pass foo([x=]1) - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | @@ -631,7 +629,7 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(x: int, /, y: int): pass foo(1, [y=]2) - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:2:20 | @@ -686,7 +684,7 @@ mod tests { def __init__(self, x: int): pass Foo([x=]1) f[: Foo] = Foo([x=]1) - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | @@ -726,7 +724,7 @@ mod tests { def __new__(cls, x: int): pass Foo([x=]1) f[: Foo] = Foo([x=]1) - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:3:22 | @@ -768,7 +766,7 @@ mod tests { class Foo(metaclass=MetaFoo): pass Foo([x=]1) - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | @@ -811,7 +809,7 @@ mod tests { class Foo: def bar(self, y: int): pass Foo().bar([y=]2) - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:3:19 | @@ -839,7 +837,7 @@ mod tests { @classmethod def bar(cls, y: int): pass Foo.bar([y=]2) - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:4:18 | @@ -868,7 +866,7 @@ mod tests { @staticmethod def bar(y: int): pass Foo.bar([y=]2) - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:4:13 | @@ -895,7 +893,7 @@ mod tests { def foo(x: int | str): pass foo([x=]1) foo([x=]'abc') - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | @@ -929,7 +927,7 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(x: int, y: str, z: bool): pass foo([x=]1, [y=]'hello', [z=]True) - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | @@ -970,7 +968,7 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(x: int, y: str, z: bool): pass foo([x=]1, z=True, y='hello') - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | @@ -997,7 +995,7 @@ mod tests { foo([x=]1) foo([x=]1, [y=]'custom') foo([x=]1, [y=]'custom', [z=]True) - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | @@ -1085,7 +1083,7 @@ mod tests { def baz(a: int, b: str, c: bool): pass baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True) - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:8:9 | @@ -1176,7 +1174,7 @@ mod tests { return self def baz(self): pass A().foo([value=]42).bar([name=]'test').baz() - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:3:19 | @@ -1219,7 +1217,7 @@ mod tests { def bar(y: int): pass bar(y=foo([x=]'test')) - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | @@ -1263,7 +1261,7 @@ mod tests { def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass foo(1, 'pos', [c=]3.14, [d=]False, e=42) foo(1, 'pos', [c=]3.14, e=42, f='custom') - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:2:28 | @@ -1321,7 +1319,7 @@ mod tests { identity([x=]42) identity([x=]'hello') - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:278:13 | @@ -1387,7 +1385,7 @@ mod tests { foo([x=]42) foo([x=]'hello') - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:5:9 | @@ -1444,7 +1442,7 @@ mod tests { def bar(y: int): pass foo([x=]1) bar(2) - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | @@ -1468,7 +1466,7 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(_x: int, y: int): pass foo(1, [y=]2) - + --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target --> main.py:2:18 | From 2726a6eb6b4d26c0907bc41e80502b182241442b Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Thu, 11 Sep 2025 17:28:41 +0100 Subject: [PATCH 06/21] Fix windows test by using local import --- crates/ty_ide/src/inlay_hints.rs | 68 ++++++++++---------------------- 1 file changed, 21 insertions(+), 47 deletions(-) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 923074c7a8d8e..dfae29b1f238e 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -412,6 +412,10 @@ mod tests { }) } + fn with_extra_file(&mut self, file_name: &str, content: &str) { + self.db.write_file(file_name, content).unwrap(); + } + /// Returns the inlay hints for the given test case with custom settings. fn inlay_hints_with_settings(&self, settings: &InlayHintSettings) -> String { let hints = inlay_hints(&self.db, self.file, self.range, settings); @@ -1295,64 +1299,34 @@ mod tests { } #[test] - fn test_generic_function_calls() { - let test = inlay_hint_test( + fn test_function_calls_different_file() { + let mut test = inlay_hint_test( " - from typing import TypeVar, Generic + from foo import bar - T = TypeVar('T') - - def identity(x: T) -> T: - return x + bar(1)", + ); - identity(42) - identity('hello')", + test.with_extra_file( + "foo.py", + " + def bar(x: int | str): + pass", ); assert_snapshot!(test.inlay_hints(), @r" - from typing import TypeVar, Generic + from foo import bar - T[: typing.TypeVar] = TypeVar([name=]'T') - - def identity(x: T) -> T: - return x - - identity([x=]42) - identity([x=]'hello') + bar([x=]1) --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target - --> stdlib/typing.pyi:278:13 - | - 276 | def __new__( - 277 | cls, - 278 | name: str, - | ^^^^ - 279 | *constraints: Any, # AnnotationForm - 280 | bound: Any | None = None, # AnnotationForm - | - info: For inlay hint label 'name' at 68..72 - - info[inlay-hint-location]: Inlay Hint Target - --> main.py:6:14 - | - 4 | T = TypeVar('T') - 5 | - 6 | def identity(x: T) -> T: - | ^ - 7 | return x - | - info: For inlay hint label 'x' at 129..130 - - info[inlay-hint-location]: Inlay Hint Target - --> main.py:6:14 + --> foo.py:2:17 | - 4 | T = TypeVar('T') - 5 | - 6 | def identity(x: T) -> T: - | ^ - 7 | return x + 2 | def bar(x: int | str): + | ^ + 3 | pass | - info: For inlay hint label 'x' at 146..147 + info: For inlay hint label 'x' at 26..27 "); } From 3378713c8a5ce25c9e3f75e29ad37e9199f93b75 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Fri, 12 Sep 2025 01:01:59 +0100 Subject: [PATCH 07/21] Small changes --- crates/ty_ide/src/inlay_hints.rs | 38 +++++++++---------- crates/ty_python_semantic/src/types.rs | 2 +- .../src/types/ide_support.rs | 14 ++++--- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index dfae29b1f238e..fdcbb4dcad4e3 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -6,7 +6,7 @@ use ruff_db::parsed::parsed_module; use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal}; use ruff_python_ast::{AnyNodeRef, Expr, Stmt}; use ruff_text_size::{Ranged, TextRange, TextSize}; -use ty_python_semantic::types::{Type, inlay_hint_function_argument_details}; +use ty_python_semantic::types::{Type, inlay_hint_call_argument_details}; use ty_python_semantic::{HasType, SemanticModel}; #[derive(Debug, Clone)] @@ -33,7 +33,7 @@ impl InlayHint { fn call_argument_name( position: TextSize, name: &str, - navigation_target: Option, + navigation_target: Option, ) -> Self { let label_parts = vec![ InlayHintLabelPart::new(name).with_target(navigation_target), @@ -86,7 +86,7 @@ impl fmt::Display for InlayHintDisplay<'_> { pub struct InlayHintLabelPart { text: String, - target: Option, + target: Option, } impl InlayHintLabelPart { @@ -101,11 +101,11 @@ impl InlayHintLabelPart { &self.text } - pub fn target(&self) -> Option<&crate::NavigationTarget> { + pub fn target(&self) -> Option<&NavigationTarget> { self.target.as_ref() } - pub fn with_target(self, target: Option) -> Self { + pub fn with_target(self, target: Option) -> Self { Self { text: self.text, target, @@ -208,8 +208,10 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { if !self.settings.variable_types { return; } - self.hints - .push(InlayHint::variable_type(position, ty, self.db)); + + let inlay_hint = InlayHint::variable_type(position, ty, self.db); + + self.hints.push(inlay_hint); } fn add_call_argument_name( @@ -285,25 +287,19 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { source_order::walk_expr(self, expr); } Expr::Call(call) => { - let inlay_hint_function_argument_details = - inlay_hint_function_argument_details(self.db, &self.model, call) - .unwrap_or_default(); + let details = inlay_hint_call_argument_details(self.db, &self.model, call) + .unwrap_or_default(); self.visit_expr(&call.func); for (index, arg_or_keyword) in call.arguments.arguments_source_order().enumerate() { - if let Some((name, parameter_label_offset)) = - inlay_hint_function_argument_details - .argument_names - .get(&index) + if let Some((name, parameter_label_offset)) = details.argument_names.get(&index) { - let navigation_target = parameter_label_offset - .map(|offset| { - inlay_hint_function_argument_details - .target_signature_file - .map(|file| crate::NavigationTarget::new(file, offset)) - }) - .flatten(); + let navigation_target = parameter_label_offset.and_then(|offset| { + details + .target_signature_file + .map(|file| NavigationTarget::new(file, offset)) + }); self.add_call_argument_name( arg_or_keyword.range().start(), diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 42ad5d2fd8664..de013abcd2751 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -56,7 +56,7 @@ use crate::types::generics::{ pub use crate::types::ide_support::{ CallSignatureDetails, Member, all_members, call_signature_details, definition_kind_for_name, definitions_for_attribute, definitions_for_imported_symbol, definitions_for_keyword_argument, - definitions_for_name, find_active_signature_from_details, inlay_hint_function_argument_details, + definitions_for_name, find_active_signature_from_details, inlay_hint_call_argument_details, }; use crate::types::infer::infer_unpack_types; use crate::types::mro::{Mro, MroError, MroIterator}; diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 30611a849cafc..5c41a2e0ff6b4 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -857,7 +857,9 @@ pub fn call_signature_details<'db>( let parameter_label_offsets = display_details.parameter_ranges.clone(); let parameter_names = display_details.parameter_names.clone(); let definition_parameter_offsets = signature.definition().and_then(|definition| { - let module_ref = parsed_module(db, definition.file(db)).load(db); + let file = definition.file(db); + let module_ref = parsed_module(db, file).load(db); + definition_parameter_offsets(definition.kind(db), &module_ref) }); @@ -920,7 +922,7 @@ pub fn find_active_signature_from_details( } #[derive(Default)] -pub struct InlayHintFunctionArgumentDetails { +pub struct InlayHintCallArgumentDetails { /// The file containing the signature derived from the [`ast::ExprCall`]. /// /// This is [`None`] if the signature does not have a definition. @@ -931,11 +933,11 @@ pub struct InlayHintFunctionArgumentDetails { pub argument_names: HashMap)>, } -pub fn inlay_hint_function_argument_details<'db>( +pub fn inlay_hint_call_argument_details<'db>( db: &'db dyn Db, model: &SemanticModel<'db>, call_expr: &ast::ExprCall, -) -> Option { +) -> Option { let signature_details = call_signature_details(db, model, call_expr); if signature_details.is_empty() { @@ -949,11 +951,11 @@ pub fn inlay_hint_function_argument_details<'db>( let parameters = call_signature_details.signature.parameters(); let target_signature_file = call_signature_details - .signature .definition .map(|definition| definition.file(db)); let definition_parameter_offsets = &call_signature_details.definition_parameter_offsets; + let mut argument_names = HashMap::new(); for arg_index in 0..call_expr.arguments.args.len() { @@ -996,7 +998,7 @@ pub fn inlay_hint_function_argument_details<'db>( } } - Some(InlayHintFunctionArgumentDetails { + Some(InlayHintCallArgumentDetails { target_signature_file, argument_names, }) From e3b6616d702d4fa79954b8d6d3581768684cce0d Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Mon, 15 Sep 2025 15:06:12 +0100 Subject: [PATCH 08/21] Small changes --- crates/ty_ide/src/inlay_hints.rs | 5 +---- crates/ty_python_semantic/src/types/ide_support.rs | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index fdcbb4dcad4e3..3c2163379db38 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -106,10 +106,7 @@ impl InlayHintLabelPart { } pub fn with_target(self, target: Option) -> Self { - Self { - text: self.text, - target, - } + Self { target, ..self } } } diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 5c41a2e0ff6b4..5978df8ca98fa 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -792,8 +792,6 @@ pub struct CallSignatureDetails<'db> { pub parameter_label_offsets: Vec, /// Offsets for each parameter in the signature definition. - /// Each range specifies the start position and length of a parameter definition - /// within the file containing the signature definition. pub definition_parameter_offsets: Option>, /// The names of the parameters in the signature, in order. From d0d3d3dfccd17e4172d142764b7b85fd178d2ab7 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Fri, 19 Sep 2025 16:49:30 +0100 Subject: [PATCH 09/21] Fix todo case --- crates/ty_python_semantic/src/types/ide_support.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 4b5022939278d..91ff524d482ab 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -875,7 +875,7 @@ fn definition_parameter_offsets( .map(|param| (param.name().to_string(), param.name().range())) .collect(), ), - // Todo: lambda functions + // TODO: lambda functions _ => None, } } From 74a9bde79c9f1103ac64d6563b23d2ca8cfb6e9b Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Fri, 19 Sep 2025 16:52:30 +0100 Subject: [PATCH 10/21] Fix clippy --- crates/ty_python_semantic/src/types.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 109006908d525..04edd335621b9 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -55,9 +55,10 @@ use crate::types::generics::{ walk_partial_specialization, walk_specialization, }; pub use crate::types::ide_support::{ - CallSignatureDetails, Member, all_members, call_signature_details, definition_kind_for_name, - definitions_for_attribute, definitions_for_imported_symbol, definitions_for_keyword_argument, - definitions_for_name, find_active_signature_from_details, inlay_hint_call_argument_details, + CallSignatureDetails, Member, MemberWithDefinition, all_members, call_signature_details, + definition_kind_for_name, definitions_for_attribute, definitions_for_imported_symbol, + definitions_for_keyword_argument, definitions_for_name, find_active_signature_from_details, + inlay_hint_call_argument_details, }; use crate::types::infer::infer_unpack_types; use crate::types::mro::{Mro, MroError, MroIterator}; From 1f74a1cd4b4d577faaf76f8ec504bd5adaddcbe4 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Mon, 22 Sep 2025 22:04:16 +0100 Subject: [PATCH 11/21] Add test for function with weird formatting --- crates/ty_ide/src/inlay_hints.rs | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 3c2163379db38..0902db0a2db1a 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -1445,6 +1445,50 @@ mod tests { "); } + #[test] + fn test_function_call_different_formatting() { + let test = inlay_hint_test( + " + def foo( + x: int, + y: int + ): ... + + foo(1, 2)", + ); + + assert_snapshot!(test.inlay_hints(), @r" + def foo( + x: int, + y: int + ): ... + + foo([x=]1, [y=]2) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:5 + | + 2 | def foo( + 3 | x: int, + | ^ + 4 | y: int + 5 | ): ... + | + info: For inlay hint label 'x' at 45..46 + + info[inlay-hint-location]: Inlay Hint Target + --> main.py:4:5 + | + 2 | def foo( + 3 | x: int, + 4 | y: int + | ^ + 5 | ): ... + | + info: For inlay hint label 'y' at 52..53 + "); + } + struct InlayHintLocationDiagnostic { source_text: String, source_range: TextRange, From 20364f173946173210255be731b938b0bf71bdea Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Tue, 23 Sep 2025 16:36:19 +0100 Subject: [PATCH 12/21] Use FileRange --- crates/ty_ide/src/inlay_hints.rs | 6 ++-- .../src/types/ide_support.rs | 28 ++++++++----------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 0902db0a2db1a..6346d17367bc9 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -292,10 +292,8 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { for (index, arg_or_keyword) in call.arguments.arguments_source_order().enumerate() { if let Some((name, parameter_label_offset)) = details.argument_names.get(&index) { - let navigation_target = parameter_label_offset.and_then(|offset| { - details - .target_signature_file - .map(|file| NavigationTarget::new(file, offset)) + let navigation_target = parameter_label_offset.map(|file_range| { + NavigationTarget::new(file_range.file(), file_range.range()) }); self.add_call_argument_name( diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 91ff524d482ab..d807829fe9f60 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -848,7 +848,7 @@ pub struct CallSignatureDetails<'db> { pub parameter_label_offsets: Vec, /// Offsets for each parameter in the signature definition. - pub definition_parameter_offsets: HashMap, + pub definition_parameter_offsets: HashMap, /// The names of the parameters in the signature, in order. /// This provides easy access to parameter names for documentation lookup. @@ -916,7 +916,14 @@ pub fn call_signature_details<'db>( let file = definition.file(db); let module_ref = parsed_module(db, file).load(db); - definition_parameter_offsets(definition.kind(db), &module_ref) + definition_parameter_offsets(definition.kind(db), &module_ref).map( + |offsets| { + offsets + .into_iter() + .map(|(name, offset)| (name, FileRange::new(file, offset))) + .collect() + }, + ) }) .unwrap_or_default(); @@ -980,14 +987,8 @@ pub fn find_active_signature_from_details( #[derive(Default)] pub struct InlayHintCallArgumentDetails { - /// The file containing the signature derived from the [`ast::ExprCall`]. - /// - /// This is [`None`] if the signature does not have a definition. - /// This should only happen if [`definition_parameter_offsets`] is incomplete. - pub target_signature_file: Option, - /// The position of the arguments mapped to their name and the range of the argument definition in the signature. - pub argument_names: HashMap)>, + pub argument_names: HashMap)>, } pub fn inlay_hint_call_argument_details<'db>( @@ -1007,10 +1008,6 @@ pub fn inlay_hint_call_argument_details<'db>( let parameters = call_signature_details.signature.parameters(); - let target_signature_file = call_signature_details - .definition - .map(|definition| definition.file(db)); - let definition_parameter_offsets = &call_signature_details.definition_parameter_offsets; let mut argument_names = HashMap::new(); @@ -1049,10 +1046,7 @@ pub fn inlay_hint_call_argument_details<'db>( } } - Some(InlayHintCallArgumentDetails { - target_signature_file, - argument_names, - }) + Some(InlayHintCallArgumentDetails { argument_names }) } /// Find the text range of a specific parameter in function parameters by name. From 7d703685c01365993519fc8f457d47c5c4c21cdb Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Tue, 23 Sep 2025 16:39:00 +0100 Subject: [PATCH 13/21] Address review --- crates/ty_ide/src/lib.rs | 4 ++++ crates/ty_wasm/src/lib.rs | 6 +----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index 9febfb06ece00..1ff96ee2ba0a7 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -128,6 +128,10 @@ impl NavigationTarget { pub fn full_range(&self) -> TextRange { self.full_range } + + pub fn full_file_range(&self) -> FileRange { + FileRange::new(self.file, self.full_range) + } } /// Specifies the kind of reference operation. diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index 7b2f7e5d14a33..12e3180899825 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -931,11 +931,7 @@ fn location_link_from_navigation_target( ) -> LocationLink { LocationLink { path: target.file().path(db).to_string(), - full_range: Range::from_file_range( - db, - FileRange::new(target.file(), target.full_range()), - position_encoding, - ), + full_range: Range::from_file_range(db, target.full_file_range(), position_encoding), selection_range: Some(Range::from_file_range( db, FileRange::new(target.file(), target.focus_range()), From 64194dcd3b10e9744bf4f4a886f4c760b4b497fb Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Tue, 23 Sep 2025 16:45:34 +0100 Subject: [PATCH 14/21] Merge main --- Cargo.lock | 9 +- Cargo.toml | 2 +- crates/ruff_db/src/system/path.rs | 4 +- crates/ruff_db/src/vendored/path.rs | 4 +- .../test/fixtures/flake8_async/ASYNC240.py | 104 ++++++++ .../test/fixtures/flake8_builtins/A003.py | 15 ++ .../test/fixtures/pyupgrade/UP008.py | 16 ++ .../src/checkers/ast/analyze/expression.rs | 3 + crates/ruff_linter/src/checkers/ast/mod.rs | 4 +- crates/ruff_linter/src/codes.rs | 1 + crates/ruff_linter/src/preview.rs | 7 + .../ruff_linter/src/rules/flake8_async/mod.rs | 1 + .../rules/blocking_path_methods.rs | 246 ++++++++++++++++++ .../src/rules/flake8_async/rules/mod.rs | 2 + ...e8_async__tests__ASYNC240_ASYNC240.py.snap | 111 ++++++++ .../src/rules/flake8_builtins/mod.rs | 23 ++ .../rules/builtin_attribute_shadowing.rs | 23 +- ...uiltins__tests__preview__A003_A003.py.snap | 50 ++++ crates/ruff_linter/src/rules/pyflakes/mod.rs | 36 +++ .../rules/super_call_with_parameters.rs | 10 +- crates/ruff_python_stdlib/src/builtins.rs | 21 ++ crates/ty/docs/environment.md | 7 + crates/ty/docs/rules.md | 177 +++++++------ crates/ty/tests/cli/python_environment.rs | 79 +++++- crates/ty_ide/src/inlay_hints.rs | 46 ++++ crates/ty_project/Cargo.toml | 1 + crates/ty_project/src/metadata/options.rs | 51 ++-- .../resources/mdtest/attributes.md | 52 ++-- .../mdtest/boundness_declaredness/public.md | 12 +- .../mdtest/call/callable_instance.md | 8 +- .../resources/mdtest/call/constructor.md | 14 +- .../resources/mdtest/call/dunder.md | 13 +- .../resources/mdtest/call/function.md | 9 +- .../resources/mdtest/call/methods.md | 2 +- .../resources/mdtest/class/super.md | 4 +- .../resources/mdtest/descriptor_protocol.md | 18 +- .../diagnostics/attribute_assignment.md | 6 +- .../mdtest/diagnostics/invalid_await.md | 2 +- .../mdtest/diagnostics/union_call.md | 2 +- .../resources/mdtest/enums.md | 49 ++++ .../resources/mdtest/expression/attribute.md | 2 +- .../mdtest/generics/legacy/functions.md | 25 ++ .../mdtest/generics/pep695/functions.md | 21 ++ .../resources/mdtest/import/conditional.md | 4 +- .../resources/mdtest/import/conventions.md | 6 +- .../resources/mdtest/loops/async_for.md | 4 +- .../resources/mdtest/loops/for.md | 14 +- .../resources/mdtest/narrow/assignment.md | 8 +- .../mdtest/narrow/conditionals/nested.md | 2 +- .../resources/mdtest/narrow/hasattr.md | 6 +- .../resources/mdtest/narrow/isinstance.md | 14 +- .../resources/mdtest/overloads.md | 14 +- .../mdtest/scopes/annotate_global.md | 31 +++ .../mdtest/scopes/moduletype_attrs.md | 4 + .../resources/mdtest/scopes/unbound.md | 4 +- ...g_`__\342\200\246_(33924dbae5117216).snap" | 2 +- ...g_`__\342\200\246_(e2600ca4708d9e54).snap" | 2 +- ...g_att\342\200\246_(e603e3da35f55c73).snap" | 18 +- ...g_`__\342\200\246_(77269542b8e81774).snap" | 2 +- ...g_`__\342\200\246_(9f781babda99d74b).snap" | 2 +- ...g_`__\342\200\246_(d8a02a0fcbb390a3).snap" | 2 +- ...th_pos\342\200\246_(a028edbafe180ca).snap" | 4 +- ...rd_re\342\200\246_(707b284610419a54).snap" | 20 +- .../mdtest/statically_known_branches.md | 2 +- .../resources/mdtest/subscript/instance.md | 8 +- .../mdtest/type_properties/is_subtype_of.md | 21 +- .../resources/mdtest/unreachable.md | 2 +- .../resources/mdtest/with/async.md | 4 +- .../resources/mdtest/with/sync.md | 4 +- crates/ty_python_semantic/src/place.rs | 81 ++++-- crates/ty_python_semantic/src/types.rs | 160 ++++++++++-- .../ty_python_semantic/src/types/call/bind.rs | 64 ++++- crates/ty_python_semantic/src/types/class.rs | 28 +- .../src/types/diagnostic.rs | 60 +++-- crates/ty_python_semantic/src/types/enums.rs | 17 +- .../ty_python_semantic/src/types/generics.rs | 12 + .../src/types/infer/builder.rs | 35 ++- .../src/types/signatures.rs | 19 +- .../e2e__commands__debug_command.snap | 7 +- crates/ty_static/src/env_vars.rs | 6 + playground/ruff/src/Editor/SourceEditor.tsx | 14 +- ruff.schema.json | 2 + ty.schema.json | 28 +- 83 files changed, 1658 insertions(+), 371 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC240.py create mode 100644 crates/ruff_linter/src/rules/flake8_async/rules/blocking_path_methods.rs create mode 100644 crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC240_ASYNC240.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__preview__A003_A003.py.snap create mode 100644 crates/ty_python_semantic/resources/mdtest/scopes/annotate_global.md rename "crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_unbound_`__\342\200\246_(42b1d61a2b7be1b5).snap" => "crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_missing_`__\342\200\246_(33924dbae5117216).snap" (94%) rename "crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_unbound_`__\342\200\246_(74ad2f945cad6ed8).snap" => "crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_missing_`__\342\200\246_(e2600ca4708d9e54).snap" (94%) rename "crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Possibly-unbound_att\342\200\246_(e5bdf78c427cb7fc).snap" => "crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Possibly-missing_att\342\200\246_(e603e3da35f55c73).snap" (52%) rename "crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(b1ce0da35c06026).snap" => "crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_missing_`__\342\200\246_(77269542b8e81774).snap" (98%) rename "crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(3b75cc467e6e012).snap" => "crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_missing_`__\342\200\246_(9f781babda99d74b).snap" (96%) rename "crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(8745233539d31200).snap" => "crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_missing_`__\342\200\246_(d8a02a0fcbb390a3).snap" (93%) rename "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_pos\342\200\246_(e3444b7a7f960d04).snap" => "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_pos\342\200\246_(a028edbafe180ca).snap" (92%) diff --git a/Cargo.lock b/Cargo.lock index 86857ec18268b..8fb3ec752f82a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1167,9 +1167,9 @@ dependencies = [ [[package]] name = "get-size-derive2" -version = "0.6.3" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a443e77201a230c25f0c11574e9b20e5705f749520e0f30ab0d0974fb1a794" +checksum = "e3814abc7da8ab18d2fd820f5b540b5e39b6af0a32de1bdd7c47576693074843" dependencies = [ "attribute-derive", "quote", @@ -1178,9 +1178,9 @@ dependencies = [ [[package]] name = "get-size2" -version = "0.6.3" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0594e2a78d082f2f8b1615c728391c6a5277f6c017474a7249934fc735945d55" +checksum = "5dfe2cec5b5ce8fb94dcdb16a1708baa4d0609cc3ce305ca5d3f6f2ffb59baed" dependencies = [ "compact_str", "get-size-derive2", @@ -4291,6 +4291,7 @@ dependencies = [ "tracing", "ty_combine", "ty_python_semantic", + "ty_static", "ty_vendored", ] diff --git a/Cargo.toml b/Cargo.toml index 3dd863ee994e6..3d7b30cb069c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,7 +86,7 @@ etcetera = { version = "0.10.0" } fern = { version = "0.7.0" } filetime = { version = "0.2.23" } getrandom = { version = "0.3.1" } -get-size2 = { version = "0.6.3", features = [ +get-size2 = { version = "0.7.0", features = [ "derive", "smallvec", "hashbrown", diff --git a/crates/ruff_db/src/system/path.rs b/crates/ruff_db/src/system/path.rs index d0ff92e5d8bc1..71a92fb4c8f67 100644 --- a/crates/ruff_db/src/system/path.rs +++ b/crates/ruff_db/src/system/path.rs @@ -504,8 +504,8 @@ impl ToOwned for SystemPath { pub struct SystemPathBuf(#[cfg_attr(feature = "schemars", schemars(with = "String"))] Utf8PathBuf); impl get_size2::GetSize for SystemPathBuf { - fn get_heap_size(&self) -> usize { - self.0.capacity() + fn get_heap_size_with_tracker(&self, tracker: T) -> (usize, T) { + (self.0.capacity(), tracker) } } diff --git a/crates/ruff_db/src/vendored/path.rs b/crates/ruff_db/src/vendored/path.rs index f59d093acbc40..86cdd5057e1e1 100644 --- a/crates/ruff_db/src/vendored/path.rs +++ b/crates/ruff_db/src/vendored/path.rs @@ -92,8 +92,8 @@ impl ToOwned for VendoredPath { pub struct VendoredPathBuf(Utf8PathBuf); impl get_size2::GetSize for VendoredPathBuf { - fn get_heap_size(&self) -> usize { - self.0.capacity() + fn get_heap_size_with_tracker(&self, tracker: T) -> (usize, T) { + (self.0.capacity(), tracker) } } diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC240.py b/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC240.py new file mode 100644 index 0000000000000..6eff90e56cfbd --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC240.py @@ -0,0 +1,104 @@ +import os +from typing import Optional +from pathlib import Path + +## Valid cases: + +def os_path_in_foo(): + file = "file.txt" + + os.path.abspath(file) # OK + os.path.exists(file) # OK + os.path.split() # OK + +async def non_io_os_path_methods(): + os.path.split() # OK + os.path.dirname() # OK + os.path.basename() # OK + os.path.join() # OK + +def pathlib_path_in_foo(): + path = Path("src/my_text.txt") # OK + path.exists() # OK + with path.open() as f: # OK + ... + path = Path("src/my_text.txt").open() # OK + +async def non_io_pathlib_path_methods(): + path = Path("src/my_text.txt") + path.is_absolute() # OK + path.is_relative_to() # OK + path.as_posix() # OK + path.relative_to() # OK + +def inline_path_method_call(): + Path("src/my_text.txt").open() # OK + Path("src/my_text.txt").open().flush() # OK + with Path("src/my_text.txt").open() as f: # OK + ... + +async def trio_path_in_foo(): + from trio import Path + + path = Path("src/my_text.txt") # OK + await path.absolute() # OK + await path.exists() # OK + with Path("src/my_text.txt").open() as f: # OK + ... + +async def anyio_path_in_foo(): + from anyio import Path + + path = Path("src/my_text.txt") # OK + await path.absolute() # OK + await path.exists() # OK + with Path("src/my_text.txt").open() as f: # OK + ... + +async def path_open_in_foo(): + path = Path("src/my_text.txt") # OK + path.open() # OK, covered by ASYNC230 + +## Invalid cases: + +async def os_path_in_foo(): + file = "file.txt" + + os.path.abspath(file) # ASYNC240 + os.path.exists(file) # ASYNC240 + +async def pathlib_path_in_foo(): + path = Path("src/my_text.txt") + path.exists() # ASYNC240 + +async def pathlib_path_in_foo(): + import pathlib + + path = pathlib.Path("src/my_text.txt") + path.exists() # ASYNC240 + +async def inline_path_method_call(): + Path("src/my_text.txt").exists() # ASYNC240 + Path("src/my_text.txt").absolute().exists() # ASYNC240 + +async def aliased_path_in_foo(): + from pathlib import Path as PathAlias + + path = PathAlias("src/my_text.txt") + path.exists() # ASYNC240 + +global_path = Path("src/my_text.txt") + +async def global_path_in_foo(): + global_path.exists() # ASYNC240 + +async def path_as_simple_parameter_type(path: Path): + path.exists() # ASYNC240 + +async def path_as_union_parameter_type(path: Path | None): + path.exists() # ASYNC240 + +async def path_as_optional_parameter_type(path: Optional[Path]): + path.exists() # ASYNC240 + + diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_builtins/A003.py b/crates/ruff_linter/resources/test/fixtures/flake8_builtins/A003.py index b2bb8b872ffc4..c7db608f2d77d 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_builtins/A003.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_builtins/A003.py @@ -19,3 +19,18 @@ def method_usage(self) -> str: def attribute_usage(self) -> id: pass + + +class C: + @staticmethod + def property(f): + return f + + id = 1 + + @[property][0] + def f(self, x=[id]): + return x + + bin = 2 + foo = [bin] diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP008.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP008.py index dd46c6c4d0196..96c3acc75d334 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP008.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP008.py @@ -287,3 +287,19 @@ class C(B): def f(self): C = B # Local variable C shadows the class name return super(C, self).f() # Should NOT trigger UP008 + + +# See: https://github.com/astral-sh/ruff/issues/20491 +# UP008 should not apply when __class__ is a local variable +class A: + def f(self): + return 1 + +class B(A): + def f(self): + return 2 + +class C(B): + def f(self): + __class__ = B # Local variable __class__ shadows the implicit __class__ + return super(__class__, self).f() # Should NOT trigger UP008 diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 65c61ff802eef..d664b7da98412 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -669,6 +669,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if checker.is_rule_enabled(Rule::BlockingOpenCallInAsyncFunction) { flake8_async::rules::blocking_open_call(checker, call); } + if checker.is_rule_enabled(Rule::BlockingPathMethodInAsyncFunction) { + flake8_async::rules::blocking_os_path(checker, call); + } if checker.any_rule_enabled(&[ Rule::CreateSubprocessInAsyncFunction, Rule::RunProcessInAsyncFunction, diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 2434ae19cb3ec..ed8ab2b585672 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -56,7 +56,7 @@ use ruff_python_semantic::{ Import, Module, ModuleKind, ModuleSource, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport, }; -use ruff_python_stdlib::builtins::{MAGIC_GLOBALS, python_builtins}; +use ruff_python_stdlib::builtins::{python_builtins, python_magic_globals}; use ruff_python_trivia::CommentRanges; use ruff_source_file::{OneIndexed, SourceFile, SourceFileBuilder, SourceRow}; use ruff_text_size::{Ranged, TextRange, TextSize}; @@ -2550,7 +2550,7 @@ impl<'a> Checker<'a> { for builtin in standard_builtins { bind_builtin(builtin); } - for builtin in MAGIC_GLOBALS { + for builtin in python_magic_globals(target_version.minor) { bind_builtin(builtin); } for builtin in &settings.builtins { diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index bcfbcf2d557cf..6b2a317baedc6 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -341,6 +341,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Async, "221") => (RuleGroup::Stable, rules::flake8_async::rules::RunProcessInAsyncFunction), (Flake8Async, "222") => (RuleGroup::Stable, rules::flake8_async::rules::WaitForProcessInAsyncFunction), (Flake8Async, "230") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingOpenCallInAsyncFunction), + (Flake8Async, "240") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingPathMethodInAsyncFunction), (Flake8Async, "250") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingInputInAsyncFunction), (Flake8Async, "251") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingSleepInAsyncFunction), diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 2459fd1afcdd9..be0a925fc7c21 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -228,3 +228,10 @@ pub(crate) const fn is_sim910_expanded_key_support_enabled(settings: &LinterSett pub(crate) const fn is_fix_builtin_open_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() } + +// https://github.com/astral-sh/ruff/pull/20178 +pub(crate) const fn is_a003_class_scope_shadowing_expansion_enabled( + settings: &LinterSettings, +) -> bool { + settings.preview.is_enabled() +} diff --git a/crates/ruff_linter/src/rules/flake8_async/mod.rs b/crates/ruff_linter/src/rules/flake8_async/mod.rs index bdfee91469c05..1c92fe835827c 100644 --- a/crates/ruff_linter/src/rules/flake8_async/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_async/mod.rs @@ -28,6 +28,7 @@ mod tests { #[test_case(Rule::RunProcessInAsyncFunction, Path::new("ASYNC22x.py"))] #[test_case(Rule::WaitForProcessInAsyncFunction, Path::new("ASYNC22x.py"))] #[test_case(Rule::BlockingOpenCallInAsyncFunction, Path::new("ASYNC230.py"))] + #[test_case(Rule::BlockingPathMethodInAsyncFunction, Path::new("ASYNC240.py"))] #[test_case(Rule::BlockingInputInAsyncFunction, Path::new("ASYNC250.py"))] #[test_case(Rule::BlockingSleepInAsyncFunction, Path::new("ASYNC251.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_path_methods.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_path_methods.rs new file mode 100644 index 0000000000000..14827c5b43d20 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_path_methods.rs @@ -0,0 +1,246 @@ +use crate::Violation; +use crate::checkers::ast::Checker; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{self as ast, Expr, ExprCall}; +use ruff_python_semantic::analyze::typing::{TypeChecker, check_type, traverse_union_and_optional}; +use ruff_text_size::Ranged; + +/// ## What it does +/// Checks that async functions do not call blocking `os.path` or `pathlib.Path` +/// methods. +/// +/// ## Why is this bad? +/// Calling some `os.path` or `pathlib.Path` methods in an async function will block +/// the entire event loop, preventing it from executing other tasks while waiting +/// for the operation. This negates the benefits of asynchronous programming. +/// +/// Instead, use the methods' async equivalents from `trio.Path` or `anyio.Path`. +/// +/// ## Example +/// ```python +/// import os +/// +/// +/// async def func(): +/// path = "my_file.txt" +/// file_exists = os.path.exists(path) +/// ``` +/// +/// Use instead: +/// ```python +/// import trio +/// +/// +/// async def func(): +/// path = trio.Path("my_file.txt") +/// file_exists = await path.exists() +/// ``` +/// +/// Non-blocking methods are OK to use: +/// ```python +/// import pathlib +/// +/// +/// async def func(): +/// path = pathlib.Path("my_file.txt") +/// file_dirname = path.dirname() +/// new_path = os.path.join("/tmp/src/", path) +/// ``` +#[derive(ViolationMetadata)] +pub(crate) struct BlockingPathMethodInAsyncFunction { + path_library: String, +} + +impl Violation for BlockingPathMethodInAsyncFunction { + #[derive_message_formats] + fn message(&self) -> String { + format!( + "Async functions should not use {path_library} methods, use trio.Path or anyio.path", + path_library = self.path_library + ) + } +} + +/// ASYNC240 +pub(crate) fn blocking_os_path(checker: &Checker, call: &ExprCall) { + let semantic = checker.semantic(); + if !semantic.in_async_context() { + return; + } + + // Check if an expression is calling I/O related os.path method. + // Just initializing pathlib.Path object is OK, we can return + // early in that scenario. + if let Some(qualified_name) = semantic.resolve_qualified_name(call.func.as_ref()) { + let segments = qualified_name.segments(); + if !matches!(segments, ["os", "path", _]) { + return; + } + + let Some(os_path_method) = segments.last() else { + return; + }; + + if maybe_calling_io_operation(os_path_method) { + checker.report_diagnostic( + BlockingPathMethodInAsyncFunction { + path_library: "os.path".to_string(), + }, + call.func.range(), + ); + } + return; + } + + let Some(ast::ExprAttribute { value, attr, .. }) = call.func.as_attribute_expr() else { + return; + }; + + if !maybe_calling_io_operation(attr.id.as_str()) { + return; + } + + // Check if an expression is a pathlib.Path constructor that directly + // calls an I/O method. + if PathlibPathChecker::match_initializer(value, semantic) { + checker.report_diagnostic( + BlockingPathMethodInAsyncFunction { + path_library: "pathlib.Path".to_string(), + }, + call.func.range(), + ); + return; + } + + // Lastly, check if a variable is a pathlib.Path instance and it's + // calling an I/O method. + let Some(name) = value.as_name_expr() else { + return; + }; + + let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else { + return; + }; + + if check_type::(binding, semantic) { + checker.report_diagnostic( + BlockingPathMethodInAsyncFunction { + path_library: "pathlib.Path".to_string(), + }, + call.func.range(), + ); + } +} +struct PathlibPathChecker; + +impl PathlibPathChecker { + fn is_pathlib_path_constructor( + semantic: &ruff_python_semantic::SemanticModel, + expr: &Expr, + ) -> bool { + let Some(qualified_name) = semantic.resolve_qualified_name(expr) else { + return false; + }; + + matches!( + qualified_name.segments(), + [ + "pathlib", + "Path" + | "PosixPath" + | "PurePath" + | "PurePosixPath" + | "PureWindowsPath" + | "WindowsPath" + ] + ) + } +} + +impl TypeChecker for PathlibPathChecker { + fn match_annotation(annotation: &Expr, semantic: &ruff_python_semantic::SemanticModel) -> bool { + if Self::is_pathlib_path_constructor(semantic, annotation) { + return true; + } + + let mut found = false; + traverse_union_and_optional( + &mut |inner_expr, _| { + if Self::is_pathlib_path_constructor(semantic, inner_expr) { + found = true; + } + }, + semantic, + annotation, + ); + found + } + + fn match_initializer( + initializer: &Expr, + semantic: &ruff_python_semantic::SemanticModel, + ) -> bool { + let Expr::Call(ast::ExprCall { func, .. }) = initializer else { + return false; + }; + + Self::is_pathlib_path_constructor(semantic, func) + } +} + +fn maybe_calling_io_operation(attr: &str) -> bool { + // ".open()" is added to the allow list to let ASYNC 230 handle + // that case. + !matches!( + attr, + "ALLOW_MISSING" + | "altsep" + | "anchor" + | "as_posix" + | "as_uri" + | "basename" + | "commonpath" + | "commonprefix" + | "curdir" + | "defpath" + | "devnull" + | "dirname" + | "drive" + | "expandvars" + | "extsep" + | "genericpath" + | "is_absolute" + | "is_relative_to" + | "is_reserved" + | "isabs" + | "join" + | "joinpath" + | "match" + | "name" + | "normcase" + | "os" + | "open" + | "pardir" + | "parent" + | "parents" + | "parts" + | "pathsep" + | "relative_to" + | "root" + | "samestat" + | "sep" + | "split" + | "splitdrive" + | "splitext" + | "splitroot" + | "stem" + | "suffix" + | "suffixes" + | "supports_unicode_filenames" + | "sys" + | "with_name" + | "with_segments" + | "with_stem" + | "with_suffix" + ) +} diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_async/rules/mod.rs index 86103c5980cee..3e12ea360c995 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/mod.rs @@ -5,6 +5,7 @@ pub(crate) use blocking_http_call::*; pub(crate) use blocking_http_call_httpx::*; pub(crate) use blocking_input::*; pub(crate) use blocking_open_call::*; +pub(crate) use blocking_path_methods::*; pub(crate) use blocking_process_invocation::*; pub(crate) use blocking_sleep::*; pub(crate) use cancel_scope_no_checkpoint::*; @@ -18,6 +19,7 @@ mod blocking_http_call; mod blocking_http_call_httpx; mod blocking_input; mod blocking_open_call; +mod blocking_path_methods; mod blocking_process_invocation; mod blocking_sleep; mod cancel_scope_no_checkpoint; diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC240_ASYNC240.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC240_ASYNC240.py.snap new file mode 100644 index 0000000000000..888905aed7f10 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC240_ASYNC240.py.snap @@ -0,0 +1,111 @@ +--- +source: crates/ruff_linter/src/rules/flake8_async/mod.rs +--- +ASYNC240 Async functions should not use os.path methods, use trio.Path or anyio.path + --> ASYNC240.py:67:5 + | +65 | file = "file.txt" +66 | +67 | os.path.abspath(file) # ASYNC240 + | ^^^^^^^^^^^^^^^ +68 | os.path.exists(file) # ASYNC240 + | + +ASYNC240 Async functions should not use os.path methods, use trio.Path or anyio.path + --> ASYNC240.py:68:5 + | +67 | os.path.abspath(file) # ASYNC240 +68 | os.path.exists(file) # ASYNC240 + | ^^^^^^^^^^^^^^ +69 | +70 | async def pathlib_path_in_foo(): + | + +ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path + --> ASYNC240.py:72:5 + | +70 | async def pathlib_path_in_foo(): +71 | path = Path("src/my_text.txt") +72 | path.exists() # ASYNC240 + | ^^^^^^^^^^^ +73 | +74 | async def pathlib_path_in_foo(): + | + +ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path + --> ASYNC240.py:78:5 + | +77 | path = pathlib.Path("src/my_text.txt") +78 | path.exists() # ASYNC240 + | ^^^^^^^^^^^ +79 | +80 | async def inline_path_method_call(): + | + +ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path + --> ASYNC240.py:81:5 + | +80 | async def inline_path_method_call(): +81 | Path("src/my_text.txt").exists() # ASYNC240 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +82 | Path("src/my_text.txt").absolute().exists() # ASYNC240 + | + +ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path + --> ASYNC240.py:82:5 + | +80 | async def inline_path_method_call(): +81 | Path("src/my_text.txt").exists() # ASYNC240 +82 | Path("src/my_text.txt").absolute().exists() # ASYNC240 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +83 | +84 | async def aliased_path_in_foo(): + | + +ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path + --> ASYNC240.py:88:5 + | +87 | path = PathAlias("src/my_text.txt") +88 | path.exists() # ASYNC240 + | ^^^^^^^^^^^ +89 | +90 | global_path = Path("src/my_text.txt") + | + +ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path + --> ASYNC240.py:93:5 + | +92 | async def global_path_in_foo(): +93 | global_path.exists() # ASYNC240 + | ^^^^^^^^^^^^^^^^^^ +94 | +95 | async def path_as_simple_parameter_type(path: Path): + | + +ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path + --> ASYNC240.py:96:5 + | +95 | async def path_as_simple_parameter_type(path: Path): +96 | path.exists() # ASYNC240 + | ^^^^^^^^^^^ +97 | +98 | async def path_as_union_parameter_type(path: Path | None): + | + +ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path + --> ASYNC240.py:99:5 + | + 98 | async def path_as_union_parameter_type(path: Path | None): + 99 | path.exists() # ASYNC240 + | ^^^^^^^^^^^ +100 | +101 | async def path_as_optional_parameter_type(path: Optional[Path]): + | + +ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path + --> ASYNC240.py:102:5 + | +101 | async def path_as_optional_parameter_type(path: Optional[Path]): +102 | path.exists() # ASYNC240 + | ^^^^^^^^^^^ + | diff --git a/crates/ruff_linter/src/rules/flake8_builtins/mod.rs b/crates/ruff_linter/src/rules/flake8_builtins/mod.rs index 6f863dc2e3908..315ab704e72ad 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/mod.rs @@ -14,6 +14,7 @@ mod tests { use crate::registry::Rule; use crate::rules::flake8_builtins; use crate::settings::LinterSettings; + use crate::settings::types::PreviewMode; use crate::test::{test_path, test_resource_path}; use ruff_python_ast::PythonVersion; @@ -63,6 +64,28 @@ mod tests { Ok(()) } + #[test_case(Rule::BuiltinAttributeShadowing, Path::new("A003.py"))] + fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!( + "preview__{}_{}", + rule_code.noqa_code(), + path.to_string_lossy() + ); + let diagnostics = test_path( + Path::new("flake8_builtins").join(path).as_path(), + &LinterSettings { + preview: PreviewMode::Enabled, + flake8_builtins: flake8_builtins::settings::Settings { + strict_checking: true, + ..Default::default() + }, + ..LinterSettings::for_rule(rule_code) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + #[test_case( Rule::StdlibModuleShadowing, Path::new("A005/modules/utils/logging.py"), diff --git a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs index b0e5c4979c7d0..948e0c892d62a 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/rules/builtin_attribute_shadowing.rs @@ -6,6 +6,7 @@ use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; +use crate::preview::is_a003_class_scope_shadowing_expansion_enabled; use crate::rules::flake8_builtins::helpers::shadows_builtin; /// ## What it does @@ -123,16 +124,26 @@ pub(crate) fn builtin_attribute_shadowing( // def repeat(value: int, times: int) -> list[int]: // return [value] * times // ``` + // In stable, only consider references whose first non-type parent scope is the class + // scope (e.g., decorators, default args, and attribute initializers). + // In preview, also consider references from within the class scope. + let consider_reference = |reference_scope_id: ScopeId| { + if is_a003_class_scope_shadowing_expansion_enabled(checker.settings()) { + if reference_scope_id == scope_id { + return true; + } + } + checker + .semantic() + .first_non_type_parent_scope_id(reference_scope_id) + == Some(scope_id) + }; + for reference in binding .references .iter() .map(|reference_id| checker.semantic().reference(*reference_id)) - .filter(|reference| { - checker - .semantic() - .first_non_type_parent_scope_id(reference.scope_id()) - == Some(scope_id) - }) + .filter(|reference| consider_reference(reference.scope_id())) { checker.report_diagnostic( BuiltinAttributeShadowing { diff --git a/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__preview__A003_A003.py.snap b/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__preview__A003_A003.py.snap new file mode 100644 index 0000000000000..6fd8c6f96104c --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_builtins/snapshots/ruff_linter__rules__flake8_builtins__tests__preview__A003_A003.py.snap @@ -0,0 +1,50 @@ +--- +source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs +--- +A003 Python builtin is shadowed by method `str` from line 14 + --> A003.py:17:31 + | +15 | pass +16 | +17 | def method_usage(self) -> str: + | ^^^ +18 | pass + | + +A003 Python builtin is shadowed by class attribute `id` from line 3 + --> A003.py:20:34 + | +18 | pass +19 | +20 | def attribute_usage(self) -> id: + | ^^ +21 | pass + | + +A003 Python builtin is shadowed by method `property` from line 26 + --> A003.py:31:7 + | +29 | id = 1 +30 | +31 | @[property][0] + | ^^^^^^^^ +32 | def f(self, x=[id]): +33 | return x + | + +A003 Python builtin is shadowed by class attribute `id` from line 29 + --> A003.py:32:20 + | +31 | @[property][0] +32 | def f(self, x=[id]): + | ^^ +33 | return x + | + +A003 Python builtin is shadowed by class attribute `bin` from line 35 + --> A003.py:36:12 + | +35 | bin = 2 +36 | foo = [bin] + | ^^^ + | diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index 7590c31a39f8f..b645198b494b8 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -920,6 +920,42 @@ mod tests { flakes("__annotations__", &[]); } + #[test] + fn module_warningregistry() { + // Using __warningregistry__ should not be considered undefined. + flakes("__warningregistry__", &[]); + } + + #[test] + fn module_annotate_py314_available() { + // __annotate__ is available starting in Python 3.14. + let diagnostics = crate::test::test_snippet( + "__annotate__", + &crate::settings::LinterSettings { + unresolved_target_version: ruff_python_ast::PythonVersion::PY314.into(), + ..crate::settings::LinterSettings::for_rules(vec![ + crate::codes::Rule::UndefinedName, + ]) + }, + ); + assert!(diagnostics.is_empty()); + } + + #[test] + fn module_annotate_pre_py314_undefined() { + // __annotate__ is not available before Python 3.14. + let diagnostics = crate::test::test_snippet( + "__annotate__", + &crate::settings::LinterSettings { + unresolved_target_version: ruff_python_ast::PythonVersion::PY313.into(), + ..crate::settings::LinterSettings::for_rules(vec![ + crate::codes::Rule::UndefinedName, + ]) + }, + ); + assert_eq!(diagnostics.len(), 1); + } + #[test] fn magic_globals_file() { // Use of the C{__file__} magic global should not emit an undefined name diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs index 30ff93d6be543..9f7f31a2f8a6c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs @@ -139,11 +139,11 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall return; }; - if !((first_arg_id == "__class__" - || (first_arg_id == parent_name.as_str() - // If the first argument matches the class name, check if it's a local variable - // that shadows the class name. If so, don't apply UP008. - && !checker.semantic().current_scope().has(first_arg_id))) + // The `super(__class__, self)` and `super(ParentClass, self)` patterns are redundant in Python 3 + // when the first argument refers to the implicit `__class__` cell or to the enclosing class. + // Avoid triggering if a local variable shadows either name. + if !(((first_arg_id == "__class__") || (first_arg_id == parent_name.as_str())) + && !checker.semantic().current_scope().has(first_arg_id) && second_arg_id == parent_arg.name().as_str()) { return; diff --git a/crates/ruff_python_stdlib/src/builtins.rs b/crates/ruff_python_stdlib/src/builtins.rs index 02830f9360c9c..5176de6a225cb 100644 --- a/crates/ruff_python_stdlib/src/builtins.rs +++ b/crates/ruff_python_stdlib/src/builtins.rs @@ -20,9 +20,15 @@ pub const MAGIC_GLOBALS: &[&str] = &[ "__annotations__", "__builtins__", "__cached__", + "__warningregistry__", "__file__", ]; +/// Magic globals that are only available starting in specific Python versions. +/// +/// `__annotate__` was introduced in Python 3.14. +static PY314_PLUS_MAGIC_GLOBALS: &[&str] = &["__annotate__"]; + static ALWAYS_AVAILABLE_BUILTINS: &[&str] = &[ "ArithmeticError", "AssertionError", @@ -216,6 +222,21 @@ pub fn python_builtins(minor_version: u8, is_notebook: bool) -> impl Iterator impl Iterator { + let py314_magic_globals = if minor_version >= 14 { + Some(PY314_PLUS_MAGIC_GLOBALS) + } else { + None + }; + + py314_magic_globals + .into_iter() + .flatten() + .chain(MAGIC_GLOBALS) + .copied() +} + /// Returns `true` if the given name is that of a Python builtin. /// /// Intended to be kept in sync with [`python_builtins`]. diff --git a/crates/ty/docs/environment.md b/crates/ty/docs/environment.md index c4552f2e58b49..9b9061518fee4 100644 --- a/crates/ty/docs/environment.md +++ b/crates/ty/docs/environment.md @@ -42,6 +42,13 @@ Used to determine if an active Conda environment is the base environment or not. Used to detect an activated Conda environment location. If both `VIRTUAL_ENV` and `CONDA_PREFIX` are present, `VIRTUAL_ENV` will be preferred. +### `PYTHONPATH` + +Adds additional directories to ty's search paths. +The format is the same as the shell’s PATH: +one or more directory pathnames separated by os appropriate pathsep +(e.g. colons on Unix or semicolons on Windows). + ### `RAYON_NUM_THREADS` Specifies an upper limit for the number of threads ty uses when performing work in parallel. diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 7904c1cb7f446..3371905cfe26f 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -36,7 +36,7 @@ def test(): -> "int": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L113) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L114) **What it does** @@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L157) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L158) **What it does** @@ -88,7 +88,7 @@ f(int) # error Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L183) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L184) **What it does** @@ -117,7 +117,7 @@ a = 1 Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L208) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L209) **What it does** @@ -147,7 +147,7 @@ class C(A, B): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L234) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L235) **What it does** @@ -177,7 +177,7 @@ class B(A): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L299) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L300) **What it does** @@ -202,7 +202,7 @@ class B(A, A): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L320) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L321) **What it does** @@ -306,7 +306,7 @@ def test(): -> "Literal[5]": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L523) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L524) **What it does** @@ -334,7 +334,7 @@ class C(A, B): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L547) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L548) **What it does** @@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L352) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L353) **What it does** @@ -445,7 +445,7 @@ an atypical memory layout. Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L592) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L593) **What it does** @@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L632) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L633) **What it does** @@ -496,7 +496,7 @@ a: int = '' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1666) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1688) **What it does** @@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L654) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L655) **What it does** @@ -562,7 +562,7 @@ asyncio.run(main()) Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L684) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L685) **What it does** @@ -584,7 +584,7 @@ class A(42): ... # error: [invalid-base] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L735) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L736) **What it does** @@ -609,7 +609,7 @@ with 1: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L756) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L757) **What it does** @@ -636,7 +636,7 @@ a: str Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L779) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L780) **What it does** @@ -678,7 +678,7 @@ except ZeroDivisionError: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L815) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L816) **What it does** @@ -709,7 +709,7 @@ class C[U](Generic[T]): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L567) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L568) **What it does** @@ -738,7 +738,7 @@ alice["height"] # KeyError: 'height' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L841) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L842) **What it does** @@ -771,7 +771,7 @@ def f(t: TypeVar("U")): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L890) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L891) **What it does** @@ -803,7 +803,7 @@ class B(metaclass=f): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L497) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L498) **What it does** @@ -833,7 +833,7 @@ TypeError: can only inherit from a NamedTuple type and Generic Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L917) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L918) **What it does** @@ -881,7 +881,7 @@ def foo(x: int) -> int: ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L960) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L961) **What it does** @@ -905,7 +905,7 @@ def f(a: int = ''): ... Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L434) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L435) **What it does** @@ -937,7 +937,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L980) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L981) Checks for `raise` statements that raise non-exceptions or use invalid @@ -984,7 +984,7 @@ def g(): Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L613) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L614) **What it does** @@ -1007,7 +1007,7 @@ def func() -> int: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1023) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1024) **What it does** @@ -1061,7 +1061,7 @@ TODO #14889 Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L869) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L870) **What it does** @@ -1086,7 +1086,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1062) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1063) **What it does** @@ -1114,7 +1114,7 @@ TYPE_CHECKING = '' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1086) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1087) **What it does** @@ -1142,7 +1142,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1138) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1139) **What it does** @@ -1174,7 +1174,7 @@ f(10) # Error Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1110) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1111) **What it does** @@ -1206,7 +1206,7 @@ class C: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1166) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1167) **What it does** @@ -1239,7 +1239,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1195) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1196) **What it does** @@ -1262,7 +1262,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1765) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1787) **What it does** @@ -1293,7 +1293,7 @@ alice["age"] # KeyError Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1214) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1215) **What it does** @@ -1320,7 +1320,7 @@ func("string") # error: [no-matching-overload] Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1237) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1238) **What it does** @@ -1342,7 +1342,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1255) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1256) **What it does** @@ -1366,7 +1366,7 @@ for i in 34: # TypeError: 'int' object is not iterable Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1306) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1307) **What it does** @@ -1386,6 +1386,31 @@ def f(x: int) -> int: ... f(1, x=2) # Error raised here ``` +## `positional-only-parameter-as-kwarg` + + +Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20positional-only-parameter-as-kwarg) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1542) + + +**What it does** + +Checks for keyword arguments in calls that match positional-only parameters of the callable. + +**Why is this bad?** + +Providing a positional-only parameter as a keyword argument will raise `TypeError` at runtime. + +**Example** + + +```python +def f(x: int, /) -> int: ... + +f(x=1) # Error raised here +``` + ## `raw-string-type-annotation` @@ -1420,7 +1445,7 @@ def test(): -> "int": Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1642) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1664) **What it does** @@ -1448,7 +1473,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1397) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1398) **What it does** @@ -1475,7 +1500,7 @@ class B(A): ... # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1442) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1443) **What it does** @@ -1500,7 +1525,7 @@ f("foo") # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1420) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1421) **What it does** @@ -1526,7 +1551,7 @@ def _(x: int): Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1463) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1464) **What it does** @@ -1570,7 +1595,7 @@ class A: Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1520) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1521) **What it does** @@ -1595,7 +1620,7 @@ f(x=1, y=2) # Error raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1541) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1563) **What it does** @@ -1621,7 +1646,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1563) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1585) **What it does** @@ -1644,7 +1669,7 @@ import foo # ModuleNotFoundError: No module named 'foo' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1582) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1604) **What it does** @@ -1667,7 +1692,7 @@ print(x) # NameError: name 'x' is not defined Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1275) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1276) **What it does** @@ -1702,7 +1727,7 @@ b1 < b2 < b1 # exception raised here Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1601) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1623) **What it does** @@ -1728,7 +1753,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A' Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1623) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1645) **What it does** @@ -1751,7 +1776,7 @@ l[1:10:0] # ValueError: slice step cannot be zero Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L462) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L463) **What it does** @@ -1790,7 +1815,7 @@ class SubProto(BaseProto, Protocol): Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L278) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L279) **What it does** @@ -1838,21 +1863,21 @@ Use instead: a = 20 / 0 # type: ignore ``` -## `possibly-unbound-attribute` +## `possibly-missing-attribute` Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · -[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1327) +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-attribute) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1328) **What it does** -Checks for possibly unbound attributes. +Checks for possibly missing attributes. **Why is this bad?** -Attempting to access an unbound attribute will raise an `AttributeError` at runtime. +Attempting to access a missing attribute will raise an `AttributeError` at runtime. **Examples** @@ -1864,23 +1889,23 @@ class A: A.c # AttributeError: type object 'A' has no attribute 'c' ``` -## `possibly-unbound-implicit-call` +## `possibly-missing-implicit-call` Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · -[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L131) +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-implicit-call) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L132) **What it does** -Checks for implicit calls to possibly unbound methods. +Checks for implicit calls to possibly missing methods. **Why is this bad?** Expressions such as `x[y]` and `x * y` call methods under the hood (`__getitem__` and `__mul__` respectively). -Calling an unbound method will raise an `AttributeError` at runtime. +Calling a missing method will raise an `AttributeError` at runtime. **Examples** @@ -1894,21 +1919,21 @@ class A: A()[0] # TypeError: 'A' object is not subscriptable ``` -## `possibly-unbound-import` +## `possibly-missing-import` Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · -[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1349) +[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import) · +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1350) **What it does** -Checks for imports of symbols that may be unbound. +Checks for imports of symbols that may be missing. **Why is this bad?** -Importing an unbound module or name will raise a `ModuleNotFoundError` +Importing a missing module or name will raise a `ModuleNotFoundError` or `ImportError` at runtime. **Examples** @@ -1929,7 +1954,7 @@ from module import a # ImportError: cannot import name 'a' from 'module' Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1694) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1716) **What it does** @@ -1954,7 +1979,7 @@ cast(int, f()) # Redundant Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1502) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1503) **What it does** @@ -2005,7 +2030,7 @@ a = 20 / 0 # ty: ignore[division-by-zero] Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1715) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1737) **What it does** @@ -2059,7 +2084,7 @@ def g(): Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L702) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L703) **What it does** @@ -2096,7 +2121,7 @@ class D(C): ... # error: [unsupported-base] Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L260) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L261) **What it does** @@ -2118,7 +2143,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime. Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") · [Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) · -[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1375) +[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1376) **What it does** diff --git a/crates/ty/tests/cli/python_environment.rs b/crates/ty/tests/cli/python_environment.rs index 8b9dd12cf59f2..062dbbef9bbb4 100644 --- a/crates/ty/tests/cli/python_environment.rs +++ b/crates/ty/tests/cli/python_environment.rs @@ -1879,7 +1879,7 @@ fn default_root_python_package_pyi() -> anyhow::Result<()> { #[test] fn pythonpath_is_respected() -> anyhow::Result<()> { let case = CliTest::with_files([ - ("src/bar/baz.py", "it = 42"), + ("baz-dir/baz.py", "it = 42"), ( "src/foo.py", r#" @@ -1915,7 +1915,82 @@ fn pythonpath_is_respected() -> anyhow::Result<()> { "#); assert_cmd_snapshot!(case.command() - .env("PYTHONPATH", case.root().join("src/bar")), + .env("PYTHONPATH", case.root().join("baz-dir")), + @r#" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + Ok(()) +} + +#[test] +fn pythonpath_multiple_dirs_is_respected() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ("baz-dir/baz.py", "it = 42"), + ("foo-dir/foo.py", "it = 42"), + ( + "src/main.py", + r#" + import baz + import foo + + print(f"{baz.it}") + print(f"{foo.it}") + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), + @r#" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Cannot resolve imported module `baz` + --> src/main.py:2:8 + | + 2 | import baz + | ^^^ + 3 | import foo + | + info: Searched in the following paths during module resolution: + info: 1. / (first-party code) + info: 2. /src (first-party code) + info: 3. vendored://stdlib (stdlib typeshed stubs vendored by ty) + info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment + info: rule `unresolved-import` is enabled by default + + error[unresolved-import]: Cannot resolve imported module `foo` + --> src/main.py:3:8 + | + 2 | import baz + 3 | import foo + | ^^^ + 4 | + 5 | print(f"{baz.it}") + | + info: Searched in the following paths during module resolution: + info: 1. / (first-party code) + info: 2. /src (first-party code) + info: 3. vendored://stdlib (stdlib typeshed stubs vendored by ty) + info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment + info: rule `unresolved-import` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + let pythonpath = + std::env::join_paths([case.root().join("baz-dir"), case.root().join("foo-dir")])?; + assert_cmd_snapshot!(case.command() + .env("PYTHONPATH", pythonpath), @r#" success: true exit_code: 0 diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 6346d17367bc9..87dbcd1d11a2c 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -283,6 +283,15 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { } source_order::walk_expr(self, expr); } + Expr::Attribute(attribute) => { + if self.in_assignment { + if attribute.ctx.is_store() { + let ty = expr.inferred_type(&self.model); + self.add_type_hint(expr.range().end(), ty); + } + } + source_order::walk_expr(self, expr); + } Expr::Call(call) => { let details = inlay_hint_call_argument_details(self.db, &self.model, call) .unwrap_or_default(); @@ -521,6 +530,43 @@ mod tests { "); } + #[test] + fn test_assign_attribute_of_instance() { + let test = inlay_hint_test( + " + class A: + def __init__(self, y): + self.x = 1 + self.y = y + + a = A(2) + a.y = 3 + ", + ); + + assert_snapshot!(test.inlay_hints(), @r" + class A: + def __init__(self, y): + self.x[: Literal[1]] = 1 + self.y[: Unknown] = y + + a[: A] = A([y=]2) + a.y[: Literal[3]] = 3 + + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:3:24 + | + 2 | class A: + 3 | def __init__(self, y): + | ^ + 4 | self.x = 1 + 5 | self.y = y + | + info: For inlay hint label 'y' at 112..113 + "); + } + #[test] fn test_disabled_variable_types() { let test = inlay_hint_test("x = 1"); diff --git a/crates/ty_project/Cargo.toml b/crates/ty_project/Cargo.toml index 6ccc1e494c0e6..7fe125e534fd1 100644 --- a/crates/ty_project/Cargo.toml +++ b/crates/ty_project/Cargo.toml @@ -22,6 +22,7 @@ ruff_python_formatter = { workspace = true, optional = true } ruff_text_size = { workspace = true } ty_combine = { workspace = true } ty_python_semantic = { workspace = true, features = ["serde"] } +ty_static = { workspace = true } ty_vendored = { workspace = true } anyhow = { workspace = true } diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index 71a670ed8aa16..9bcc0067c4643 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -34,6 +34,7 @@ use ty_python_semantic::{ PythonVersionSource, PythonVersionWithSource, SearchPathSettings, SearchPathValidationError, SearchPaths, SitePackagesPaths, SysPrefixPathOrigin, }; +use ty_static::EnvVars; #[derive( Debug, @@ -296,38 +297,48 @@ impl Options { }; // collect the existing site packages - let mut extra_paths: Vec = Vec::new(); + let mut extra_paths: Vec = environment + .extra_paths + .as_deref() + .unwrap_or_default() + .iter() + .map(|path| path.absolute(project_root, system)) + .collect(); // read all the paths off the PYTHONPATH environment variable, check // they exist as a directory, and add them to the vec of extra_paths // as they should be checked before site-packages just like python // interpreter does - if let Ok(python_path) = system.env_var("PYTHONPATH") { - for path in python_path.split(':') { - let possible_path = SystemPath::absolute(path, system.current_directory()); + if let Ok(python_path) = system.env_var(EnvVars::PYTHONPATH) { + for path in std::env::split_paths(python_path.as_str()) { + let path = match SystemPathBuf::from_path_buf(path) { + Ok(path) => path, + Err(path) => { + tracing::debug!( + "Skipping `{path}` listed in `PYTHONPATH` because the path is not valid UTF-8", + path = path.display() + ); + continue; + } + }; - if system.is_directory(&possible_path) { - tracing::debug!( - "Adding `{possible_path}` from the `PYTHONPATH` environment variable to `extra_paths`" - ); - extra_paths.push(possible_path); - } else { + let abspath = SystemPath::absolute(path, system.current_directory()); + + if !system.is_directory(&abspath) { tracing::debug!( - "Skipping `{possible_path}` listed in `PYTHONPATH` because the path doesn't exist or isn't a directory" + "Skipping `{abspath}` listed in `PYTHONPATH` because the path doesn't exist or isn't a directory" ); + continue; } + + tracing::debug!( + "Adding `{abspath}` from the `PYTHONPATH` environment variable to `extra_paths`" + ); + + extra_paths.push(abspath); } } - extra_paths.extend( - environment - .extra_paths - .as_deref() - .unwrap_or_default() - .iter() - .map(|path| path.absolute(project_root, system)), - ); - let settings = SearchPathSettings { extra_paths, src_roots, diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 0e49bf490d5c4..8f5742bcc7348 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -914,7 +914,7 @@ def _(flag: bool): reveal_type(C3.attr2) # revealed: Literal["metaclass value", "class value"] ``` -If the *metaclass* attribute is only partially defined, we emit a `possibly-unbound-attribute` +If the *metaclass* attribute is only partially defined, we emit a `possibly-missing-attribute` diagnostic: ```py @@ -924,12 +924,12 @@ def _(flag: bool): attr1: str = "metaclass value" class C4(metaclass=Meta4): ... - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] reveal_type(C4.attr1) # revealed: str ``` Finally, if both the metaclass attribute and the class-level attribute are only partially defined, -we union them and emit a `possibly-unbound-attribute` diagnostic: +we union them and emit a `possibly-missing-attribute` diagnostic: ```py def _(flag1: bool, flag2: bool): @@ -941,7 +941,7 @@ def _(flag1: bool, flag2: bool): if flag2: attr1 = "class value" - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] reveal_type(C5.attr1) # revealed: Unknown | Literal["metaclass value", "class value"] ``` @@ -1180,13 +1180,13 @@ def _(flag1: bool, flag2: bool): C = C1 if flag1 else C2 if flag2 else C3 - # error: [possibly-unbound-attribute] "Attribute `x` on type ` | | ` is possibly unbound" + # error: [possibly-missing-attribute] "Attribute `x` on type ` | | ` may be missing" reveal_type(C.x) # revealed: Unknown | Literal[1, 3] # error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type ` | | `" C.x = 100 - # error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound" + # error: [possibly-missing-attribute] "Attribute `x` on type `C1 | C2 | C3` may be missing" reveal_type(C().x) # revealed: Unknown | Literal[1, 3] # error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `C1 | C2 | C3`" @@ -1212,18 +1212,18 @@ def _(flag: bool, flag1: bool, flag2: bool): C = C1 if flag1 else C2 if flag2 else C3 - # error: [possibly-unbound-attribute] "Attribute `x` on type ` | | ` is possibly unbound" + # error: [possibly-missing-attribute] "Attribute `x` on type ` | | ` may be missing" reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3] - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] C.x = 100 - # Note: we might want to consider ignoring possibly-unbound diagnostics for instance attributes eventually, + # Note: we might want to consider ignoring possibly-missing diagnostics for instance attributes eventually, # see the "Possibly unbound/undeclared instance attribute" section below. - # error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound" + # error: [possibly-missing-attribute] "Attribute `x` on type `C1 | C2 | C3` may be missing" reveal_type(C().x) # revealed: Unknown | Literal[1, 2, 3] - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] C().x = 100 ``` @@ -1287,16 +1287,16 @@ def _(flag: bool): if flag: x = 2 - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1] - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] Bar.x = 3 - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1] - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] Bar().x = 3 ``` @@ -1304,7 +1304,7 @@ def _(flag: bool): We currently treat implicit instance attributes to be bound, even if they are only conditionally defined within a method. If the class-level definition or the whole method is only conditionally -available, we emit a `possibly-unbound-attribute` diagnostic. +available, we emit a `possibly-missing-attribute` diagnostic. #### Possibly unbound and undeclared @@ -1484,17 +1484,17 @@ def _(flag: bool): class B1: ... def inner1(a_and_b: Intersection[A1, B1]): - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] reveal_type(a_and_b.x) # revealed: P - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] a_and_b.x = R() # Same for class objects def inner1_class(a_and_b: Intersection[type[A1], type[B1]]): - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] reveal_type(a_and_b.x) # revealed: P - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] a_and_b.x = R() class A2: @@ -1509,7 +1509,7 @@ def _(flag: bool): # TODO: this should not be an error, we need better intersection # handling in `validate_attribute_assignment` for this - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] a_and_b.x = R() # Same for class objects def inner2_class(a_and_b: Intersection[type[A2], type[B1]]): @@ -1524,17 +1524,17 @@ def _(flag: bool): x: Q = Q() def inner3(a_and_b: Intersection[A3, B3]): - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] reveal_type(a_and_b.x) # revealed: P & Q - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] a_and_b.x = R() # Same for class objects def inner3_class(a_and_b: Intersection[type[A3], type[B3]]): - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] reveal_type(a_and_b.x) # revealed: P & Q - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] a_and_b.x = R() class A4: ... @@ -1649,7 +1649,7 @@ If an attribute is defined on the class, it takes precedence over the `__getattr reveal_type(c.class_attr) # revealed: int ``` -If the class attribute is possibly unbound, we union the type of the attribute with the fallback +If the class attribute is possibly missing, we union the type of the attribute with the fallback type of the `__getattr__` method: ```py diff --git a/crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md b/crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md index ab4f3ff0de064..5dc20fa1d48ba 100644 --- a/crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md +++ b/crates/ty_python_semantic/resources/mdtest/boundness_declaredness/public.md @@ -26,7 +26,7 @@ In particular, we should raise errors in the "possibly-undeclared-and-unbound" a | **Diagnostic** | declared | possibly-undeclared | undeclared | | ---------------- | -------- | ------------------------- | ------------------- | | bound | | | | -| possibly-unbound | | `possibly-unbound-import` | ? | +| possibly-unbound | | `possibly-missing-import` | ? | | unbound | | ? | `unresolved-import` | ## Declared @@ -158,7 +158,7 @@ a = None If a symbol is possibly undeclared and possibly unbound, we also use the union of the declared and inferred types. This case is interesting because the "possibly declared" definition might not be the -same as the "possibly bound" definition (symbol `b`). Note that we raise a `possibly-unbound-import` +same as the "possibly bound" definition (symbol `b`). Note that we raise a `possibly-missing-import` error for both `a` and `b`: `mod.py`: @@ -177,8 +177,8 @@ else: ``` ```py -# error: [possibly-unbound-import] -# error: [possibly-unbound-import] +# error: [possibly-missing-import] "Member `a` of module `mod` may be missing" +# error: [possibly-missing-import] "Member `b` of module `mod` may be missing" from mod import a, b reveal_type(a) # revealed: Literal[1] | Any @@ -332,8 +332,8 @@ if flag(): ``` ```py -# error: [possibly-unbound-import] -# error: [possibly-unbound-import] +# error: [possibly-missing-import] +# error: [possibly-missing-import] from mod import MyInt, C reveal_type(MyInt) # revealed: diff --git a/crates/ty_python_semantic/resources/mdtest/call/callable_instance.md b/crates/ty_python_semantic/resources/mdtest/call/callable_instance.md index d6e36e061f908..52f61bb5ede9f 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/callable_instance.md +++ b/crates/ty_python_semantic/resources/mdtest/call/callable_instance.md @@ -19,7 +19,7 @@ b = Unit()(3.0) # error: "Object of type `Unit` is not callable" reveal_type(b) # revealed: Unknown ``` -## Possibly unbound `__call__` method +## Possibly missing `__call__` method ```py def _(flag: bool): @@ -29,7 +29,7 @@ def _(flag: bool): return 1 a = PossiblyNotCallable() - result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" + result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly missing `__call__` method)" reveal_type(result) # revealed: int ``` @@ -105,7 +105,7 @@ reveal_type(c()) # revealed: int ## Union over callables -### Possibly unbound `__call__` +### Possibly missing `__call__` ```py def outer(cond1: bool): @@ -122,6 +122,6 @@ def outer(cond1: bool): else: a = Other() - # error: [call-non-callable] "Object of type `Test` is not callable (possibly unbound `__call__` method)" + # error: [call-non-callable] "Object of type `Test` is not callable (possibly missing `__call__` method)" a() ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/constructor.md b/crates/ty_python_semantic/resources/mdtest/call/constructor.md index c8bdd664cda20..b6e65fcdf2bbd 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/constructor.md +++ b/crates/ty_python_semantic/resources/mdtest/call/constructor.md @@ -158,15 +158,15 @@ def _(flag: bool) -> None: def __new__(cls): return object.__new__(cls) - # error: [possibly-unbound-implicit-call] + # error: [possibly-missing-implicit-call] reveal_type(Foo()) # revealed: Foo - # error: [possibly-unbound-implicit-call] + # error: [possibly-missing-implicit-call] # error: [too-many-positional-arguments] reveal_type(Foo(1)) # revealed: Foo ``` -#### Possibly unbound `__call__` on `__new__` callable +#### Possibly missing `__call__` on `__new__` callable ```py def _(flag: bool) -> None: @@ -178,11 +178,11 @@ def _(flag: bool) -> None: class Foo: __new__ = Callable() - # error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)" + # error: [call-non-callable] "Object of type `Callable` is not callable (possibly missing `__call__` method)" reveal_type(Foo(1)) # revealed: Foo # TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" # but we currently infer the signature of `__call__` as unknown, so it accepts any arguments - # error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)" + # error: [call-non-callable] "Object of type `Callable` is not callable (possibly missing `__call__` method)" reveal_type(Foo()) # revealed: Foo ``` @@ -294,11 +294,11 @@ def _(flag: bool) -> None: class Foo: __init__ = Callable() - # error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)" + # error: [call-non-callable] "Object of type `Callable` is not callable (possibly missing `__call__` method)" reveal_type(Foo(1)) # revealed: Foo # TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`" # but we currently infer the signature of `__call__` as unknown, so it accepts any arguments - # error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)" + # error: [call-non-callable] "Object of type `Callable` is not callable (possibly missing `__call__` method)" reveal_type(Foo()) # revealed: Foo ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/dunder.md b/crates/ty_python_semantic/resources/mdtest/call/dunder.md index 7eadb96dae9ea..09b83f0035f89 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/dunder.md +++ b/crates/ty_python_semantic/resources/mdtest/call/dunder.md @@ -114,7 +114,11 @@ def _(flag: bool): this_fails = ThisFails() - # error: [possibly-unbound-implicit-call] + # TODO: this would be a friendlier diagnostic if we propagated the error up the stack + # and transformed it into a `[not-subscriptable]` error with a subdiagnostic explaining + # that the cause of the error was a possibly missing `__getitem__` method + # + # error: [possibly-missing-implicit-call] "Method `__getitem__` of type `ThisFails` may be missing" reveal_type(this_fails[0]) # revealed: Unknown | str ``` @@ -270,6 +274,11 @@ def _(flag: bool): return str(key) c = C() - # error: [possibly-unbound-implicit-call] + + # TODO: this would be a friendlier diagnostic if we propagated the error up the stack + # and transformed it into a `[not-subscriptable]` error with a subdiagnostic explaining + # that the cause of the error was a possibly missing `__getitem__` method + # + # error: [possibly-missing-implicit-call] "Method `__getitem__` of type `C` may be missing" reveal_type(c[0]) # revealed: str ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index dee6eb1866fa2..58ee1428a38a5 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -90,8 +90,7 @@ still continue to use the old convention, so it is supported by ty as well. def f(__x: int): ... f(1) -# error: [missing-argument] -# error: [unknown-argument] +# error: [positional-only-parameter-as-kwarg] f(__x=1) ``` @@ -131,11 +130,9 @@ class C: @staticmethod def static_method(self, __x: int): ... -# error: [missing-argument] -# error: [unknown-argument] +# error: [positional-only-parameter-as-kwarg] C().method(__x=1) -# error: [missing-argument] -# error: [unknown-argument] +# error: [positional-only-parameter-as-kwarg] C.class_method(__x="1") C.static_method("x", __x=42) # fine ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index 02893d9a24142..68c2175e6fa83 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -325,7 +325,7 @@ class D(metaclass=Meta): reveal_type(D.f(1)) # revealed: Literal["a"] ``` -If the class method is possibly unbound, we union the return types: +If the class method is possibly missing, we union the return types: ```py def flag() -> bool: diff --git a/crates/ty_python_semantic/resources/mdtest/class/super.md b/crates/ty_python_semantic/resources/mdtest/class/super.md index c7b6e0ef803af..b4befc0e44069 100644 --- a/crates/ty_python_semantic/resources/mdtest/class/super.md +++ b/crates/ty_python_semantic/resources/mdtest/class/super.md @@ -219,7 +219,7 @@ def f(x: C | D): s = super(A, x) reveal_type(s) # revealed: , C> | , D> - # error: [possibly-unbound-attribute] "Attribute `b` on type `, C> | , D>` is possibly unbound" + # error: [possibly-missing-attribute] "Attribute `b` on type `, C> | , D>` may be missing" s.b def f(flag: bool): @@ -259,7 +259,7 @@ def f(flag: bool): reveal_type(s.x) # revealed: Unknown | Literal[1, 2] reveal_type(s.y) # revealed: int | str - # error: [possibly-unbound-attribute] "Attribute `a` on type `, B> | , D>` is possibly unbound" + # error: [possibly-missing-attribute] "Attribute `a` on type `, B> | , D>` may be missing" reveal_type(s.a) # revealed: str ``` diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index 04b2d9209c308..ed80839a767b9 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -351,7 +351,7 @@ reveal_type(C4.meta_attribute) # revealed: Literal["value on metaclass"] reveal_type(C4.meta_non_data_descriptor) # revealed: Literal["non-data"] ``` -When a metaclass data descriptor is possibly unbound, we union the result type of its `__get__` +When a metaclass data descriptor is possibly missing, we union the result type of its `__get__` method with an underlying class level attribute, if present: ```py @@ -365,7 +365,7 @@ def _(flag: bool): meta_data_descriptor1: Literal["value on class"] = "value on class" reveal_type(C5.meta_data_descriptor1) # revealed: Literal["data", "value on class"] - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] reveal_type(C5.meta_data_descriptor2) # revealed: Literal["data"] # TODO: We currently emit two diagnostics here, corresponding to the two states of `flag`. The diagnostics are not @@ -375,11 +375,11 @@ def _(flag: bool): # error: [invalid-assignment] "Object of type `None` is not assignable to attribute `meta_data_descriptor1` of type `Literal["value on class"]`" C5.meta_data_descriptor1 = None - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] C5.meta_data_descriptor2 = 1 ``` -When a class-level attribute is possibly unbound, we union its (descriptor protocol) type with the +When a class-level attribute is possibly missing, we union its (descriptor protocol) type with the metaclass attribute (unless it's a data descriptor, which always takes precedence): ```py @@ -401,7 +401,7 @@ def _(flag: bool): reveal_type(C6.attribute1) # revealed: Literal["data"] reveal_type(C6.attribute2) # revealed: Literal["non-data", "value on class"] reveal_type(C6.attribute3) # revealed: Literal["value on metaclass", "value on class"] - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] reveal_type(C6.attribute4) # revealed: Literal["value on class"] ``` @@ -756,16 +756,16 @@ def _(flag: bool): non_data: NonDataDescriptor = NonDataDescriptor() data: DataDescriptor = DataDescriptor() - # error: [possibly-unbound-attribute] "Attribute `non_data` on type `` is possibly unbound" + # error: [possibly-missing-attribute] "Attribute `non_data` on type `` may be missing" reveal_type(PossiblyUnbound.non_data) # revealed: int - # error: [possibly-unbound-attribute] "Attribute `non_data` on type `PossiblyUnbound` is possibly unbound" + # error: [possibly-missing-attribute] "Attribute `non_data` on type `PossiblyUnbound` may be missing" reveal_type(PossiblyUnbound().non_data) # revealed: int - # error: [possibly-unbound-attribute] "Attribute `data` on type `` is possibly unbound" + # error: [possibly-missing-attribute] "Attribute `data` on type `` may be missing" reveal_type(PossiblyUnbound.data) # revealed: int - # error: [possibly-unbound-attribute] "Attribute `data` on type `PossiblyUnbound` is possibly unbound" + # error: [possibly-missing-attribute] "Attribute `data` on type `PossiblyUnbound` may be missing" reveal_type(PossiblyUnbound().data) # revealed: int ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md index 83db02c2be8e2..61816ff56fccb 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md @@ -69,7 +69,7 @@ instance = C() instance.non_existent = 1 # error: [unresolved-attribute] ``` -## Possibly-unbound attributes +## Possibly-missing attributes When trying to set an attribute that is not defined in all branches, we emit errors: @@ -79,10 +79,10 @@ def _(flag: bool) -> None: if flag: attr: int = 0 - C.attr = 1 # error: [possibly-unbound-attribute] + C.attr = 1 # error: [possibly-missing-attribute] instance = C() - instance.attr = 1 # error: [possibly-unbound-attribute] + instance.attr = 1 # error: [possibly-missing-attribute] ``` ## Data descriptors diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md index 60d3439366f17..24f5f98c49266 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md @@ -23,7 +23,7 @@ async def main() -> None: await MissingAwait() # error: [invalid-await] ``` -## Custom type with possibly unbound `__await__` +## Custom type with possibly missing `__await__` This diagnostic also points to the method definition if available. diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md index c6f8cd803a6f4..31cafa14bfd8e 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md @@ -116,7 +116,7 @@ def _(n: int): # error: [invalid-argument-type] "Argument to function `f5` is incorrect: Expected `str`, found `Literal[3]`" # error: [no-matching-overload] "No overload of function `f6` matches arguments" # error: [call-non-callable] "Object of type `Literal[5]` is not callable" - # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" + # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly missing `__call__` method)" x = f(3) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index 0e314e982cd55..2c3c03375ca28 100644 --- a/crates/ty_python_semantic/resources/mdtest/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/enums.md @@ -267,6 +267,11 @@ reveal_type(Color.red) ### Using `auto()` +```toml +[environment] +python-version = "3.11" +``` + ```py from enum import Enum, auto from ty_extensions import enum_members @@ -277,6 +282,50 @@ class Answer(Enum): # revealed: tuple[Literal["YES"], Literal["NO"]] reveal_type(enum_members(Answer)) + +reveal_type(Answer.YES.value) # revealed: Literal[1] +reveal_type(Answer.NO.value) # revealed: Literal[2] +``` + +Usages of `auto()` can be combined with manual value assignments: + +```py +class Mixed(Enum): + MANUAL_1 = -1 + AUTO_1 = auto() + MANUAL_2 = -2 + AUTO_2 = auto() + +reveal_type(Mixed.MANUAL_1.value) # revealed: Literal[-1] +reveal_type(Mixed.AUTO_1.value) # revealed: Literal[1] +reveal_type(Mixed.MANUAL_2.value) # revealed: Literal[-2] +reveal_type(Mixed.AUTO_2.value) # revealed: Literal[2] +``` + +When using `auto()` with `StrEnum`, the value is the lowercase name of the member: + +```py +from enum import StrEnum, auto + +class Answer(StrEnum): + YES = auto() + NO = auto() + +reveal_type(Answer.YES.value) # revealed: Literal["yes"] +reveal_type(Answer.NO.value) # revealed: Literal["no"] +``` + +Using `auto()` with `IntEnum` also works as expected: + +```py +from enum import IntEnum, auto + +class Answer(IntEnum): + YES = auto() + NO = auto() + +reveal_type(Answer.YES.value) # revealed: Literal[1] +reveal_type(Answer.NO.value) # revealed: Literal[2] ``` Combining aliases with `auto()`: diff --git a/crates/ty_python_semantic/resources/mdtest/expression/attribute.md b/crates/ty_python_semantic/resources/mdtest/expression/attribute.md index 43df56264cb5c..f59da0bc4856f 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/attribute.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/attribute.md @@ -26,7 +26,7 @@ def _(flag: bool): reveal_type(A.union_declared) # revealed: int | str - # error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `` is possibly unbound" + # error: [possibly-missing-attribute] "Attribute `possibly_unbound` on type `` may be missing" reveal_type(A.possibly_unbound) # revealed: str # error: [unresolved-attribute] "Type `` has no attribute `non_existent`" diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index c2fb6cbc1b081..3175ed7216dce 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -366,6 +366,31 @@ reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int] reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None ``` +## Passing generic functions to generic functions + +```py +from typing import Callable, TypeVar + +A = TypeVar("A") +B = TypeVar("B") +T = TypeVar("T") + +def invoke(fn: Callable[[A], B], value: A) -> B: + return fn(value) + +def identity(x: T) -> T: + return x + +def head(xs: list[T]) -> T: + return xs[0] + +# TODO: this should be `Literal[1]` +reveal_type(invoke(identity, 1)) # revealed: Unknown + +# TODO: this should be `Unknown | int` +reveal_type(invoke(head, [1, 2, 3])) # revealed: Unknown +``` + ## Opaque decorators don't affect typevar binding Inside the body of a generic function, we should be able to see that the typevars bound by that diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index 6dda932d4f4cd..a9224d46c800a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -323,6 +323,27 @@ reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int] reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None ``` +## Passing generic functions to generic functions + +```py +from typing import Callable + +def invoke[A, B](fn: Callable[[A], B], value: A) -> B: + return fn(value) + +def identity[T](x: T) -> T: + return x + +def head[T](xs: list[T]) -> T: + return xs[0] + +# TODO: this should be `Literal[1]` +reveal_type(invoke(identity, 1)) # revealed: Unknown + +# TODO: this should be `Unknown | int` +reveal_type(invoke(head, [1, 2, 3])) # revealed: Unknown +``` + ## Protocols as TypeVar bounds Protocol types can be used as TypeVar bounds, just like nominal types. diff --git a/crates/ty_python_semantic/resources/mdtest/import/conditional.md b/crates/ty_python_semantic/resources/mdtest/import/conditional.md index 703bf6078fd01..d2896ae2cd27a 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/conditional.md +++ b/crates/ty_python_semantic/resources/mdtest/import/conditional.md @@ -22,7 +22,7 @@ reveal_type(y) ``` ```py -# error: [possibly-unbound-import] "Member `y` of module `maybe_unbound` is possibly unbound" +# error: [possibly-missing-import] "Member `y` of module `maybe_unbound` may be missing" from maybe_unbound import x, y reveal_type(x) # revealed: Unknown | Literal[3] @@ -53,7 +53,7 @@ reveal_type(y) Importing an annotated name prefers the declared type over the inferred type: ```py -# error: [possibly-unbound-import] "Member `y` of module `maybe_unbound_annotated` is possibly unbound" +# error: [possibly-missing-import] "Member `y` of module `maybe_unbound_annotated` may be missing" from maybe_unbound_annotated import x, y reveal_type(x) # revealed: Unknown | Literal[3] diff --git a/crates/ty_python_semantic/resources/mdtest/import/conventions.md b/crates/ty_python_semantic/resources/mdtest/import/conventions.md index c4950b5d1cc61..48d93515a88d4 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/conventions.md +++ b/crates/ty_python_semantic/resources/mdtest/import/conventions.md @@ -306,7 +306,7 @@ The following scenarios are when a re-export happens conditionally in a stub fil ### Global import ```py -# error: "Member `Foo` of module `a` is possibly unbound" +# error: "Member `Foo` of module `a` may be missing" from a import Foo reveal_type(Foo) # revealed: str @@ -337,7 +337,7 @@ Here, both the branches of the condition are import statements where one of them the other does not. ```py -# error: "Member `Foo` of module `a` is possibly unbound" +# error: "Member `Foo` of module `a` may be missing" from a import Foo reveal_type(Foo) # revealed: @@ -365,7 +365,7 @@ class Foo: ... ### Re-export in one branch ```py -# error: "Member `Foo` of module `a` is possibly unbound" +# error: "Member `Foo` of module `a` may be missing" from a import Foo reveal_type(Foo) # revealed: diff --git a/crates/ty_python_semantic/resources/mdtest/loops/async_for.md b/crates/ty_python_semantic/resources/mdtest/loops/async_for.md index 25eee5c071710..1271e2b41f44b 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/async_for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/async_for.md @@ -88,7 +88,7 @@ async def foo(): reveal_type(x) # revealed: Unknown ``` -### Possibly unbound `__anext__` method +### Possibly missing `__anext__` method ```py from typing_extensions import reveal_type @@ -108,7 +108,7 @@ async def foo(flag: bool): reveal_type(x) # revealed: int ``` -### Possibly unbound `__aiter__` method +### Possibly missing `__aiter__` method ```py from typing_extensions import reveal_type diff --git a/crates/ty_python_semantic/resources/mdtest/loops/for.md b/crates/ty_python_semantic/resources/mdtest/loops/for.md index 9cc073b91e350..b0433156a3a9e 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/for.md @@ -363,7 +363,7 @@ for x in Bad(): reveal_type(x) # revealed: Unknown ``` -## `__iter__` returns an object with a possibly unbound `__next__` method +## `__iter__` returns an object with a possibly missing `__next__` method ```py def _(flag: bool): @@ -412,7 +412,7 @@ for y in Iterable2(): reveal_type(y) # revealed: Unknown ``` -## Possibly unbound `__iter__` and bad `__getitem__` method +## Possibly missing `__iter__` and bad `__getitem__` method @@ -438,12 +438,12 @@ def _(flag: bool): reveal_type(x) # revealed: int | bytes ``` -## Possibly unbound `__iter__` and not-callable `__getitem__` +## Possibly missing `__iter__` and not-callable `__getitem__` This snippet tests that we infer the element type correctly in the following edge case: - `__iter__` is a method with the correct parameter spec that returns a valid iterator; BUT -- `__iter__` is possibly unbound; AND +- `__iter__` is possibly missing; AND - `__getitem__` is set to a non-callable type It's important that we emit a diagnostic here, but it's also important that we still use the return @@ -466,7 +466,7 @@ def _(flag: bool): reveal_type(x) # revealed: int ``` -## Possibly unbound `__iter__` and possibly unbound `__getitem__` +## Possibly missing `__iter__` and possibly missing `__getitem__` @@ -560,7 +560,7 @@ for x in Iterable(): reveal_type(x) # revealed: int ``` -## Possibly unbound `__iter__` but definitely bound `__getitem__` +## Possibly missing `__iter__` but definitely bound `__getitem__` Here, we should not emit a diagnostic: if `__iter__` is unbound, we should fallback to `__getitem__`: @@ -694,7 +694,7 @@ def _(flag: bool): reveal_type(y) # revealed: str | int ``` -## Possibly unbound `__iter__` and possibly invalid `__getitem__` +## Possibly missing `__iter__` and possibly invalid `__getitem__` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md b/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md index 18dc4242a5d30..bc2def2752a0b 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md @@ -135,9 +135,9 @@ a.b = B() reveal_type(a.b) # revealed: B reveal_type(a.b.c1) # revealed: C | None reveal_type(a.b.c2) # revealed: C | None -# error: [possibly-unbound-attribute] +# error: [possibly-missing-attribute] reveal_type(a.b.c1.d) # revealed: D | None -# error: [possibly-unbound-attribute] +# error: [possibly-missing-attribute] reveal_type(a.b.c2.d) # revealed: D | None ``` @@ -295,9 +295,9 @@ class C: reveal_type(b.a.x[0]) # revealed: Literal[0] def _(): - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] reveal_type(b.a.x[0]) # revealed: Unknown | int | None - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] reveal_type(b.a.x) # revealed: Unknown | list[int | None] reveal_type(b.a) # revealed: Unknown | A | None ``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md index 52750aec89f6f..dd10ff95859e0 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/nested.md @@ -161,7 +161,7 @@ class _: a.b = B() class _: - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] reveal_type(a.b.c1.d) # revealed: D | None reveal_type(a.b.c1) # revealed: C | None ``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md b/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md index 02e07d80c279d..ab38563ee7df9 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md @@ -60,7 +60,7 @@ def _(obj: WithSpam): ``` When a class may or may not have a `spam` attribute, `hasattr` narrowing can provide evidence that -the attribute exists. Here, no `possibly-unbound-attribute` error is emitted in the `if` branch: +the attribute exists. Here, no `possibly-missing-attribute` error is emitted in the `if` branch: ```py def returns_bool() -> bool: @@ -71,7 +71,7 @@ class MaybeWithSpam: spam: int = 42 def _(obj: MaybeWithSpam): - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] reveal_type(obj.spam) # revealed: int if hasattr(obj, "spam"): @@ -81,7 +81,7 @@ def _(obj: MaybeWithSpam): reveal_type(obj) # revealed: MaybeWithSpam & ~ # TODO: Ideally, we would emit `[unresolved-attribute]` and reveal `Unknown` here: - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] reveal_type(obj.spam) # revealed: int ``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index d28d261fb1091..486c354c248db 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -321,8 +321,11 @@ a covariant generic, this is equivalent to using the upper bound of the type par `object`): ```py +from typing import Self + class Covariant[T]: - def get(self) -> T: + # TODO: remove the explicit `Self` annotation, once we support the implicit type of `self` + def get(self: Self) -> T: raise NotImplementedError def _(x: object): @@ -335,7 +338,8 @@ Similarly, contravariant type parameters use their lower bound of `Never`: ```py class Contravariant[T]: - def push(self, x: T) -> None: ... + # TODO: remove the explicit `Self` annotation, once we support the implicit type of `self` + def push(self: Self, x: T) -> None: ... def _(x: object): if isinstance(x, Contravariant): @@ -350,8 +354,10 @@ the type system, so we represent it with the internal `Top[]` special form. ```py class Invariant[T]: - def push(self, x: T) -> None: ... - def get(self) -> T: + # TODO: remove the explicit `Self` annotation, once we support the implicit type of `self` + def push(self: Self, x: T) -> None: ... + # TODO: remove the explicit `Self` annotation, once we support the implicit type of `self` + def get(self: Self) -> T: raise NotImplementedError def _(x: object): diff --git a/crates/ty_python_semantic/resources/mdtest/overloads.md b/crates/ty_python_semantic/resources/mdtest/overloads.md index 08bd0c3021ff2..8e8e9e524bfbb 100644 --- a/crates/ty_python_semantic/resources/mdtest/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/overloads.md @@ -99,7 +99,7 @@ reveal_type(foo(b"")) # revealed: bytes ## Methods ```py -from typing import overload +from typing_extensions import Self, overload class Foo1: @overload @@ -126,6 +126,18 @@ foo2 = Foo2() reveal_type(foo2.method) # revealed: Overload[() -> None, (x: str) -> str] reveal_type(foo2.method()) # revealed: None reveal_type(foo2.method("")) # revealed: str + +class Foo3: + @overload + def takes_self_or_int(self: Self, x: Self) -> Self: ... + @overload + def takes_self_or_int(self: Self, x: int) -> int: ... + def takes_self_or_int(self: Self, x: Self | int) -> Self | int: + return x + +foo3 = Foo3() +reveal_type(foo3.takes_self_or_int(foo3)) # revealed: Foo3 +reveal_type(foo3.takes_self_or_int(1)) # revealed: int ``` ## Constructor diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/annotate_global.md b/crates/ty_python_semantic/resources/mdtest/scopes/annotate_global.md new file mode 100644 index 0000000000000..221b92d777796 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/scopes/annotate_global.md @@ -0,0 +1,31 @@ +# `__annotate__` as an implicit global is version-gated (Py3.14+) + +## Absent before 3.14 + +`__annotate__` is never present in the global namespace on Python \<3.14. + +```toml +[environment] +python-version = "3.13" +``` + +```py +# error: [unresolved-reference] +reveal_type(__annotate__) # revealed: Unknown +``` + +## Present in 3.14+ + +The `__annotate__` global may be present in Python 3.14, but only if at least one global symbol in +the module is annotated (e.g. `x: int` or `x: int = 42`). Currently we model `__annotate__` as +always being possibly unbound on Python 3.14+. + +```toml +[environment] +python-version = "3.14" +``` + +```py +# error: [possibly-unresolved-reference] +reveal_type(__annotate__) # revealed: (format: int, /) -> dict[str, Any] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md b/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md index ddc5527bf8803..7c6c3e6d9ea32 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/moduletype_attrs.md @@ -16,6 +16,8 @@ reveal_type(__doc__) # revealed: str | None reveal_type(__spec__) # revealed: ModuleSpec | None reveal_type(__path__) # revealed: MutableSequence[str] reveal_type(__builtins__) # revealed: Any +# error: [possibly-unresolved-reference] "Name `__warningregistry__` used when possibly not defined" +reveal_type(__warningregistry__) # revealed: dict[Any, int] import sys @@ -75,6 +77,8 @@ reveal_type(module.__file__) # revealed: Unknown | None reveal_type(module.__path__) # revealed: list[str] reveal_type(module.__doc__) # revealed: Unknown reveal_type(module.__spec__) # revealed: Unknown | ModuleSpec | None +# error: [unresolved-attribute] +reveal_type(module.__warningregistry__) # revealed: Unknown def nested_scope(): global __loader__ diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md b/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md index 87290120936c1..43c8f5f5644e4 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md @@ -16,7 +16,7 @@ class C: if flag: x = 2 -# error: [possibly-unbound-attribute] "Attribute `x` on type `` is possibly unbound" +# error: [possibly-missing-attribute] "Attribute `x` on type `` may be missing" reveal_type(C.x) # revealed: Unknown | Literal[2] reveal_type(C.y) # revealed: Unknown | Literal[1] ``` @@ -52,7 +52,7 @@ class C: elif coinflip(): x: str = "abc" -# error: [possibly-unbound-attribute] +# error: [possibly-missing-attribute] reveal_type(C.x) # revealed: int | str ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_unbound_`__\342\200\246_(42b1d61a2b7be1b5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_missing_`__\342\200\246_(33924dbae5117216).snap" similarity index 94% rename from "crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_unbound_`__\342\200\246_(42b1d61a2b7be1b5).snap" rename to "crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_missing_`__\342\200\246_(33924dbae5117216).snap" index c41a49507e8f3..ddc15768bce9c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_unbound_`__\342\200\246_(42b1d61a2b7be1b5).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_missing_`__\342\200\246_(33924dbae5117216).snap" @@ -3,7 +3,7 @@ source: crates/ty_test/src/lib.rs expression: snapshot --- --- -mdtest name: async_for.md - Async - Error cases - Possibly unbound `__aiter__` method +mdtest name: async_for.md - Async - Error cases - Possibly missing `__aiter__` method mdtest path: crates/ty_python_semantic/resources/mdtest/loops/async_for.md --- diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_unbound_`__\342\200\246_(74ad2f945cad6ed8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_missing_`__\342\200\246_(e2600ca4708d9e54).snap" similarity index 94% rename from "crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_unbound_`__\342\200\246_(74ad2f945cad6ed8).snap" rename to "crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_missing_`__\342\200\246_(e2600ca4708d9e54).snap" index 4060670bd9e26..f6e388811646a 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_unbound_`__\342\200\246_(74ad2f945cad6ed8).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_missing_`__\342\200\246_(e2600ca4708d9e54).snap" @@ -3,7 +3,7 @@ source: crates/ty_test/src/lib.rs expression: snapshot --- --- -mdtest name: async_for.md - Async - Error cases - Possibly unbound `__anext__` method +mdtest name: async_for.md - Async - Error cases - Possibly missing `__anext__` method mdtest path: crates/ty_python_semantic/resources/mdtest/loops/async_for.md --- diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Possibly-unbound_att\342\200\246_(e5bdf78c427cb7fc).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Possibly-missing_att\342\200\246_(e603e3da35f55c73).snap" similarity index 52% rename from "crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Possibly-unbound_att\342\200\246_(e5bdf78c427cb7fc).snap" rename to "crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Possibly-missing_att\342\200\246_(e603e3da35f55c73).snap" index 369a67a25667b..9335fec6da022 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Possibly-unbound_att\342\200\246_(e5bdf78c427cb7fc).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/attribute_assignment\342\200\246_-_Attribute_assignment_-_Possibly-missing_att\342\200\246_(e603e3da35f55c73).snap" @@ -3,7 +3,7 @@ source: crates/ty_test/src/lib.rs expression: snapshot --- --- -mdtest name: attribute_assignment.md - Attribute assignment - Possibly-unbound attributes +mdtest name: attribute_assignment.md - Attribute assignment - Possibly-missing attributes mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md --- @@ -17,37 +17,37 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_as 3 | if flag: 4 | attr: int = 0 5 | -6 | C.attr = 1 # error: [possibly-unbound-attribute] +6 | C.attr = 1 # error: [possibly-missing-attribute] 7 | 8 | instance = C() -9 | instance.attr = 1 # error: [possibly-unbound-attribute] +9 | instance.attr = 1 # error: [possibly-missing-attribute] ``` # Diagnostics ``` -warning[possibly-unbound-attribute]: Attribute `attr` on type `` is possibly unbound +warning[possibly-missing-attribute]: Attribute `attr` on type `` may be missing --> src/mdtest_snippet.py:6:5 | 4 | attr: int = 0 5 | -6 | C.attr = 1 # error: [possibly-unbound-attribute] +6 | C.attr = 1 # error: [possibly-missing-attribute] | ^^^^^^ 7 | 8 | instance = C() | -info: rule `possibly-unbound-attribute` is enabled by default +info: rule `possibly-missing-attribute` is enabled by default ``` ``` -warning[possibly-unbound-attribute]: Attribute `attr` on type `C` is possibly unbound +warning[possibly-missing-attribute]: Attribute `attr` on type `C` may be missing --> src/mdtest_snippet.py:9:5 | 8 | instance = C() -9 | instance.attr = 1 # error: [possibly-unbound-attribute] +9 | instance.attr = 1 # error: [possibly-missing-attribute] | ^^^^^^^^^^^^^ | -info: rule `possibly-unbound-attribute` is enabled by default +info: rule `possibly-missing-attribute` is enabled by default ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(b1ce0da35c06026).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_missing_`__\342\200\246_(77269542b8e81774).snap" similarity index 98% rename from "crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(b1ce0da35c06026).snap" rename to "crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_missing_`__\342\200\246_(77269542b8e81774).snap" index 9d09acef590b3..dde017803c718 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(b1ce0da35c06026).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_missing_`__\342\200\246_(77269542b8e81774).snap" @@ -3,7 +3,7 @@ source: crates/ty_test/src/lib.rs expression: snapshot --- --- -mdtest name: for.md - For loops - Possibly unbound `__iter__` and possibly invalid `__getitem__` +mdtest name: for.md - For loops - Possibly missing `__iter__` and possibly invalid `__getitem__` mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(3b75cc467e6e012).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_missing_`__\342\200\246_(9f781babda99d74b).snap" similarity index 96% rename from "crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(3b75cc467e6e012).snap" rename to "crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_missing_`__\342\200\246_(9f781babda99d74b).snap" index c7ec49090296d..b2bfc28b10b45 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(3b75cc467e6e012).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_missing_`__\342\200\246_(9f781babda99d74b).snap" @@ -3,7 +3,7 @@ source: crates/ty_test/src/lib.rs expression: snapshot --- --- -mdtest name: for.md - For loops - Possibly unbound `__iter__` and bad `__getitem__` method +mdtest name: for.md - For loops - Possibly missing `__iter__` and bad `__getitem__` method mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(8745233539d31200).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_missing_`__\342\200\246_(d8a02a0fcbb390a3).snap" similarity index 93% rename from "crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(8745233539d31200).snap" rename to "crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_missing_`__\342\200\246_(d8a02a0fcbb390a3).snap" index 48013f59483b0..41e7b6a425b0c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__\342\200\246_(8745233539d31200).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_missing_`__\342\200\246_(d8a02a0fcbb390a3).snap" @@ -3,7 +3,7 @@ source: crates/ty_test/src/lib.rs expression: snapshot --- --- -mdtest name: for.md - For loops - Possibly unbound `__iter__` and possibly unbound `__getitem__` +mdtest name: for.md - For loops - Possibly missing `__iter__` and possibly missing `__getitem__` mdtest path: crates/ty_python_semantic/resources/mdtest/loops/for.md --- diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_pos\342\200\246_(e3444b7a7f960d04).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_pos\342\200\246_(a028edbafe180ca).snap" similarity index 92% rename from "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_pos\342\200\246_(e3444b7a7f960d04).snap" rename to "crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_pos\342\200\246_(a028edbafe180ca).snap" index 22233a6acda59..ac576c9ad7634 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_pos\342\200\246_(e3444b7a7f960d04).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_pos\342\200\246_(a028edbafe180ca).snap" @@ -3,7 +3,7 @@ source: crates/ty_test/src/lib.rs expression: snapshot --- --- -mdtest name: invalid_await.md - Invalid await diagnostics - Custom type with possibly unbound `__await__` +mdtest name: invalid_await.md - Invalid await diagnostics - Custom type with possibly missing `__await__` mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md --- @@ -41,7 +41,7 @@ error[invalid-await]: `PossiblyUnbound` is not awaitable | --------------- method defined here 6 | yield | -info: `__await__` is possibly unbound +info: `__await__` may be missing info: rule `invalid-await` is enabled by default ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_non-keyword_re\342\200\246_(707b284610419a54).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_non-keyword_re\342\200\246_(707b284610419a54).snap" index 86e63f2a35a8a..929ad29c37d44 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_non-keyword_re\342\200\246_(707b284610419a54).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_non-keyword_re\342\200\246_(707b284610419a54).snap" @@ -70,7 +70,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.m 56 | # error: [invalid-argument-type] "Argument to function `f5` is incorrect: Expected `str`, found `Literal[3]`" 57 | # error: [no-matching-overload] "No overload of function `f6` matches arguments" 58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" -59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly missing `__call__` method)" 60 | x = f(3) ``` @@ -81,7 +81,7 @@ error[call-non-callable]: Object of type `Literal[5]` is not callable --> src/mdtest_snippet.py:60:9 | 58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" -59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly missing `__call__` method)" 60 | x = f(3) | ^^^^ | @@ -92,11 +92,11 @@ info: rule `call-non-callable` is enabled by default ``` ``` -error[call-non-callable]: Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method) +error[call-non-callable]: Object of type `PossiblyNotCallable` is not callable (possibly missing `__call__` method) --> src/mdtest_snippet.py:60:9 | 58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" -59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly missing `__call__` method)" 60 | x = f(3) | ^^^^ | @@ -111,7 +111,7 @@ error[missing-argument]: No argument provided for required parameter `b` of func --> src/mdtest_snippet.py:60:9 | 58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" -59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly missing `__call__` method)" 60 | x = f(3) | ^^^^ | @@ -126,7 +126,7 @@ error[no-matching-overload]: No overload of function `f6` matches arguments --> src/mdtest_snippet.py:60:9 | 58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" -59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly missing `__call__` method)" 60 | x = f(3) | ^^^^ | @@ -162,7 +162,7 @@ error[invalid-argument-type]: Argument to function `f2` is incorrect --> src/mdtest_snippet.py:60:11 | 58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" -59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly missing `__call__` method)" 60 | x = f(3) | ^ Expected `str`, found `Literal[3]` | @@ -186,7 +186,7 @@ error[invalid-argument-type]: Argument to function `f4` is incorrect --> src/mdtest_snippet.py:60:11 | 58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" -59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly missing `__call__` method)" 60 | x = f(3) | ^ Argument type `Literal[3]` does not satisfy upper bound `str` of type variable `T` | @@ -210,7 +210,7 @@ error[invalid-argument-type]: Argument to function `f5` is incorrect --> src/mdtest_snippet.py:60:11 | 58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" -59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly missing `__call__` method)" 60 | x = f(3) | ^ Expected `str`, found `Literal[3]` | @@ -237,7 +237,7 @@ error[too-many-positional-arguments]: Too many positional arguments to function --> src/mdtest_snippet.py:60:11 | 58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" -59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly missing `__call__` method)" 60 | x = f(3) | ^ | diff --git a/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md b/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md index 3416d124e3871..4b7e73498c666 100644 --- a/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md +++ b/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md @@ -1530,7 +1530,7 @@ if flag(): ``` ```py -# error: [possibly-unbound-import] +# error: [possibly-missing-import] from module import symbol ``` diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/instance.md b/crates/ty_python_semantic/resources/mdtest/subscript/instance.md index a7399d7152093..7d1ad7f183e58 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/instance.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/instance.md @@ -14,7 +14,11 @@ a = NotSubscriptable()[0] # error: "Cannot subscript object of type `NotSubscri class NotSubscriptable: __getitem__ = None -# error: "Method `__getitem__` of type `Unknown | None` is possibly not callable on object of type `NotSubscriptable`" +# TODO: this would be more user-friendly if the `call-non-callable` diagnostic was +# transformed into a `not-subscriptable` diagnostic with a subdiagnostic explaining +# that this was because `__getitem__` was possibly not callable +# +# error: [call-non-callable] "Method `__getitem__` of type `Unknown | None` may not be callable on object of type `NotSubscriptable`" a = NotSubscriptable()[0] ``` @@ -82,7 +86,7 @@ class NoSetitem: __setitem__ = None a = NoSetitem() -a[0] = 0 # error: "Method `__setitem__` of type `Unknown | None` is possibly not callable on object of type `NoSetitem`" +a[0] = 0 # error: "Method `__setitem__` of type `Unknown | None` may not be callable on object of type `NoSetitem`" ``` ## Valid `__setitem__` method diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index 57f52a037ccb0..71ed2b81f2347 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -1753,12 +1753,6 @@ static_assert(is_subtype_of(TypeOf[B], ReturnsWithArgument[int, B])) static_assert(is_subtype_of(TypeOf[B], Callable[[], B])) static_assert(is_subtype_of(TypeOf[B], Returns[B])) -class C: ... - -# TODO: These assertions should be true once we understand `Self` -static_assert(is_subtype_of(TypeOf[C], Callable[[], C])) # error: [static-assert-error] -static_assert(is_subtype_of(TypeOf[C], Returns[C])) # error: [static-assert-error] - class D[T]: def __init__(self, x: T) -> None: ... @@ -1875,6 +1869,21 @@ static_assert(not is_subtype_of(TypeOf[F], Callable[[int], F])) static_assert(not is_subtype_of(TypeOf[F], ReturnsWithArgument[int, F])) ``` +### Classes with no constructor methods + +```py +from typing import Callable, Protocol +from ty_extensions import TypeOf, static_assert, is_subtype_of + +class Returns[T](Protocol): + def __call__(self) -> T: ... + +class A: ... + +static_assert(is_subtype_of(TypeOf[A], Callable[[], A])) +static_assert(is_subtype_of(TypeOf[A], Returns[A])) +``` + ### Subclass of #### Type of a class with constructor methods diff --git a/crates/ty_python_semantic/resources/mdtest/unreachable.md b/crates/ty_python_semantic/resources/mdtest/unreachable.md index 4f0513176351f..7321ed9b0150c 100644 --- a/crates/ty_python_semantic/resources/mdtest/unreachable.md +++ b/crates/ty_python_semantic/resources/mdtest/unreachable.md @@ -198,7 +198,7 @@ import sys if sys.platform == "win32": # TODO: we should not emit an error here - # error: [possibly-unbound-attribute] + # error: [possibly-missing-attribute] sys.getwindowsversion() ``` diff --git a/crates/ty_python_semantic/resources/mdtest/with/async.md b/crates/ty_python_semantic/resources/mdtest/with/async.md index 2d907fa381ecc..0c55e5087d928 100644 --- a/crates/ty_python_semantic/resources/mdtest/with/async.md +++ b/crates/ty_python_semantic/resources/mdtest/with/async.md @@ -113,7 +113,7 @@ async def _(flag: bool): class NotAContextManager: ... context_expr = Manager1() if flag else NotAContextManager() - # error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `async with` because the methods `__aenter__` and `__aexit__` are possibly unbound" + # error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `async with` because the methods `__aenter__` and `__aexit__` are possibly missing" async with context_expr as f: reveal_type(f) # revealed: str ``` @@ -129,7 +129,7 @@ async def _(flag: bool): async def __exit__(self, *args): ... - # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because the method `__aenter__` is possibly unbound" + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because the method `__aenter__` may be missing" async with Manager() as f: reveal_type(f) # revealed: CoroutineType[Any, Any, str] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/with/sync.md b/crates/ty_python_semantic/resources/mdtest/with/sync.md index 1dd8d0ea70314..15d6aec51e408 100644 --- a/crates/ty_python_semantic/resources/mdtest/with/sync.md +++ b/crates/ty_python_semantic/resources/mdtest/with/sync.md @@ -113,7 +113,7 @@ def _(flag: bool): class NotAContextManager: ... context_expr = Manager1() if flag else NotAContextManager() - # error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly unbound" + # error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the methods `__enter__` and `__exit__` are possibly missing" with context_expr as f: reveal_type(f) # revealed: str ``` @@ -129,7 +129,7 @@ def _(flag: bool): def __exit__(self, *args): ... - # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` is possibly unbound" + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` may be missing" with Manager() as f: reveal_type(f) # revealed: str ``` diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index d140882877d4f..993075cd5da48 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -1339,12 +1339,15 @@ fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool { mod implicit_globals { use ruff_python_ast as ast; + use ruff_python_ast::name::Name; + use crate::Program; use crate::db::Db; - use crate::place::PlaceAndQualifiers; + use crate::place::{Boundness, PlaceAndQualifiers}; use crate::semantic_index::symbol::Symbol; use crate::semantic_index::{place_table, use_def_map}; - use crate::types::{KnownClass, Type}; + use crate::types::{CallableType, KnownClass, Parameter, Parameters, Signature, Type}; + use ruff_python_ast::PythonVersion; use super::{Place, place_from_declarations}; @@ -1392,28 +1395,58 @@ mod implicit_globals { db: &'db dyn Db, name: &str, ) -> PlaceAndQualifiers<'db> { - // We special-case `__file__` here because we know that for an internal implicit global - // lookup in a Python module, it is always a string, even though typeshed says `str | - // None`. - if name == "__file__" { - Place::bound(KnownClass::Str.to_instance(db)).into() - } else if name == "__builtins__" { - Place::bound(Type::any()).into() - } else if name == "__debug__" { - Place::bound(KnownClass::Bool.to_instance(db)).into() - } - // In general we wouldn't check to see whether a symbol exists on a class before doing the - // `.member()` call on the instance type -- we'd just do the `.member`() call on the instance - // type, since it has the same end result. The reason to only call `.member()` on `ModuleType` - // when absolutely necessary is that this function is used in a very hot path (name resolution - // in `infer.rs`). We use less idiomatic (and much more verbose) code here as a micro-optimisation. - else if module_type_symbols(db) - .iter() - .any(|module_type_member| &**module_type_member == name) - { - KnownClass::ModuleType.to_instance(db).member(db, name) - } else { - Place::Unbound.into() + match name { + // We special-case `__file__` here because we know that for an internal implicit global + // lookup in a Python module, it is always a string, even though typeshed says `str | + // None`. + "__file__" => Place::bound(KnownClass::Str.to_instance(db)).into(), + + "__builtins__" => Place::bound(Type::any()).into(), + + "__debug__" => Place::bound(KnownClass::Bool.to_instance(db)).into(), + + // Created lazily by the warnings machinery; may be absent. + // Model as possibly-unbound to avoid false negatives. + "__warningregistry__" => Place::Type( + KnownClass::Dict + .to_specialized_instance(db, [Type::any(), KnownClass::Int.to_instance(db)]), + Boundness::PossiblyUnbound, + ) + .into(), + + // Marked as possibly-unbound as it is only present in the module namespace + // if at least one global symbol is annotated in the module. + "__annotate__" if Program::get(db).python_version(db) >= PythonVersion::PY314 => { + let signature = Signature::new( + Parameters::new( + [Parameter::positional_only(Some(Name::new_static("format"))) + .with_annotated_type(KnownClass::Int.to_instance(db))], + ), + Some(KnownClass::Dict.to_specialized_instance( + db, + [KnownClass::Str.to_instance(db), Type::any()], + )), + ); + Place::Type( + CallableType::function_like(db, signature), + Boundness::PossiblyUnbound, + ) + .into() + } + + // In general we wouldn't check to see whether a symbol exists on a class before doing the + // `.member()` call on the instance type -- we'd just do the `.member`() call on the instance + // type, since it has the same end result. The reason to only call `.member()` on `ModuleType` + // when absolutely necessary is that this function is used in a very hot path (name resolution + // in `infer.rs`). We use less idiomatic (and much more verbose) code here as a micro-optimisation. + _ if module_type_symbols(db) + .iter() + .any(|module_type_member| &**module_type_member == name) => + { + KnownClass::ModuleType.to_instance(db).member(db, name) + } + + _ => Place::Unbound.into(), } } diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 847fcf258e79c..6a5e255ba0b13 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -8,7 +8,7 @@ use bitflags::bitflags; use call::{CallDunderError, CallError, CallErrorKind}; use context::InferContext; use diagnostic::{ - INVALID_CONTEXT_MANAGER, INVALID_SUPER_ARGUMENT, NOT_ITERABLE, POSSIBLY_UNBOUND_IMPLICIT_CALL, + INVALID_CONTEXT_MANAGER, INVALID_SUPER_ARGUMENT, NOT_ITERABLE, POSSIBLY_MISSING_IMPLICIT_CALL, UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS, }; use ruff_db::diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity}; @@ -28,7 +28,7 @@ pub(crate) use self::infer::{ infer_expression_types, infer_isolated_expression, infer_scope_types, static_expression_truthiness, }; -pub(crate) use self::signatures::{CallableSignature, Signature}; +pub(crate) use self::signatures::{CallableSignature, Parameter, Parameters, Signature}; pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; use crate::module_name::ModuleName; use crate::module_resolver::{KnownModule, resolve_module}; @@ -64,7 +64,7 @@ pub use crate::types::ide_support::{ use crate::types::infer::infer_unpack_types; use crate::types::mro::{Mro, MroError, MroIterator}; pub(crate) use crate::types::narrow::infer_narrowing_constraint; -use crate::types::signatures::{Parameter, ParameterForm, Parameters, walk_signature}; +use crate::types::signatures::{ParameterForm, walk_signature}; use crate::types::tuple::TupleSpec; pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type}; use crate::types::variance::{TypeVarVariance, VarianceInferable}; @@ -1593,9 +1593,16 @@ impl<'db> Type<'db> { }) } + (Type::TypeVar(_), _) if relation.is_assignability() => { + // The implicit lower bound of a typevar is `Never`, which means + // that it is always assignable to any other type. + + // TODO: record the unification constraints + + ConstraintSet::from(true) + } + // `Never` is the bottom type, the empty set. - // Other than one unlikely edge case (TypeVars bound to `Never`), - // no other type is a subtype of or assignable to `Never`. (_, Type::Never) => ConstraintSet::from(false), (Type::Union(union), _) => union.elements(db).iter().when_all(db, |&elem_ty| { @@ -1632,6 +1639,22 @@ impl<'db> Type<'db> { // be specialized to `Never`.) (_, Type::NonInferableTypeVar(_)) => ConstraintSet::from(false), + (_, Type::TypeVar(typevar)) + if relation.is_assignability() + && typevar.typevar(db).upper_bound(db).is_none_or(|bound| { + !self + .has_relation_to_impl(db, bound, relation, visitor) + .is_never_satisfied() + }) => + { + // TODO: record the unification constraints + + typevar + .typevar(db) + .upper_bound(db) + .when_none_or(|bound| self.has_relation_to_impl(db, bound, relation, visitor)) + } + // TODO: Infer specializations here (Type::TypeVar(_), _) | (_, Type::TypeVar(_)) => ConstraintSet::from(false), @@ -3807,7 +3830,7 @@ impl<'db> Type<'db> { }); } - // Don't trust possibly unbound `__bool__` method. + // Don't trust possibly missing `__bool__` method. Ok(Truthiness::Ambiguous) } @@ -5662,13 +5685,25 @@ impl<'db> Type<'db> { ], }); }; - let instance = Type::instance(db, class.unknown_specialization(db)); + + let upper_bound = Type::instance( + db, + class.apply_specialization(db, |generic_context| { + let types = generic_context + .variables(db) + .iter() + .map(|typevar| Type::NonInferableTypeVar(*typevar)); + + generic_context.specialize(db, types.collect()) + }), + ); + let class_definition = class.definition(db); let typevar = TypeVarInstance::new( db, ast::name::Name::new_static("Self"), Some(class_definition), - Some(TypeVarBoundOrConstraints::UpperBound(instance).into()), + Some(TypeVarBoundOrConstraints::UpperBound(upper_bound).into()), // According to the [spec], we can consider `Self` // equivalent to an invariant type variable // [spec]: https://typing.python.org/en/latest/spec/generics.html#self @@ -6010,8 +6045,8 @@ impl<'db> Type<'db> { partial.get(db, bound_typevar).unwrap_or(self) } TypeMapping::MarkTypeVarsInferable(binding_context) => { - if bound_typevar.binding_context(db) == *binding_context { - Type::TypeVar(bound_typevar) + if binding_context.is_none_or(|context| context == bound_typevar.binding_context(db)) { + Type::TypeVar(bound_typevar.mark_typevars_inferable(db, visitor)) } else { self } @@ -6695,8 +6730,17 @@ pub enum TypeMapping<'a, 'db> { BindSelf(Type<'db>), /// Replaces occurrences of `typing.Self` with a new `Self` type variable with the given upper bound. ReplaceSelf { new_upper_bound: Type<'db> }, - /// Marks the typevars that are bound by a generic class or function as inferable. - MarkTypeVarsInferable(BindingContext<'db>), + /// Marks type variables as inferable. + /// + /// When we create the signature for a generic function, we mark its type variables as inferable. Since + /// the generic function might reference type variables from enclosing generic scopes, we include the + /// function's binding context in order to only mark those type variables as inferable that are actually + /// bound by that function. + /// + /// When the parameter is set to `None`, *all* type variables will be marked as inferable. We use this + /// variant when descending into the bounds and/or constraints, and the default value of a type variable, + /// which may include nested type variables (`Self` has a bound of `C[T]` for a generic class `C[T]`). + MarkTypeVarsInferable(Option>), /// Create the top or bottom materialization of a type. Materialize(MaterializationKind), } @@ -7637,6 +7681,43 @@ impl<'db> TypeVarInstance<'db> { ) } + fn mark_typevars_inferable( + self, + db: &'db dyn Db, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Self { + // Type variables can have nested type variables in their bounds, constraints, or default value. + // When we mark a type variable as inferable, we also mark all of these nested type variables as + // inferable, so we set the parameter to `None` here. + let type_mapping = &TypeMapping::MarkTypeVarsInferable(None); + + Self::new( + db, + self.name(db), + self.definition(db), + self._bound_or_constraints(db) + .map(|bound_or_constraints| match bound_or_constraints { + TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => { + bound_or_constraints + .mark_typevars_inferable(db, visitor) + .into() + } + TypeVarBoundOrConstraintsEvaluation::LazyUpperBound + | TypeVarBoundOrConstraintsEvaluation::LazyConstraints => bound_or_constraints, + }), + self.explicit_variance(db), + self._default(db).and_then(|default| match default { + TypeVarDefaultEvaluation::Eager(ty) => { + Some(ty.apply_type_mapping_impl(db, type_mapping, visitor).into()) + } + TypeVarDefaultEvaluation::Lazy => self + .lazy_default(db) + .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor).into()), + }), + self.kind(db), + ) + } + fn to_instance(self, db: &'db dyn Db) -> Option { let bound_or_constraints = match self.bound_or_constraints(db)? { TypeVarBoundOrConstraints::UpperBound(upper_bound) => { @@ -7867,6 +7948,18 @@ impl<'db> BoundTypeVarInstance<'db> { ) } + fn mark_typevars_inferable( + self, + db: &'db dyn Db, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Self { + Self::new( + db, + self.typevar(db).mark_typevars_inferable(db, visitor), + self.binding_context(db), + ) + } + fn to_instance(self, db: &'db dyn Db) -> Option { Some(Self::new( db, @@ -7972,6 +8065,31 @@ impl<'db> TypeVarBoundOrConstraints<'db> { } } } + + fn mark_typevars_inferable( + self, + db: &'db dyn Db, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Self { + let type_mapping = &TypeMapping::MarkTypeVarsInferable(None); + + match self { + TypeVarBoundOrConstraints::UpperBound(bound) => TypeVarBoundOrConstraints::UpperBound( + bound.apply_type_mapping_impl(db, type_mapping, visitor), + ), + TypeVarBoundOrConstraints::Constraints(constraints) => { + TypeVarBoundOrConstraints::Constraints(UnionType::new( + db, + constraints + .elements(db) + .iter() + .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor)) + .collect::>() + .into_boxed_slice(), + )) + } + } + } } /// Error returned if a type is not awaitable. @@ -8030,7 +8148,7 @@ impl<'db> AwaitError<'db> { } } Self::Call(CallDunderError::PossiblyUnbound(bindings)) => { - diag.info("`__await__` is possibly unbound"); + diag.info("`__await__` may be missing"); if let Some(definition_spans) = bindings.callable_type().function_spans(db) { diag.annotate( Annotation::secondary(definition_spans.signature) @@ -8137,7 +8255,7 @@ impl<'db> ContextManagerError<'db> { match call_dunder_error { CallDunderError::MethodNotAvailable => format!("it does not implement `{name}`"), CallDunderError::PossiblyUnbound(_) => { - format!("the method `{name}` is possibly unbound") + format!("the method `{name}` may be missing") } // TODO: Use more specific error messages for the different error cases. // E.g. hint toward the union variant that doesn't correctly implement enter, @@ -8154,7 +8272,7 @@ impl<'db> ContextManagerError<'db> { name_b: &str| { match (error_a, error_b) { (CallDunderError::PossiblyUnbound(_), CallDunderError::PossiblyUnbound(_)) => { - format!("the methods `{name_a}` and `{name_b}` are possibly unbound") + format!("the methods `{name_a}` and `{name_b}` are possibly missing") } (CallDunderError::MethodNotAvailable, CallDunderError::MethodNotAvailable) => { format!("it does not implement `{name_a}` and `{name_b}`") @@ -8703,7 +8821,7 @@ pub(super) enum BoolError<'db> { /// Any other reason why the type can't be converted to a bool. /// E.g. because calling `__bool__` returns in a union type and not all variants support `__bool__` or - /// because `__bool__` points to a type that has a possibly unbound `__call__` method. + /// because `__bool__` points to a type that has a possibly missing `__call__` method. Other { not_boolable_type: Type<'db> }, } @@ -8876,7 +8994,7 @@ impl<'db> ConstructorCallError<'db> { let report_init_error = |call_dunder_error: &CallDunderError<'db>| match call_dunder_error { CallDunderError::MethodNotAvailable => { if let Some(builder) = - context.report_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL, context_expression_node) + context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, context_expression_node) { // If we are using vendored typeshed, it should be impossible to have missing // or unbound `__init__` method on a class, as all classes have `object` in MRO. @@ -8890,10 +9008,10 @@ impl<'db> ConstructorCallError<'db> { } CallDunderError::PossiblyUnbound(bindings) => { if let Some(builder) = - context.report_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL, context_expression_node) + context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, context_expression_node) { builder.into_diagnostic(format_args!( - "Method `__init__` on type `{}` is possibly unbound.", + "Method `__init__` on type `{}` may be missing.", context_expression_type.display(context.db()), )); } @@ -8908,10 +9026,10 @@ impl<'db> ConstructorCallError<'db> { let report_new_error = |error: &DunderNewCallError<'db>| match error { DunderNewCallError::PossiblyUnbound(call_error) => { if let Some(builder) = - context.report_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL, context_expression_node) + context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, context_expression_node) { builder.into_diagnostic(format_args!( - "Method `__new__` on type `{}` is possibly unbound.", + "Method `__new__` on type `{}` may be missing.", context_expression_type.display(context.db()), )); } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index e19e1c6c04030..f5b2e4e349306 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -19,8 +19,8 @@ use crate::place::{Boundness, Place}; use crate::types::call::arguments::{Expansion, is_expandable_type}; use crate::types::diagnostic::{ CALL_NON_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, - NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS, - UNKNOWN_ARGUMENT, + NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, POSITIONAL_ONLY_PARAMETER_AS_KWARG, + TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, }; use crate::types::enums::is_enum_class; use crate::types::function::{ @@ -1758,7 +1758,7 @@ impl<'db> CallableBinding<'db> { if self.dunder_call_is_possibly_unbound { if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) { let mut diag = builder.into_diagnostic(format_args!( - "Object of type `{}` is not callable (possibly unbound `__call__` method)", + "Object of type `{}` is not callable (possibly missing `__call__` method)", self.callable_type.display(context.db()), )); if let Some(union_diag) = union_diag { @@ -1991,6 +1991,7 @@ struct ArgumentMatcher<'a, 'db> { argument_matches: Vec>, parameter_matched: Vec, + suppress_missing_error: Vec, next_positional: usize, first_excess_positional: Option, num_synthetic_args: usize, @@ -2009,6 +2010,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { errors, argument_matches: vec![MatchedArgument::default(); arguments.len()], parameter_matched: vec![false; parameters.len()], + suppress_missing_error: vec![false; parameters.len()], next_positional: 0, first_excess_positional: None, num_synthetic_args: 0, @@ -2105,10 +2107,21 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { .keyword_by_name(name) .or_else(|| self.parameters.keyword_variadic()) else { - self.errors.push(BindingError::UnknownArgument { - argument_name: ast::name::Name::new(name), - argument_index: self.get_argument_index(argument_index), - }); + if let Some((parameter_index, parameter)) = + self.parameters.positional_only_by_name(name) + { + self.errors + .push(BindingError::PositionalOnlyParameterAsKwarg { + argument_index: self.get_argument_index(argument_index), + parameter: ParameterContext::new(parameter, parameter_index, true), + }); + self.suppress_missing_error[parameter_index] = true; + } else { + self.errors.push(BindingError::UnknownArgument { + argument_name: ast::name::Name::new(name), + argument_index: self.get_argument_index(argument_index), + }); + } return Err(()); }; self.assign_argument( @@ -2223,6 +2236,9 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { let mut missing = vec![]; for (index, matched) in self.parameter_matched.iter().copied().enumerate() { if !matched { + if self.suppress_missing_error[index] { + continue; + } let param = &self.parameters[index]; if param.is_variadic() || param.is_keyword_variadic() @@ -3094,6 +3110,11 @@ pub(crate) enum BindingError<'db> { argument_name: ast::name::Name, argument_index: Option, }, + /// A positional-only parameter is passed as keyword argument. + PositionalOnlyParameterAsKwarg { + argument_index: Option, + parameter: ParameterContext, + }, /// More positional arguments are provided in the call than can be handled by the signature. TooManyPositionalArguments { first_excess_argument_index: Option, @@ -3349,6 +3370,35 @@ impl<'db> BindingError<'db> { } } + Self::PositionalOnlyParameterAsKwarg { + argument_index, + parameter, + } => { + let node = Self::get_node(node, *argument_index); + if let Some(builder) = + context.report_lint(&POSITIONAL_ONLY_PARAMETER_AS_KWARG, node) + { + let mut diag = builder.into_diagnostic(format_args!( + "Positional-only parameter {parameter} passed as keyword argument{}", + if let Some(CallableDescription { kind, name }) = callable_description { + format!(" of {kind} `{name}`") + } else { + String::new() + } + )); + if let Some(union_diag) = union_diag { + union_diag.add_union_context(context.db(), &mut diag); + } else if let Some(spans) = callable_ty.function_spans(context.db()) { + let mut sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!("{callable_kind} signature here"), + ); + sub.annotate(Annotation::primary(spans.signature)); + diag.sub(sub); + } + } + } + Self::ParameterAlreadyAssigned { argument_index, parameter, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 9a45eb6b7d76c..b53d2c7fd4588 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1185,7 +1185,7 @@ impl<'db> ClassType<'db> { if let Place::Type(Type::FunctionLiteral(new_function), _) = new_function_symbol { Type::Callable( new_function - .into_bound_method_type(db, self_ty) + .into_bound_method_type(db, correct_return_type) .into_callable_type(db), ) } else { @@ -3664,6 +3664,7 @@ pub enum KnownClass { Auto, Member, Nonmember, + StrEnum, // abc ABCMeta, // Types @@ -3804,6 +3805,7 @@ impl KnownClass { | Self::Auto | Self::Member | Self::Nonmember + | Self::StrEnum | Self::ABCMeta | Self::Iterable | Self::Iterator @@ -3864,6 +3866,7 @@ impl KnownClass { | KnownClass::Auto | KnownClass::Member | KnownClass::Nonmember + | KnownClass::StrEnum | KnownClass::ABCMeta | KnownClass::GenericAlias | KnownClass::ModuleType @@ -3944,6 +3947,7 @@ impl KnownClass { | KnownClass::Auto | KnownClass::Member | KnownClass::Nonmember + | KnownClass::StrEnum | KnownClass::ABCMeta | KnownClass::GenericAlias | KnownClass::ModuleType @@ -4024,6 +4028,7 @@ impl KnownClass { | KnownClass::Auto | KnownClass::Member | KnownClass::Nonmember + | KnownClass::StrEnum | KnownClass::ABCMeta | KnownClass::GenericAlias | KnownClass::ModuleType @@ -4142,6 +4147,7 @@ impl KnownClass { | Self::Auto | Self::Member | Self::Nonmember + | Self::StrEnum | Self::ABCMeta | Self::Super | Self::StdlibAlias @@ -4226,6 +4232,7 @@ impl KnownClass { Self::Auto => "auto", Self::Member => "member", Self::Nonmember => "nonmember", + Self::StrEnum => "StrEnum", Self::ABCMeta => "ABCMeta", Self::Super => "super", Self::Iterable => "Iterable", @@ -4462,9 +4469,12 @@ impl KnownClass { | Self::Property => KnownModule::Builtins, Self::VersionInfo => KnownModule::Sys, Self::ABCMeta => KnownModule::Abc, - Self::Enum | Self::EnumType | Self::Auto | Self::Member | Self::Nonmember => { - KnownModule::Enum - } + Self::Enum + | Self::EnumType + | Self::Auto + | Self::Member + | Self::Nonmember + | Self::StrEnum => KnownModule::Enum, Self::GenericAlias | Self::ModuleType | Self::FunctionType @@ -4591,6 +4601,7 @@ impl KnownClass { | Self::Auto | Self::Member | Self::Nonmember + | Self::StrEnum | Self::ABCMeta | Self::Super | Self::NewType @@ -4675,6 +4686,7 @@ impl KnownClass { | Self::Auto | Self::Member | Self::Nonmember + | Self::StrEnum | Self::ABCMeta | Self::Super | Self::UnionType @@ -4762,6 +4774,9 @@ impl KnownClass { "EnumType" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { Self::EnumType } + "StrEnum" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { + Self::StrEnum + } "auto" => Self::Auto, "member" => Self::Member, "nonmember" => Self::Nonmember, @@ -4835,6 +4850,7 @@ impl KnownClass { | Self::Auto | Self::Member | Self::Nonmember + | Self::StrEnum | Self::ABCMeta | Self::Super | Self::NotImplementedType @@ -5387,7 +5403,9 @@ mod tests { } KnownClass::GenericAlias => PythonVersion::PY39, KnownClass::KwOnly => PythonVersion::PY310, - KnownClass::Member | KnownClass::Nonmember => PythonVersion::PY311, + KnownClass::Member | KnownClass::Nonmember | KnownClass::StrEnum => { + PythonVersion::PY311 + } _ => PythonVersion::PY37, }; (class, version_added) diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 77f01da1fe141..214eb30749c0a 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -38,7 +38,7 @@ use std::fmt::Formatter; pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&AMBIGUOUS_PROTOCOL_MEMBER); registry.register_lint(&CALL_NON_CALLABLE); - registry.register_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL); + registry.register_lint(&POSSIBLY_MISSING_IMPLICIT_CALL); registry.register_lint(&CONFLICTING_ARGUMENT_FORMS); registry.register_lint(&CONFLICTING_DECLARATIONS); registry.register_lint(&CONFLICTING_METACLASS); @@ -80,8 +80,8 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&NOT_ITERABLE); registry.register_lint(&UNSUPPORTED_BOOL_CONVERSION); registry.register_lint(&PARAMETER_ALREADY_ASSIGNED); - registry.register_lint(&POSSIBLY_UNBOUND_ATTRIBUTE); - registry.register_lint(&POSSIBLY_UNBOUND_IMPORT); + registry.register_lint(&POSSIBLY_MISSING_ATTRIBUTE); + registry.register_lint(&POSSIBLY_MISSING_IMPORT); registry.register_lint(&POSSIBLY_UNRESOLVED_REFERENCE); registry.register_lint(&SUBCLASS_OF_FINAL_CLASS); registry.register_lint(&TYPE_ASSERTION_FAILURE); @@ -89,6 +89,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS); registry.register_lint(&UNDEFINED_REVEAL); registry.register_lint(&UNKNOWN_ARGUMENT); + registry.register_lint(&POSITIONAL_ONLY_PARAMETER_AS_KWARG); registry.register_lint(&UNRESOLVED_ATTRIBUTE); registry.register_lint(&UNRESOLVED_IMPORT); registry.register_lint(&UNRESOLVED_REFERENCE); @@ -130,12 +131,12 @@ declare_lint! { declare_lint! { /// ## What it does - /// Checks for implicit calls to possibly unbound methods. + /// Checks for implicit calls to possibly missing methods. /// /// ## Why is this bad? /// Expressions such as `x[y]` and `x * y` call methods /// under the hood (`__getitem__` and `__mul__` respectively). - /// Calling an unbound method will raise an `AttributeError` at runtime. + /// Calling a missing method will raise an `AttributeError` at runtime. /// /// ## Examples /// ```python @@ -147,8 +148,8 @@ declare_lint! { /// /// A()[0] # TypeError: 'A' object is not subscriptable /// ``` - pub(crate) static POSSIBLY_UNBOUND_IMPLICIT_CALL = { - summary: "detects implicit calls to possibly unbound methods", + pub(crate) static POSSIBLY_MISSING_IMPLICIT_CALL = { + summary: "detects implicit calls to possibly missing methods", status: LintStatus::preview("1.0.0"), default_level: Level::Warn, } @@ -1326,10 +1327,10 @@ declare_lint! { declare_lint! { /// ## What it does - /// Checks for possibly unbound attributes. + /// Checks for possibly missing attributes. /// /// ## Why is this bad? - /// Attempting to access an unbound attribute will raise an `AttributeError` at runtime. + /// Attempting to access a missing attribute will raise an `AttributeError` at runtime. /// /// ## Examples /// ```python @@ -1339,8 +1340,8 @@ declare_lint! { /// /// A.c # AttributeError: type object 'A' has no attribute 'c' /// ``` - pub(crate) static POSSIBLY_UNBOUND_ATTRIBUTE = { - summary: "detects references to possibly unbound attributes", + pub(crate) static POSSIBLY_MISSING_ATTRIBUTE = { + summary: "detects references to possibly missing attributes", status: LintStatus::preview("1.0.0"), default_level: Level::Warn, } @@ -1348,10 +1349,10 @@ declare_lint! { declare_lint! { /// ## What it does - /// Checks for imports of symbols that may be unbound. + /// Checks for imports of symbols that may be missing. /// /// ## Why is this bad? - /// Importing an unbound module or name will raise a `ModuleNotFoundError` + /// Importing a missing module or name will raise a `ModuleNotFoundError` /// or `ImportError` at runtime. /// /// ## Examples @@ -1365,8 +1366,8 @@ declare_lint! { /// # main.py /// from module import a # ImportError: cannot import name 'a' from 'module' /// ``` - pub(crate) static POSSIBLY_UNBOUND_IMPORT = { - summary: "detects possibly unbound imports", + pub(crate) static POSSIBLY_MISSING_IMPORT = { + summary: "detects possibly missing imports", status: LintStatus::preview("1.0.0"), default_level: Level::Warn, } @@ -1538,6 +1539,27 @@ declare_lint! { } } +declare_lint! { + /// ## What it does + /// Checks for keyword arguments in calls that match positional-only parameters of the callable. + /// + /// ## Why is this bad? + /// Providing a positional-only parameter as a keyword argument will raise `TypeError` at runtime. + /// + /// ## Example + /// + /// ```python + /// def f(x: int, /) -> int: ... + /// + /// f(x=1) # Error raised here + /// ``` + pub(crate) static POSITIONAL_ONLY_PARAMETER_AS_KWARG = { + summary: "detects positional-only parameters passed as keyword arguments", + status: LintStatus::preview("1.0.0"), + default_level: Level::Error, + } +} + declare_lint! { /// ## What it does /// Checks for unresolved attributes. @@ -2175,17 +2197,17 @@ pub(super) fn report_possibly_unresolved_reference( builder.into_diagnostic(format_args!("Name `{id}` used when possibly not defined")); } -pub(super) fn report_possibly_unbound_attribute( +pub(super) fn report_possibly_missing_attribute( context: &InferContext, target: &ast::ExprAttribute, attribute: &str, object_ty: Type, ) { - let Some(builder) = context.report_lint(&POSSIBLY_UNBOUND_ATTRIBUTE, target) else { + let Some(builder) = context.report_lint(&POSSIBLY_MISSING_ATTRIBUTE, target) else { return; }; builder.into_diagnostic(format_args!( - "Attribute `{attribute}` on type `{}` is possibly unbound", + "Attribute `{attribute}` on type `{}` may be missing", object_ty.display(context.db()), )); } @@ -2771,7 +2793,7 @@ pub(crate) fn report_invalid_or_unsupported_base( CallDunderError::PossiblyUnbound(_) => { explain_mro_entries(&mut diagnostic); diagnostic.info(format_args!( - "Type `{}` has an `__mro_entries__` attribute, but it is possibly unbound", + "Type `{}` may have an `__mro_entries__` attribute, but it may be missing", base_type.display(db) )); } diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index 828ed580b8652..47000ce7ea114 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -6,8 +6,8 @@ use crate::{ place::{Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations}, semantic_index::{place_table, use_def_map}, types::{ - ClassLiteral, DynamicType, EnumLiteralType, KnownClass, MemberLookupPolicy, Type, - TypeQualifiers, + ClassLiteral, DynamicType, EnumLiteralType, KnownClass, MemberLookupPolicy, + StringLiteralType, Type, TypeQualifiers, }, }; @@ -77,12 +77,14 @@ pub(crate) fn enum_metadata<'db>( return None; } + let is_str_enum = + Type::ClassLiteral(class).is_subtype_of(db, KnownClass::StrEnum.to_subclass_of(db)); + let scope_id = class.body_scope(db); let use_def_map = use_def_map(db, scope_id); let table = place_table(db, scope_id); let mut enum_values: FxHashMap, Name> = FxHashMap::default(); - // TODO: handle `StrEnum` which uses lowercase names as values when using `auto()`. let mut auto_counter = 0; let ignored_names: Option> = if let Some(ignore) = table.symbol_id("_ignore_") { @@ -148,7 +150,14 @@ pub(crate) fn enum_metadata<'db>( // enum.auto Some(KnownClass::Auto) => { auto_counter += 1; - Some(Type::IntLiteral(auto_counter)) + Some(if is_str_enum { + Type::StringLiteral(StringLiteralType::new( + db, + name.to_lowercase().as_str(), + )) + } else { + Type::IntLiteral(auto_counter) + }) } _ => None, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 1677a7ea6feb0..7650a4eac3e38 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -491,6 +491,18 @@ fn is_subtype_in_invariant_position<'db>( let base_bottom = base_type.bottom_materialization(db); let is_subtype_of = |derived: Type<'db>, base: Type<'db>| { + // TODO: + // This should be removed and properly handled in the respective + // `(Type::TypeVar(_), _) | (_, Type::TypeVar(_))` branch of + // `Type::has_relation_to_impl`. Right now, we can not generally + // return `ConstraintSet::from(true)` from that branch, as that + // leads to union simplification, which means that we lose track + // of type variables without recording the constraints under which + // the relation holds. + if matches!(base, Type::TypeVar(_)) || matches!(derived, Type::TypeVar(_)) { + return ConstraintSet::from(true); + } + derived.has_relation_to_impl(db, base, TypeRelation::Subtyping, visitor) }; match (derived_materialization, base_materialization) { diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index db04778cf8398..a276063415a2a 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -52,7 +52,7 @@ use crate::types::diagnostic::{ INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_NAMED_TUPLE, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, - IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT, + IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict, report_implicit_return_type, @@ -60,7 +60,7 @@ use crate::types::diagnostic::{ report_invalid_attribute_assignment, report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, report_invalid_return_type, report_namedtuple_field_without_default_after_field_with_default, - report_possibly_unbound_attribute, + report_possibly_missing_attribute, }; use crate::types::diagnostic::{ INVALID_METACLASS, INVALID_OVERLOAD, INVALID_PROTOCOL, SUBCLASS_OF_FINAL_CLASS, @@ -3222,10 +3222,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Err(err) => match err { CallDunderError::PossiblyUnbound { .. } => { if let Some(builder) = - context.report_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL, &**value) + context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, &**value) { builder.into_diagnostic(format_args!( - "Method `__setitem__` of type `{}` is possibly unbound", + "Method `__setitem__` of type `{}` may be missing", value_ty.display(db), )); } @@ -3306,7 +3306,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, &**value) { builder.into_diagnostic(format_args!( - "Method `__setitem__` of type `{}` is possibly not \ + "Method `__setitem__` of type `{}` may not be \ callable on object of type `{}`", bindings.callable_type().display(db), value_ty.display(db), @@ -3642,7 +3642,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; if boundness == Boundness::PossiblyUnbound { - report_possibly_unbound_attribute( + report_possibly_missing_attribute( &self.context, target, attribute, @@ -3672,7 +3672,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } if instance_attr_boundness == Boundness::PossiblyUnbound { - report_possibly_unbound_attribute( + report_possibly_missing_attribute( &self.context, target, attribute, @@ -3752,7 +3752,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; if boundness == Boundness::PossiblyUnbound { - report_possibly_unbound_attribute( + report_possibly_missing_attribute( &self.context, target, attribute, @@ -3783,7 +3783,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } if class_attr_boundness == Boundness::PossiblyUnbound { - report_possibly_unbound_attribute( + report_possibly_missing_attribute( &self.context, target, attribute, @@ -4680,10 +4680,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // together if the attribute exists but is possibly-unbound. if let Some(builder) = self .context - .report_lint(&POSSIBLY_UNBOUND_IMPORT, AnyNodeRef::Alias(alias)) + .report_lint(&POSSIBLY_MISSING_IMPORT, AnyNodeRef::Alias(alias)) { builder.into_diagnostic(format_args!( - "Member `{name}` of module `{module_name}` is possibly unbound", + "Member `{name}` of module `{module_name}` may be missing", )); } } @@ -6804,7 +6804,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::unknown().into() } LookupError::PossiblyUnbound(type_when_bound) => { - report_possibly_unbound_attribute( + report_possibly_missing_attribute( &self.context, attribute, &attr.id, @@ -8757,10 +8757,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } Err(err @ CallDunderError::PossiblyUnbound { .. }) => { if let Some(builder) = - context.report_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL, value_node) + context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, value_node) { builder.into_diagnostic(format_args!( - "Method `__getitem__` of type `{}` is possibly unbound", + "Method `__getitem__` of type `{}` may be missing", value_ty.display(db), )); } @@ -8808,7 +8808,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { CallErrorKind::PossiblyNotCallable => { if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, value_node) { builder.into_diagnostic(format_args!( - "Method `__getitem__` of type `{}` is possibly not callable on object of type `{}`", + "Method `__getitem__` of type `{}` may not be callable on object of type `{}`", bindings.callable_type().display(db), value_ty.display(db), )); @@ -8840,11 +8840,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Place::Type(ty, boundness) => { if boundness == Boundness::PossiblyUnbound { if let Some(builder) = - context.report_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL, value_node) + context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, value_node) { builder.into_diagnostic(format_args!( - "Method `__class_getitem__` of type `{}` \ - is possibly unbound", + "Method `__class_getitem__` of type `{}` may be missing", value_ty.display(db), )); } diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 35da85a09f2bd..ddc06951fc8b3 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -367,7 +367,9 @@ impl<'db> Signature<'db> { let plain_return_ty = definition_expression_type(db, definition, returns.as_ref()) .apply_type_mapping( db, - &TypeMapping::MarkTypeVarsInferable(BindingContext::Definition(definition)), + &TypeMapping::MarkTypeVarsInferable(Some(BindingContext::Definition( + definition, + ))), ); if function_node.is_async && !is_generator { KnownClass::CoroutineType @@ -1334,6 +1336,17 @@ impl<'db> Parameters<'db> { .and_then(|parameter| parameter.is_positional().then_some(parameter)) } + /// Return a positional-only parameter (with index) with the given name. + pub(crate) fn positional_only_by_name(&self, name: &str) -> Option<(usize, &Parameter<'db>)> { + self.iter().enumerate().find(|(_, parameter)| { + parameter.is_positional_only() + && parameter + .name() + .map(|p_name| p_name == name) + .unwrap_or(false) + }) + } + /// Return the variadic parameter (`*args`), if any, and its index, or `None`. pub(crate) fn variadic(&self) -> Option<(usize, &Parameter<'db>)> { self.iter() @@ -1549,7 +1562,9 @@ impl<'db> Parameter<'db> { annotated_type: parameter.annotation().map(|annotation| { definition_expression_type(db, definition, annotation).apply_type_mapping( db, - &TypeMapping::MarkTypeVarsInferable(BindingContext::Definition(definition)), + &TypeMapping::MarkTypeVarsInferable(Some(BindingContext::Definition( + definition, + ))), ) }), kind, diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap index cc435c14c987d..0f51c3aa5b1e1 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap @@ -77,9 +77,10 @@ Settings: Settings { "non-subscriptable": Error (Default), "not-iterable": Error (Default), "parameter-already-assigned": Error (Default), - "possibly-unbound-attribute": Warning (Default), - "possibly-unbound-implicit-call": Warning (Default), - "possibly-unbound-import": Warning (Default), + "positional-only-parameter-as-kwarg": Error (Default), + "possibly-missing-attribute": Warning (Default), + "possibly-missing-implicit-call": Warning (Default), + "possibly-missing-import": Warning (Default), "raw-string-type-annotation": Error (Default), "redundant-cast": Warning (Default), "static-assert-error": Error (Default), diff --git a/crates/ty_static/src/env_vars.rs b/crates/ty_static/src/env_vars.rs index ebad90e8e5eb5..bd7c05a3c0651 100644 --- a/crates/ty_static/src/env_vars.rs +++ b/crates/ty_static/src/env_vars.rs @@ -42,6 +42,12 @@ impl EnvVars { /// Used to detect an activated virtual environment. pub const VIRTUAL_ENV: &'static str = "VIRTUAL_ENV"; + /// Adds additional directories to ty's search paths. + /// The format is the same as the shell’s PATH: + /// one or more directory pathnames separated by os appropriate pathsep + /// (e.g. colons on Unix or semicolons on Windows). + pub const PYTHONPATH: &'static str = "PYTHONPATH"; + /// Used to determine if an active Conda environment is the base environment or not. pub const CONDA_DEFAULT_ENV: &'static str = "CONDA_DEFAULT_ENV"; diff --git a/playground/ruff/src/Editor/SourceEditor.tsx b/playground/ruff/src/Editor/SourceEditor.tsx index a131a4e1533f0..eb39a8c063b54 100644 --- a/playground/ruff/src/Editor/SourceEditor.tsx +++ b/playground/ruff/src/Editor/SourceEditor.tsx @@ -124,7 +124,18 @@ class RuffCodeActionProvider implements CodeActionProvider { range: Range, ): languages.ProviderResult { const actions = this.diagnostics - .filter((check) => range.startLineNumber === check.start_location.row) + // Show fixes for any diagnostic whose range intersects the requested range + .filter((check) => + Range.areIntersecting( + new Range( + check.start_location.row, + check.start_location.column, + check.end_location.row, + check.end_location.column, + ), + range, + ), + ) .filter(({ fix }) => fix) .map((check) => ({ title: check.fix @@ -173,6 +184,7 @@ function updateMarkers(monaco: Monaco, diagnostics: Array) { model, "owner", diagnostics.map((diagnostic) => ({ + code: diagnostic.code ?? undefined, startLineNumber: diagnostic.start_location.row, startColumn: diagnostic.start_location.column, endLineNumber: diagnostic.end_location.row, diff --git a/ruff.schema.json b/ruff.schema.json index 954d88ab4aef7..3b1e4da9d3c28 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3020,6 +3020,8 @@ "ASYNC222", "ASYNC23", "ASYNC230", + "ASYNC24", + "ASYNC240", "ASYNC25", "ASYNC250", "ASYNC251", diff --git a/ty.schema.json b/ty.schema.json index feaa039d431b4..f7b2b1095dd3d 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -785,9 +785,19 @@ } ] }, - "possibly-unbound-attribute": { - "title": "detects references to possibly unbound attributes", - "description": "## What it does\nChecks for possibly unbound attributes.\n\n## Why is this bad?\nAttempting to access an unbound attribute will raise an `AttributeError` at runtime.\n\n## Examples\n```python\nclass A:\n if b:\n c = 0\n\nA.c # AttributeError: type object 'A' has no attribute 'c'\n```", + "positional-only-parameter-as-kwarg": { + "title": "detects positional-only parameters passed as keyword arguments", + "description": "## What it does\nChecks for keyword arguments in calls that match positional-only parameters of the callable.\n\n## Why is this bad?\nProviding a positional-only parameter as a keyword argument will raise `TypeError` at runtime.\n\n## Example\n\n```python\ndef f(x: int, /) -> int: ...\n\nf(x=1) # Error raised here\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "possibly-missing-attribute": { + "title": "detects references to possibly missing attributes", + "description": "## What it does\nChecks for possibly missing attributes.\n\n## Why is this bad?\nAttempting to access a missing attribute will raise an `AttributeError` at runtime.\n\n## Examples\n```python\nclass A:\n if b:\n c = 0\n\nA.c # AttributeError: type object 'A' has no attribute 'c'\n```", "default": "warn", "oneOf": [ { @@ -795,9 +805,9 @@ } ] }, - "possibly-unbound-implicit-call": { - "title": "detects implicit calls to possibly unbound methods", - "description": "## What it does\nChecks for implicit calls to possibly unbound methods.\n\n## Why is this bad?\nExpressions such as `x[y]` and `x * y` call methods\nunder the hood (`__getitem__` and `__mul__` respectively).\nCalling an unbound method will raise an `AttributeError` at runtime.\n\n## Examples\n```python\nimport datetime\n\nclass A:\n if datetime.date.today().weekday() != 6:\n def __getitem__(self, v): ...\n\nA()[0] # TypeError: 'A' object is not subscriptable\n```", + "possibly-missing-implicit-call": { + "title": "detects implicit calls to possibly missing methods", + "description": "## What it does\nChecks for implicit calls to possibly missing methods.\n\n## Why is this bad?\nExpressions such as `x[y]` and `x * y` call methods\nunder the hood (`__getitem__` and `__mul__` respectively).\nCalling a missing method will raise an `AttributeError` at runtime.\n\n## Examples\n```python\nimport datetime\n\nclass A:\n if datetime.date.today().weekday() != 6:\n def __getitem__(self, v): ...\n\nA()[0] # TypeError: 'A' object is not subscriptable\n```", "default": "warn", "oneOf": [ { @@ -805,9 +815,9 @@ } ] }, - "possibly-unbound-import": { - "title": "detects possibly unbound imports", - "description": "## What it does\nChecks for imports of symbols that may be unbound.\n\n## Why is this bad?\nImporting an unbound module or name will raise a `ModuleNotFoundError`\nor `ImportError` at runtime.\n\n## Examples\n```python\n# module.py\nimport datetime\n\nif datetime.date.today().weekday() != 6:\n a = 1\n\n# main.py\nfrom module import a # ImportError: cannot import name 'a' from 'module'\n```", + "possibly-missing-import": { + "title": "detects possibly missing imports", + "description": "## What it does\nChecks for imports of symbols that may be missing.\n\n## Why is this bad?\nImporting a missing module or name will raise a `ModuleNotFoundError`\nor `ImportError` at runtime.\n\n## Examples\n```python\n# module.py\nimport datetime\n\nif datetime.date.today().weekday() != 6:\n a = 1\n\n# main.py\nfrom module import a # ImportError: cannot import name 'a' from 'module'\n```", "default": "warn", "oneOf": [ { From 37c3c4eb12af178f31293d194c6d7601428f6750 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Tue, 23 Sep 2025 17:58:32 +0100 Subject: [PATCH 15/21] Dont use location field in playground --- playground/ty/src/Editor/Editor.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/playground/ty/src/Editor/Editor.tsx b/playground/ty/src/Editor/Editor.tsx index 6fde55bc13ae2..978bb06b476d6 100644 --- a/playground/ty/src/Editor/Editor.tsx +++ b/playground/ty/src/Editor/Editor.tsx @@ -436,10 +436,7 @@ class PlaygroundServer hints: inlayHints.map((hint) => ({ label: hint.label.map((part) => ({ label: part.label, - location: - part.location !== undefined - ? this.mapNavigationTarget(part.location) - : undefined, + // As of 2025-09-23, location isn't supported by Monaco which is why we don't set it })), position: { lineNumber: hint.position.line, From 595dc905405199beb5168977b259f8830a9550e0 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Tue, 21 Oct 2025 17:37:58 +0100 Subject: [PATCH 16/21] Fix merge conflicts --- crates/ty_python_semantic/src/types.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 661efc76e035c..3f51a49b55a5e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -58,14 +58,14 @@ use crate::types::function::{ }; pub(crate) use crate::types::generics::GenericContext; use crate::types::generics::{ - PartialSpecialization, Specialization, bind_typevar, walk_generic_context, - walk_partial_specialization, walk_specialization, + InferableTypeVars, PartialSpecialization, Specialization, bind_typevar, typing_self, + walk_generic_context, }; pub use crate::types::ide_support::{ - CallSignatureDetails, InferableTypeVars, Member, MemberWithDefinition, PartialSpecialization, - Specialization, all_members, bind_typevar, call_signature_details, definition_kind_for_name, - definitions_for_attribute, definitions_for_imported_symbol, definitions_for_keyword_argument, - definitions_for_name, find_active_signature_from_details, inlay_hint_call_argument_details, + CallSignatureDetails, Member, MemberWithDefinition, all_members, call_signature_details, + definition_kind_for_name, definitions_for_attribute, definitions_for_imported_symbol, + definitions_for_keyword_argument, definitions_for_name, find_active_signature_from_details, + inlay_hint_call_argument_details, }; use crate::types::infer::infer_unpack_types; use crate::types::mro::{Mro, MroError, MroIterator}; From 6d9387dc338ae7a1d46c86ff649cb504318e57ea Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Tue, 21 Oct 2025 17:41:40 +0100 Subject: [PATCH 17/21] Fix merge conflicts --- crates/ty_ide/src/inlay_hints.rs | 2 +- crates/ty_python_semantic/src/types.rs | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 6898d6a079e8c..998e0781dc22d 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -7,7 +7,7 @@ use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, Traversal use ruff_python_ast::{AnyNodeRef, Expr, Stmt}; use ruff_text_size::{Ranged, TextRange, TextSize}; use ty_python_semantic::types::Type; -use ty_python_semantic::types::ide_support::inlay_hint_function_argument_details; +use ty_python_semantic::types::ide_support::inlay_hint_call_argument_details; use ty_python_semantic::{HasType, SemanticModel}; #[derive(Debug, Clone)] diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 3f51a49b55a5e..d9a51ce5b9073 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -61,12 +61,6 @@ use crate::types::generics::{ InferableTypeVars, PartialSpecialization, Specialization, bind_typevar, typing_self, walk_generic_context, }; -pub use crate::types::ide_support::{ - CallSignatureDetails, Member, MemberWithDefinition, all_members, call_signature_details, - definition_kind_for_name, definitions_for_attribute, definitions_for_imported_symbol, - definitions_for_keyword_argument, definitions_for_name, find_active_signature_from_details, - inlay_hint_call_argument_details, -}; use crate::types::infer::infer_unpack_types; use crate::types::mro::{Mro, MroError, MroIterator}; pub(crate) use crate::types::narrow::infer_narrowing_constraint; From 52f36d0b35e33f985fc42008ebcb10918d281aa2 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Wed, 22 Oct 2025 13:32:44 +0100 Subject: [PATCH 18/21] Update snapshots --- crates/ty_ide/src/inlay_hints.rs | 417 +++++++++++++++--- .../src/types/ide_support.rs | 16 +- 2 files changed, 364 insertions(+), 69 deletions(-) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 998e0781dc22d..ffaf658e0c9ee 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -333,7 +333,7 @@ mod tests { Db as _, diagnostic::{ Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, - LintName, Severity, Span, + LintName, Severity, Span, SubDiagnostic, SubDiagnosticSeverity, }, files::{File, FileRange, system_path_to_file}, source::source_text, @@ -435,19 +435,13 @@ mod tests { for part in hint.label.parts() { hint_str.push_str(part.text()); - let label_length = part.text().len(); - if let Some(target) = part.target() { - let label_range = TextRange::new( - TextSize::try_from(end_position).unwrap(), - TextSize::try_from(end_position + label_length).unwrap(), - ); + let label_range = TextRange::at(hint.position, TextSize::ZERO); - diagnostics.push(InlayHintLocationDiagnostic::new( - part.text().to_string(), - label_range, - target, - )); + let label_file_range = FileRange::new(self.file, label_range); + + diagnostics + .push(InlayHintLocationDiagnostic::new(label_file_range, target)); } } @@ -564,7 +558,15 @@ mod tests { 4 | self.x = 1 5 | self.y = y | - info: For inlay hint label 'y' at 112..113 + info: Source + --> main.py:7:7 + | + 5 | self.y = y + 6 | + 7 | a = A(2) + | ^ + 8 | a.y = 3 + | "); } @@ -600,7 +602,13 @@ mod tests { | ^ 3 | foo(1) | - info: For inlay hint label 'x' at 27..28 + info: Source + --> main.py:3:5 + | + 2 | def foo(x: int): pass + 3 | foo(1) + | ^ + | "); } @@ -679,7 +687,13 @@ mod tests { | ^ 3 | foo(1, 2) | - info: For inlay hint label 'y' at 41..42 + info: Source + --> main.py:3:8 + | + 2 | def foo(x: int, /, y: int): pass + 3 | foo(1, 2) + | ^ + | "); } @@ -736,7 +750,15 @@ mod tests { 4 | Foo(1) 5 | f = Foo(1) | - info: For inlay hint label 'x' at 53..54 + info: Source + --> main.py:4:5 + | + 2 | class Foo: + 3 | def __init__(self, x: int): pass + 4 | Foo(1) + | ^ + 5 | f = Foo(1) + | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 @@ -747,7 +769,14 @@ mod tests { 4 | Foo(1) 5 | f = Foo(1) | - info: For inlay hint label 'x' at 75..76 + info: Source + --> main.py:5:9 + | + 3 | def __init__(self, x: int): pass + 4 | Foo(1) + 5 | f = Foo(1) + | ^ + | "); } @@ -776,7 +805,15 @@ mod tests { 4 | Foo(1) 5 | f = Foo(1) | - info: For inlay hint label 'x' at 51..52 + info: Source + --> main.py:4:5 + | + 2 | class Foo: + 3 | def __new__(cls, x: int): pass + 4 | Foo(1) + | ^ + 5 | f = Foo(1) + | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:22 @@ -787,7 +824,14 @@ mod tests { 4 | Foo(1) 5 | f = Foo(1) | - info: For inlay hint label 'x' at 73..74 + info: Source + --> main.py:5:9 + | + 3 | def __new__(cls, x: int): pass + 4 | Foo(1) + 5 | f = Foo(1) + | ^ + | "); } @@ -818,7 +862,14 @@ mod tests { 4 | class Foo(metaclass=MetaFoo): 5 | pass | - info: For inlay hint label 'x' at 96..97 + info: Source + --> main.py:6:5 + | + 4 | class Foo(metaclass=MetaFoo): + 5 | pass + 6 | Foo(1) + | ^ + | "); } @@ -860,7 +911,14 @@ mod tests { | ^ 4 | Foo().bar(2) | - info: For inlay hint label 'y' at 54..55 + info: Source + --> main.py:4:11 + | + 2 | class Foo: + 3 | def bar(self, y: int): pass + 4 | Foo().bar(2) + | ^ + | "); } @@ -889,7 +947,14 @@ mod tests { | ^ 5 | Foo.bar(2) | - info: For inlay hint label 'y' at 68..69 + info: Source + --> main.py:5:9 + | + 3 | @classmethod + 4 | def bar(cls, y: int): pass + 5 | Foo.bar(2) + | ^ + | "); } @@ -918,7 +983,14 @@ mod tests { | ^ 5 | Foo.bar(2) | - info: For inlay hint label 'y' at 64..65 + info: Source + --> main.py:5:9 + | + 3 | @staticmethod + 4 | def bar(y: int): pass + 5 | Foo.bar(2) + | ^ + | "); } @@ -944,7 +1016,14 @@ mod tests { 3 | foo(1) 4 | foo('abc') | - info: For inlay hint label 'x' at 33..34 + info: Source + --> main.py:3:5 + | + 2 | def foo(x: int | str): pass + 3 | foo(1) + | ^ + 4 | foo('abc') + | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 @@ -954,7 +1033,14 @@ mod tests { 3 | foo(1) 4 | foo('abc') | - info: For inlay hint label 'x' at 44..45 + info: Source + --> main.py:4:5 + | + 2 | def foo(x: int | str): pass + 3 | foo(1) + 4 | foo('abc') + | ^ + | "); } @@ -977,7 +1063,13 @@ mod tests { | ^ 3 | foo(1, 'hello', True) | - info: For inlay hint label 'x' at 44..45 + info: Source + --> main.py:3:5 + | + 2 | def foo(x: int, y: str, z: bool): pass + 3 | foo(1, 'hello', True) + | ^ + | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:17 @@ -986,7 +1078,13 @@ mod tests { | ^ 3 | foo(1, 'hello', True) | - info: For inlay hint label 'y' at 51..52 + info: Source + --> main.py:3:8 + | + 2 | def foo(x: int, y: str, z: bool): pass + 3 | foo(1, 'hello', True) + | ^ + | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:25 @@ -995,7 +1093,13 @@ mod tests { | ^ 3 | foo(1, 'hello', True) | - info: For inlay hint label 'z' at 64..65 + info: Source + --> main.py:3:17 + | + 2 | def foo(x: int, y: str, z: bool): pass + 3 | foo(1, 'hello', True) + | ^ + | "); } @@ -1018,7 +1122,13 @@ mod tests { | ^ 3 | foo(1, z=True, y='hello') | - info: For inlay hint label 'x' at 44..45 + info: Source + --> main.py:3:5 + | + 2 | def foo(x: int, y: str, z: bool): pass + 3 | foo(1, z=True, y='hello') + | ^ + | "); } @@ -1046,7 +1156,15 @@ mod tests { 3 | foo(1) 4 | foo(1, 'custom') | - info: For inlay hint label 'x' at 64..65 + info: Source + --> main.py:3:5 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + 3 | foo(1) + | ^ + 4 | foo(1, 'custom') + 5 | foo(1, 'custom', True) + | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 @@ -1056,7 +1174,15 @@ mod tests { 3 | foo(1) 4 | foo(1, 'custom') | - info: For inlay hint label 'x' at 75..76 + info: Source + --> main.py:4:5 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + 3 | foo(1) + 4 | foo(1, 'custom') + | ^ + 5 | foo(1, 'custom', True) + | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:17 @@ -1066,7 +1192,15 @@ mod tests { 3 | foo(1) 4 | foo(1, 'custom') | - info: For inlay hint label 'y' at 82..83 + info: Source + --> main.py:4:8 + | + 2 | def foo(x: int, y: str = 'default', z: bool = False): pass + 3 | foo(1) + 4 | foo(1, 'custom') + | ^ + 5 | foo(1, 'custom', True) + | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 @@ -1076,7 +1210,14 @@ mod tests { 3 | foo(1) 4 | foo(1, 'custom') | - info: For inlay hint label 'x' at 100..101 + info: Source + --> main.py:5:5 + | + 3 | foo(1) + 4 | foo(1, 'custom') + 5 | foo(1, 'custom', True) + | ^ + | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:17 @@ -1086,7 +1227,14 @@ mod tests { 3 | foo(1) 4 | foo(1, 'custom') | - info: For inlay hint label 'y' at 107..108 + info: Source + --> main.py:5:8 + | + 3 | foo(1) + 4 | foo(1, 'custom') + 5 | foo(1, 'custom', True) + | ^ + | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:37 @@ -1096,7 +1244,14 @@ mod tests { 3 | foo(1) 4 | foo(1, 'custom') | - info: For inlay hint label 'z' at 121..122 + info: Source + --> main.py:5:18 + | + 3 | foo(1) + 4 | foo(1, 'custom') + 5 | foo(1, 'custom', True) + | ^ + | "); } @@ -1136,7 +1291,14 @@ mod tests { 9 | 10 | baz(foo(5), bar(bar('test')), True) | - info: For inlay hint label 'a' at 125..126 + info: Source + --> main.py:10:5 + | + 8 | def baz(a: int, b: str, c: bool): pass + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | ^ + | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 @@ -1145,7 +1307,14 @@ mod tests { | ^ 3 | return x * 2 | - info: For inlay hint label 'x' at 133..134 + info: Source + --> main.py:10:9 + | + 8 | def baz(a: int, b: str, c: bool): pass + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | ^ + | info[inlay-hint-location]: Inlay Hint Target --> main.py:8:17 @@ -1157,7 +1326,14 @@ mod tests { 9 | 10 | baz(foo(5), bar(bar('test')), True) | - info: For inlay hint label 'b' at 141..142 + info: Source + --> main.py:10:13 + | + 8 | def baz(a: int, b: str, c: bool): pass + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | ^ + | info[inlay-hint-location]: Inlay Hint Target --> main.py:5:9 @@ -1168,7 +1344,14 @@ mod tests { | ^ 6 | return y | - info: For inlay hint label 'y' at 149..150 + info: Source + --> main.py:10:17 + | + 8 | def baz(a: int, b: str, c: bool): pass + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | ^ + | info[inlay-hint-location]: Inlay Hint Target --> main.py:5:9 @@ -1179,7 +1362,14 @@ mod tests { | ^ 6 | return y | - info: For inlay hint label 'y' at 157..158 + info: Source + --> main.py:10:21 + | + 8 | def baz(a: int, b: str, c: bool): pass + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | ^ + | info[inlay-hint-location]: Inlay Hint Target --> main.py:8:25 @@ -1191,7 +1381,14 @@ mod tests { 9 | 10 | baz(foo(5), bar(bar('test')), True) | - info: For inlay hint label 'c' at 171..172 + info: Source + --> main.py:10:31 + | + 8 | def baz(a: int, b: str, c: bool): pass + 9 | + 10 | baz(foo(5), bar(bar('test')), True) + | ^ + | "); } @@ -1226,7 +1423,14 @@ mod tests { 4 | return self 5 | def bar(self, name: str) -> 'A': | - info: For inlay hint label 'value' at 157..162 + info: Source + --> main.py:8:9 + | + 6 | return self + 7 | def baz(self): pass + 8 | A().foo(42).bar('test').baz() + | ^ + | info[inlay-hint-location]: Inlay Hint Target --> main.py:5:19 @@ -1238,7 +1442,14 @@ mod tests { 6 | return self 7 | def baz(self): pass | - info: For inlay hint label 'name' at 173..177 + info: Source + --> main.py:8:17 + | + 6 | return self + 7 | def baz(self): pass + 8 | A().foo(42).bar('test').baz() + | ^ + | "); } @@ -1268,7 +1479,14 @@ mod tests { 3 | return x 4 | def bar(y: int): pass | - info: For inlay hint label 'x' at 70..71 + info: Source + --> main.py:5:11 + | + 3 | return x + 4 | def bar(y: int): pass + 5 | bar(y=foo('test')) + | ^ + | "); } @@ -1312,7 +1530,14 @@ mod tests { 3 | foo(1, 'pos', 3.14, False, e=42) 4 | foo(1, 'pos', 3.14, e=42, f='custom') | - info: For inlay hint label 'c' at 105..106 + info: Source + --> main.py:3:15 + | + 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass + 3 | foo(1, 'pos', 3.14, False, e=42) + | ^ + 4 | foo(1, 'pos', 3.14, e=42, f='custom') + | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:38 @@ -1322,7 +1547,14 @@ mod tests { 3 | foo(1, 'pos', 3.14, False, e=42) 4 | foo(1, 'pos', 3.14, e=42, f='custom') | - info: For inlay hint label 'd' at 115..116 + info: Source + --> main.py:3:21 + | + 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass + 3 | foo(1, 'pos', 3.14, False, e=42) + | ^ + 4 | foo(1, 'pos', 3.14, e=42, f='custom') + | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:28 @@ -1332,7 +1564,14 @@ mod tests { 3 | foo(1, 'pos', 3.14, False, e=42) 4 | foo(1, 'pos', 3.14, e=42, f='custom') | - info: For inlay hint label 'c' at 146..147 + info: Source + --> main.py:4:15 + | + 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass + 3 | foo(1, 'pos', 3.14, False, e=42) + 4 | foo(1, 'pos', 3.14, e=42, f='custom') + | ^ + | "); } @@ -1364,7 +1603,14 @@ mod tests { | ^ 3 | pass | - info: For inlay hint label 'x' at 26..27 + info: Source + --> main.py:4:5 + | + 2 | from foo import bar + 3 | + 4 | bar(1) + | ^ + | "); } @@ -1407,7 +1653,15 @@ mod tests { 6 | @overload 7 | def foo(x: str) -> int: ... | - info: For inlay hint label 'x' at 136..137 + info: Source + --> main.py:11:5 + | + 9 | return x + 10 | + 11 | foo(42) + | ^ + 12 | foo('hello') + | info[inlay-hint-location]: Inlay Hint Target --> main.py:5:9 @@ -1418,7 +1672,13 @@ mod tests { 6 | @overload 7 | def foo(x: str) -> int: ... | - info: For inlay hint label 'x' at 148..149 + info: Source + --> main.py:12:5 + | + 11 | foo(42) + 12 | foo('hello') + | ^ + | "); } @@ -1463,7 +1723,15 @@ mod tests { 3 | def bar(y: int): pass 4 | foo(1) | - info: For inlay hint label 'x' at 49..50 + info: Source + --> main.py:4:5 + | + 2 | def foo(x: int): pass + 3 | def bar(y: int): pass + 4 | foo(1) + | ^ + 5 | bar(2) + | "); } @@ -1486,7 +1754,13 @@ mod tests { | ^ 3 | foo(1, 2) | - info: For inlay hint label 'y' at 39..40 + info: Source + --> main.py:3:8 + | + 2 | def foo(_x: int, y: int): pass + 3 | foo(1, 2) + | ^ + | "); } @@ -1519,7 +1793,14 @@ mod tests { 4 | y: int 5 | ): ... | - info: For inlay hint label 'x' at 45..46 + info: Source + --> main.py:7:5 + | + 5 | ): ... + 6 | + 7 | foo(1, 2) + | ^ + | info[inlay-hint-location]: Inlay Hint Target --> main.py:4:5 @@ -1530,21 +1811,26 @@ mod tests { | ^ 5 | ): ... | - info: For inlay hint label 'y' at 52..53 + info: Source + --> main.py:7:8 + | + 5 | ): ... + 6 | + 7 | foo(1, 2) + | ^ + | "); } struct InlayHintLocationDiagnostic { - source_text: String, - source_range: TextRange, + source: FileRange, target: FileRange, } impl InlayHintLocationDiagnostic { - fn new(source_text: String, source_range: TextRange, target: &NavigationTarget) -> Self { + fn new(source: FileRange, target: &NavigationTarget) -> Self { Self { - source_text, - source_range, + source, target: FileRange::new(target.file(), target.focus_range()), } } @@ -1552,18 +1838,23 @@ mod tests { impl IntoDiagnostic for InlayHintLocationDiagnostic { fn into_diagnostic(self) -> Diagnostic { + let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source"); + + source.annotate(Annotation::primary( + Span::from(self.source.file()).with_range(self.source.range()), + )); + let mut main = Diagnostic::new( DiagnosticId::Lint(LintName::of("inlay-hint-location")), Severity::Info, "Inlay Hint Target".to_string(), ); + main.annotate(Annotation::primary( Span::from(self.target.file()).with_range(self.target.range()), )); - main.info(format!( - "For inlay hint label '{}' at {:?}", - self.source_text, self.source_range - )); + + main.sub(source); main } diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 35b509a09de35..b2f7d6904b596 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -870,7 +870,7 @@ pub struct CallSignatureDetails<'db> { pub parameter_label_offsets: Vec, /// Offsets for each parameter in the signature definition. - pub definition_parameter_offsets: HashMap, + pub definition_parameter_label_offsets: HashMap, /// The names of the parameters in the signature, in order. /// This provides easy access to parameter names for documentation lookup. @@ -933,7 +933,7 @@ pub fn call_signature_details<'db>( let display_details = signature.display(db).to_string_parts(); let parameter_label_offsets = display_details.parameter_ranges; let parameter_names = display_details.parameter_names; - let definition_parameter_offsets = signature + let definition_parameter_label_offsets = signature .definition() .and_then(|definition| { let file = definition.file(db); @@ -943,7 +943,9 @@ pub fn call_signature_details<'db>( |offsets| { offsets .into_iter() - .map(|(name, offset)| (name, FileRange::new(file, offset))) + .map(|(name, text_range)| { + (name, FileRange::new(file, text_range)) + }) .collect() }, ) @@ -955,7 +957,7 @@ pub fn call_signature_details<'db>( signature, label: display_details.label, parameter_label_offsets, - definition_parameter_offsets, + definition_parameter_label_offsets, parameter_names, argument_to_parameter_mapping, } @@ -1116,7 +1118,8 @@ pub fn inlay_hint_call_argument_details<'db>( let parameters = call_signature_details.signature.parameters(); - let definition_parameter_offsets = &call_signature_details.definition_parameter_offsets; + let definition_parameter_label_offsets = + &call_signature_details.definition_parameter_label_offsets; let mut argument_names = HashMap::new(); @@ -1140,7 +1143,8 @@ pub fn inlay_hint_call_argument_details<'db>( continue; }; - let parameter_label_offset = definition_parameter_offsets.get(¶m.name()?.to_string()); + let parameter_label_offset = + definition_parameter_label_offsets.get(¶m.name()?.to_string()); // Only add hints for parameters that can be specified by name if !param.is_positional_only() && !param.is_variadic() && !param.is_keyword_variadic() { From 58fe44124e81a269edde6fb9f3534f69f5926364 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Wed, 22 Oct 2025 14:00:47 +0100 Subject: [PATCH 19/21] Add get_definition_parameter_range --- .../src/types/ide_support.rs | 46 +++++++------------ 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index b2f7d6904b596..1ecf6f0ea6d7e 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -869,9 +869,6 @@ pub struct CallSignatureDetails<'db> { /// within the full signature string. pub parameter_label_offsets: Vec, - /// Offsets for each parameter in the signature definition. - pub definition_parameter_label_offsets: HashMap, - /// The names of the parameters in the signature, in order. /// This provides easy access to parameter names for documentation lookup. pub parameter_names: Vec, @@ -885,6 +882,20 @@ pub struct CallSignatureDetails<'db> { pub argument_to_parameter_mapping: Vec>, } +impl CallSignatureDetails<'_> { + fn get_definition_parameter_range(&self, db: &dyn Db, name: &str) -> Option { + let definition = self.signature.definition()?; + let file = definition.file(db); + let module_ref = parsed_module(db, file).load(db); + let offsets = definition_parameter_offsets(definition.kind(db), &module_ref)?; + + offsets + .into_iter() + .find(|(param_name, _)| param_name == name) + .map(|(_, text_range)| FileRange::new(file, text_range)) + } +} + fn definition_parameter_offsets( definition_kind: &DefinitionKind, module_ref: &ParsedModuleRef, @@ -933,31 +944,12 @@ pub fn call_signature_details<'db>( let display_details = signature.display(db).to_string_parts(); let parameter_label_offsets = display_details.parameter_ranges; let parameter_names = display_details.parameter_names; - let definition_parameter_label_offsets = signature - .definition() - .and_then(|definition| { - let file = definition.file(db); - let module_ref = parsed_module(db, file).load(db); - - definition_parameter_offsets(definition.kind(db), &module_ref).map( - |offsets| { - offsets - .into_iter() - .map(|(name, text_range)| { - (name, FileRange::new(file, text_range)) - }) - .collect() - }, - ) - }) - .unwrap_or_default(); CallSignatureDetails { definition: signature.definition(), signature, label: display_details.label, parameter_label_offsets, - definition_parameter_label_offsets, parameter_names, argument_to_parameter_mapping, } @@ -1118,9 +1110,6 @@ pub fn inlay_hint_call_argument_details<'db>( let parameters = call_signature_details.signature.parameters(); - let definition_parameter_label_offsets = - &call_signature_details.definition_parameter_label_offsets; - let mut argument_names = HashMap::new(); for arg_index in 0..call_expr.arguments.args.len() { @@ -1144,17 +1133,14 @@ pub fn inlay_hint_call_argument_details<'db>( }; let parameter_label_offset = - definition_parameter_label_offsets.get(¶m.name()?.to_string()); + call_signature_details.get_definition_parameter_range(db, param.name()?.as_ref()); // Only add hints for parameters that can be specified by name if !param.is_positional_only() && !param.is_variadic() && !param.is_keyword_variadic() { let Some(name) = param.name() else { continue; }; - argument_names.insert( - arg_index, - (name.to_string(), parameter_label_offset.copied()), - ); + argument_names.insert(arg_index, (name.to_string(), parameter_label_offset)); } } From 300671118a3b332d6310c8ad66fea1764d40ab90 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 17 Nov 2025 10:27:26 +0100 Subject: [PATCH 20/21] Nits --- crates/ty_ide/src/goto.rs | 2 +- crates/ty_ide/src/inlay_hints.rs | 14 ++++++--- crates/ty_ide/src/lib.rs | 10 ++++++ .../src/types/ide_support.rs | 31 +++++-------------- .../src/server/api/requests/inlay_hints.rs | 21 +++++-------- crates/ty_wasm/src/lib.rs | 6 ++-- 6 files changed, 38 insertions(+), 46 deletions(-) diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 094d2008d2447..2faacc4a86ca4 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -855,7 +855,7 @@ fn convert_resolved_definitions_to_targets( } ty_python_semantic::ResolvedDefinition::FileWithRange(file_range) => { // For file ranges, navigate to the specific range within the file - crate::NavigationTarget::new(file_range.file(), file_range.range()) + crate::NavigationTarget::from(file_range) } }) .collect() diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index fc9c05473e9af..7e1e12322802c 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -68,6 +68,10 @@ impl InlayHintLabel { pub fn parts(&self) -> &[InlayHintLabelPart] { &self.parts } + + pub fn into_parts(self) -> Vec { + self.parts + } } pub struct InlayHintDisplay<'a> { @@ -102,6 +106,10 @@ impl InlayHintLabelPart { &self.text } + pub fn into_text(self) -> String { + self.text + } + pub fn target(&self) -> Option<&NavigationTarget> { self.target.as_ref() } @@ -303,14 +311,10 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { if let Some((name, parameter_label_offset)) = details.argument_names.get(&index) && !arg_matches_name(&arg_or_keyword, name) { - let navigation_target = parameter_label_offset.map(|file_range| { - NavigationTarget::new(file_range.file(), file_range.range()) - }); - self.add_call_argument_name( arg_or_keyword.range().start(), name, - navigation_target, + parameter_label_offset.map(NavigationTarget::from), ); } self.visit_expr(arg_or_keyword.value()); diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index be803638bc26e..92f14813d40a8 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -138,6 +138,16 @@ impl NavigationTarget { } } +impl From for NavigationTarget { + fn from(value: FileRange) -> Self { + Self { + file: value.file(), + focus_range: value.range(), + full_range: value.range(), + } + } +} + /// Specifies the kind of reference operation. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ReferenceKind { diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index dfd814171c149..7647629d88f43 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -20,7 +20,7 @@ use crate::types::{ }; use crate::{Db, HasType, NameKind, SemanticModel}; use ruff_db::files::{File, FileRange}; -use ruff_db::parsed::{ParsedModuleRef, parsed_module}; +use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast}; use ruff_text_size::{Ranged, TextRange}; @@ -963,29 +963,14 @@ impl CallSignatureDetails<'_> { let definition = self.signature.definition()?; let file = definition.file(db); let module_ref = parsed_module(db, file).load(db); - let offsets = definition_parameter_offsets(definition.kind(db), &module_ref)?; - offsets - .into_iter() - .find(|(param_name, _)| param_name == name) - .map(|(_, text_range)| FileRange::new(file, text_range)) - } -} + let parameters = match definition.kind(db) { + DefinitionKind::Function(node) => &node.node(&module_ref).parameters, + // TODO: lambda functions + _ => return None, + }; -fn definition_parameter_offsets( - definition_kind: &DefinitionKind, - module_ref: &ParsedModuleRef, -) -> Option> { - match definition_kind { - DefinitionKind::Function(node) => Some( - node.node(module_ref) - .parameters - .iter() - .map(|param| (param.name().to_string(), param.name().range())) - .collect(), - ), - // TODO: lambda functions - _ => None, + Some(FileRange::new(file, parameters.find(name)?.name().range)) } } @@ -1229,7 +1214,7 @@ pub fn inlay_hint_call_argument_details<'db>( }; let parameter_label_offset = - call_signature_details.get_definition_parameter_range(db, param.name()?.as_ref()); + call_signature_details.get_definition_parameter_range(db, param.name()?); // Only add hints for parameters that can be specified by name if !param.is_positional_only() && !param.is_variadic() && !param.is_keyword_variadic() { diff --git a/crates/ty_server/src/server/api/requests/inlay_hints.rs b/crates/ty_server/src/server/api/requests/inlay_hints.rs index d66b342c96f53..ada6f183023f3 100644 --- a/crates/ty_server/src/server/api/requests/inlay_hints.rs +++ b/crates/ty_server/src/server/api/requests/inlay_hints.rs @@ -5,19 +5,14 @@ use lsp_types::{InlayHintParams, Url}; use ty_ide::{InlayHintKind, InlayHintLabel, inlay_hints}; use ty_project::ProjectDatabase; -use crate::document::{RangeExt, TextSizeExt}; +use crate::PositionEncoding; +use crate::document::{RangeExt, TextSizeExt, ToLink}; use crate::server::api::traits::{ BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, }; use crate::session::DocumentSnapshot; use crate::session::client::Client; -use crate::document::{RangeExt, TextSizeExt, ToLink}; -use crate::server::api::traits::{ - BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, -}; -use crate::session::{DocumentSnapshot, client::Client}; - pub(crate) struct InlayHintRequestHandler; impl RequestHandler for InlayHintRequestHandler { @@ -63,7 +58,7 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler { .position .to_lsp_position(db, file, snapshot.encoding())? .local_position(), - label: inlay_hint_label(&hint.label, db, snapshot), + label: inlay_hint_label(&hint.label, db, snapshot.encoding()), kind: Some(inlay_hint_kind(&hint.kind)), tooltip: None, padding_left: None, @@ -90,17 +85,15 @@ fn inlay_hint_kind(inlay_hint_kind: &InlayHintKind) -> lsp_types::InlayHintKind fn inlay_hint_label( inlay_hint_label: &InlayHintLabel, db: &ProjectDatabase, - snapshot: &DocumentSnapshot, + encoding: PositionEncoding, ) -> lsp_types::InlayHintLabel { let mut label_parts = Vec::new(); for part in inlay_hint_label.parts() { - let location = part - .target() - .and_then(|target| target.to_location(db, snapshot.encoding())); - label_parts.push(lsp_types::InlayHintLabelPart { value: part.text().into(), - location, + location: part + .target() + .and_then(|target| target.to_location(db, encoding)), tooltip: None, command: None, }); diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index 12e3180899825..3faf75a4d59c0 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -471,10 +471,9 @@ impl Workspace { .map(|hint| InlayHint { label: hint .label - .parts() - .iter() + .into_parts() + .into_iter() .map(|part| InlayHintLabelPart { - label: part.text().to_string(), location: part.target().map(|target| { location_link_from_navigation_target( target, @@ -483,6 +482,7 @@ impl Workspace { None, ) }), + label: part.into_text(), }) .collect(), position: Position::from_text_size( From 235562752811ada52444dbbe1cfe55441e49a204 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 17 Nov 2025 10:28:27 +0100 Subject: [PATCH 21/21] Discard changes to crates/ty_ide/src/signature_help.rs --- crates/ty_ide/src/signature_help.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/ty_ide/src/signature_help.rs b/crates/ty_ide/src/signature_help.rs index ca52f7074eacf..d0648b2e2514a 100644 --- a/crates/ty_ide/src/signature_help.rs +++ b/crates/ty_ide/src/signature_help.rs @@ -382,7 +382,7 @@ mod tests { f = func_a else: f = func_b - + f( "#, ); @@ -427,10 +427,10 @@ mod tests { @overload def process(value: int) -> str: ... - + @overload def process(value: str) -> int: ... - + def process(value): if isinstance(value, int): return str(value) @@ -827,10 +827,10 @@ def ab(a: int, *, c: int): r#" class Point: """A simple point class representing a 2D coordinate.""" - + def __init__(self, x: int, y: int): """Initialize a point with x and y coordinates. - + Args: x: The x-coordinate y: The y-coordinate @@ -962,12 +962,12 @@ def ab(a: int, *, c: int): r#" from typing import overload - @overload + @overload def process(value: int) -> str: ... - + @overload def process(value: str, flag: bool) -> int: ... - + def process(value, flag=None): if isinstance(value, int): return str(value)