Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions crates/ruff_db/src/diagnostic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,18 @@ impl Diagnostic {
}
}

impl AsRef<Diagnostic> for Diagnostic {
fn as_ref(&self) -> &Diagnostic {
self
}
}

impl AsMut<Diagnostic> for Diagnostic {
fn as_mut(&mut self) -> &mut Diagnostic {
self
}
}

#[derive(Debug, Clone, Eq, PartialEq)]
struct DiagnosticInner {
id: DiagnosticId,
Expand Down
73 changes: 72 additions & 1 deletion crates/ty/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ fn config_override_python_platform() -> anyhow::Result<()> {
}

#[test]
fn config_file_annotation_showing_where_python_version_set() -> anyhow::Result<()> {
fn config_file_annotation_showing_where_python_version_set_typing_error() -> anyhow::Result<()> {
let case = TestCase::with_files([
(
"pyproject.toml",
Expand Down Expand Up @@ -308,6 +308,77 @@ fn config_file_annotation_showing_where_python_version_set() -> anyhow::Result<(
Ok(())
}

#[test]
fn config_file_annotation_showing_where_python_version_set_syntax_error() -> anyhow::Result<()> {
let case = TestCase::with_files([
(
"pyproject.toml",
r#"
[project]
requires-python = ">=3.8"
"#,
),
(
"test.py",
r#"
match object():
case int():
pass
case _:
pass
"#,
),
])?;

assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]
--> test.py:2:1
|
2 | match object():
| ^^^^^ Cannot use `match` statement on Python 3.8 (syntax was added in Python 3.10)
3 | case int():
4 | pass
|
info: Python 3.8 was assumed when parsing syntax
--> pyproject.toml:3:19
|
2 | [project]
3 | requires-python = ">=3.8"
| ^^^^^^^ Python 3.8 assumed due to this configuration setting
|

Found 1 diagnostic

----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);

assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]
--> test.py:2:1
|
2 | match object():
| ^^^^^ Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
3 | case int():
4 | pass
|
info: Python 3.9 was assumed when parsing syntax because it was specified on the command line

Found 1 diagnostic

----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");

Ok(())
}

/// Paths specified on the CLI are relative to the current working directory and not the project root.
///
/// We test this by adding an extra search path from the CLI to the libs directory when
Expand Down
7 changes: 5 additions & 2 deletions crates/ty_project/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ use std::sync::Arc;
use thiserror::Error;
use tracing::error;
use ty_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSelection};
use ty_python_semantic::register_lints;
use ty_python_semantic::types::check_types;
use ty_python_semantic::{add_inferred_python_version_hint_to_diagnostic, register_lints};

pub mod combine;

Expand Down Expand Up @@ -464,7 +464,10 @@ fn check_file_impl(db: &dyn Db, file: File) -> Vec<Diagnostic> {
parsed
.unsupported_syntax_errors()
.iter()
.map(|error| create_unsupported_syntax_diagnostic(file, error)),
.map(|error| create_unsupported_syntax_diagnostic(file, error))
.map(|error| {
add_inferred_python_version_hint_to_diagnostic(db.upcast(), error, "parsing syntax")
}),
);

{
Expand Down
1 change: 1 addition & 0 deletions crates/ty_python_semantic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 types::add_inferred_python_version_hint_to_diagnostic;

pub mod ast_node_ref;
mod db;
Expand Down
1 change: 1 addition & 0 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ use crate::symbol::{
use crate::types::call::{Bindings, CallArgumentTypes, CallableBinding};
pub(crate) use crate::types::class_base::ClassBase;
use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder};
pub use crate::types::diagnostic::add_inferred_python_version_hint_to_diagnostic;
use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION};
use crate::types::generics::{GenericContext, PartialSpecialization, Specialization};
use crate::types::infer::infer_unpack_types;
Expand Down
14 changes: 14 additions & 0 deletions crates/ty_python_semantic/src/types/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,20 @@ impl std::ops::DerefMut for LintDiagnosticGuard<'_, '_> {
}
}

impl AsRef<Diagnostic> for LintDiagnosticGuard<'_, '_> {
fn as_ref(&self) -> &Diagnostic {
// OK because `self.diag` is only `None` within `Drop`.
self.diag.as_ref().unwrap()
}
}

impl AsMut<Diagnostic> for LintDiagnosticGuard<'_, '_> {
fn as_mut(&mut self) -> &mut Diagnostic {
// OK because `self.diag` is only `None` within `Drop`.
self.diag.as_mut().unwrap()
}
}

/// Finishes use of this guard.
///
/// This will add the lint as a diagnostic to the typing context if
Expand Down
34 changes: 24 additions & 10 deletions crates/ty_python_semantic/src/types/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1762,42 +1762,56 @@ pub(super) fn report_possibly_unbound_attribute(
));
}

pub(super) fn add_inferred_python_version_hint(db: &dyn Db, mut diagnostic: LintDiagnosticGuard) {
/// 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<D, A>(
db: &dyn Db,
mut diagnostic: D,
action: A,
) -> D
where
D: AsMut<Diagnostic>,
A: std::fmt::Display,
{
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",
diagnostic.as_mut().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 resolving types"),
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);
diagnostic.as_mut().sub(sub_diagnostic);
} else {
diagnostic.info(format_args!(
"Python {version} was assumed when resolving types because of your configuration file(s)",
diagnostic.as_mut().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 resolving types \
diagnostic.as_mut().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",
));
}
}

diagnostic
}

pub(super) fn report_unresolved_reference(context: &InferContext, expr_name_node: &ast::ExprName) {
Expand All @@ -1811,7 +1825,7 @@ 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(), diagnostic, "resolving types");
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/ty_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6009,7 +6009,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);
diagnostic::add_inferred_python_version_hint_to_diagnostic(db, diag, "resolving types");
}
}
Type::unknown()
Expand Down
Loading