[ty] Tell the user why we inferred a certain Python version when reporting version-specific syntax errors (#18295)

This commit is contained in:
Alex Waygood 2025-05-26 21:44:43 +01:00 committed by GitHub
parent 0a11baf29c
commit 6453ac9ea1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 201 additions and 107 deletions

View file

@ -14,6 +14,7 @@ pub use program::{
pub use python_platform::PythonPlatform;
pub use semantic_model::{HasType, SemanticModel};
pub use site_packages::SysPrefixPathOrigin;
pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
pub mod ast_node_ref;
mod db;

View file

@ -50,6 +50,7 @@ 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};
pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
use crate::{Db, FxOrderSet, Module, Program};
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass};
use instance::Protocol;

View file

@ -1,8 +1,12 @@
use super::call::CallErrorKind;
use super::context::InferContext;
use super::mro::DuplicateBaseError;
use super::{CallArgumentTypes, CallDunderError, ClassBase, ClassLiteral, KnownClass};
use super::{
CallArgumentTypes, CallDunderError, ClassBase, ClassLiteral, KnownClass,
add_inferred_python_version_hint_to_diagnostic,
};
use crate::db::Db;
use crate::declare_lint;
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
use crate::suppression::FileSuppressionId;
use crate::types::LintDiagnosticGuard;
@ -12,10 +16,8 @@ use crate::types::string_annotation::{
RAW_STRING_TYPE_ANNOTATION,
};
use crate::types::{KnownFunction, KnownInstanceType, Type, protocol_class::ProtocolClassLiteral};
use crate::{Program, PythonVersionWithSource, declare_lint};
use itertools::Itertools;
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic};
use ruff_db::files::system_path_to_file;
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic};
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_python_stdlib::builtins::version_builtin_was_added;
use ruff_text_size::{Ranged, TextRange};
@ -1762,44 +1764,6 @@ pub(super) fn report_possibly_unbound_attribute(
));
}
pub(super) fn add_inferred_python_version_hint(db: &dyn Db, mut diagnostic: LintDiagnosticGuard) {
let program = Program::get(db);
let PythonVersionWithSource { version, source } = program.python_version_with_source(db);
match source {
crate::PythonVersionSource::Cli => {
diagnostic.info(format_args!(
"Python {version} was assumed when resolving types because it was specified on the command line",
));
}
crate::PythonVersionSource::File(path, range) => {
if let Ok(file) = system_path_to_file(db.upcast(), &**path) {
let mut sub_diagnostic = SubDiagnostic::new(
Severity::Info,
format_args!("Python {version} was assumed when resolving types"),
);
sub_diagnostic.annotate(
Annotation::primary(Span::from(file).with_optional_range(*range)).message(
format_args!("Python {version} assumed due to this configuration setting"),
),
);
diagnostic.sub(sub_diagnostic);
} else {
diagnostic.info(format_args!(
"Python {version} was assumed when resolving types because of your configuration file(s)",
));
}
}
crate::PythonVersionSource::Default => {
diagnostic.info(format_args!(
"Python {version} was assumed when resolving types \
because it is the newest Python version supported by ty, \
and neither a command-line argument nor a configuration setting was provided",
));
}
}
}
pub(super) fn report_unresolved_reference(context: &InferContext, expr_name_node: &ast::ExprName) {
let Some(builder) = context.report_lint(&UNRESOLVED_REFERENCE, expr_name_node) else {
return;
@ -1811,7 +1775,11 @@ pub(super) fn report_unresolved_reference(context: &InferContext, expr_name_node
diagnostic.info(format_args!(
"`{id}` was added as a builtin in Python 3.{version_added_to_builtins}"
));
add_inferred_python_version_hint(context.db(), diagnostic);
add_inferred_python_version_hint_to_diagnostic(
context.db(),
&mut diagnostic,
"resolving types",
);
}
}

View file

@ -114,7 +114,9 @@ use super::string_annotation::{
BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation,
};
use super::subclass_of::SubclassOfInner;
use super::{BoundSuperError, BoundSuperType, ClassBase};
use super::{
BoundSuperError, BoundSuperType, ClassBase, add_inferred_python_version_hint_to_diagnostic,
};
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
@ -6029,7 +6031,7 @@ impl<'db> TypeInferenceBuilder<'db> {
diag.info(
"Note that `X | Y` PEP 604 union syntax is only available in Python 3.10 and later",
);
diagnostic::add_inferred_python_version_hint(db, diag);
add_inferred_python_version_hint_to_diagnostic(db, &mut diag, "resolving types");
}
}
Type::unknown()

View file

@ -0,0 +1,51 @@
use crate::{Db, Program, PythonVersionWithSource};
use ruff_db::{
diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic},
files::system_path_to_file,
};
/// Add a subdiagnostic to `diagnostic` that explains why a certain Python version was inferred.
///
/// ty can infer the Python version from various sources, such as command-line arguments,
/// configuration files, or defaults.
pub fn add_inferred_python_version_hint_to_diagnostic(
db: &dyn Db,
diagnostic: &mut Diagnostic,
action: &str,
) {
let program = Program::get(db);
let PythonVersionWithSource { version, source } = program.python_version_with_source(db);
match source {
crate::PythonVersionSource::Cli => {
diagnostic.info(format_args!(
"Python {version} was assumed when {action} because it was specified on the command line",
));
}
crate::PythonVersionSource::File(path, range) => {
if let Ok(file) = system_path_to_file(db.upcast(), &**path) {
let mut sub_diagnostic = SubDiagnostic::new(
Severity::Info,
format_args!("Python {version} was assumed when {action}"),
);
sub_diagnostic.annotate(
Annotation::primary(Span::from(file).with_optional_range(*range)).message(
format_args!("Python {version} assumed due to this configuration setting"),
),
);
diagnostic.sub(sub_diagnostic);
} else {
diagnostic.info(format_args!(
"Python {version} was assumed when {action} because of your configuration file(s)",
));
}
}
crate::PythonVersionSource::Default => {
diagnostic.info(format_args!(
"Python {version} was assumed when {action} \
because it is the newest Python version supported by ty, \
and neither a command-line argument nor a configuration setting was provided",
));
}
}
}

View file

@ -1 +1,2 @@
pub(crate) mod diagnostics;
pub(crate) mod subscript;