diff --git a/crates/ty/tests/cli/uv_workspace.rs b/crates/ty/tests/cli/uv_workspace.rs index b321e9213e223..74e55a7ffda73 100644 --- a/crates/ty/tests/cli/uv_workspace.rs +++ b/crates/ty/tests/cli/uv_workspace.rs @@ -142,6 +142,86 @@ fn explicit_file_path_disables_uv_workspace_discovery() -> anyhow::Result<()> { Ok(()) } +/// Scripts discovered under a selected directory are independent uv projects and must not be +/// analyzed as part of that directory's workspace member. +#[cfg(feature = "test-uv")] +#[test] +fn implicitly_discovered_scripts_are_not_checked() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file( + "packages/member/script.py", + r#" + # /// script + # dependencies = [] + # /// + + value: int = "ignored-script" + "#, + )?; + case.write_file( + "packages/member/nested/script.py", + r#" + # /// script + # dependencies = [] + # /// + + value: int = "ignored-nested-script" + "#, + )?; + + let mut command = command_with_uv(&case, None)?; + command + .current_dir(case.root().join("packages/member")) + .arg("."); + + assert_cmd_snapshot!(command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + member.py:1:14: error[invalid-assignment] Object of type `Literal["selected-member"]` is not assignable to `int` + Found 1 diagnostic + + ----- stderr ----- + "#); + + Ok(()) +} + +/// Selecting a script itself explicitly opts into checking that script even in uv integration +/// mode. +#[cfg(feature = "test-uv")] +#[test] +fn explicitly_selected_script_is_checked() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file( + "packages/member/script.py", + r#" + # /// script + # dependencies = [] + # /// + + value: int = "selected-script" + "#, + )?; + + let mut command = command_with_uv(&case, None)?; + command + .current_dir(case.root().join("packages/member")) + .arg("script.py"); + + assert_cmd_snapshot!(command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + script.py:6:14: error[invalid-assignment] Object of type `Literal["selected-script"]` is not assignable to `int` + Found 1 diagnostic + + ----- stderr ----- + "#); + + Ok(()) +} + /// An explicitly selected member inherits ty rule configuration from the uv workspace root. #[cfg(feature = "test-uv")] #[test] diff --git a/crates/ty_project/src/metadata.rs b/crates/ty_project/src/metadata.rs index b87c086ff403f..e6439cf14df43 100644 --- a/crates/ty_project/src/metadata.rs +++ b/crates/ty_project/src/metadata.rs @@ -24,7 +24,7 @@ mod configuration_file; pub mod options; pub mod pyproject; pub mod python_version; -mod script; +pub(crate) mod script; pub mod settings; mod uv; pub mod value; diff --git a/crates/ty_project/src/walk.rs b/crates/ty_project/src/walk.rs index 6efea29574048..cae68037d45d0 100644 --- a/crates/ty_project/src/walk.rs +++ b/crates/ty_project/src/walk.rs @@ -1,4 +1,5 @@ use crate::glob::IncludeExcludeFilter; +use crate::metadata::script::script_metadata; use crate::{Db, GlobFilterCheckMode, IncludeResult, Project}; use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity}; use ruff_db::files::{File, system_path_to_file}; @@ -167,6 +168,7 @@ impl ProjectFilesWalker { }; let filter = ProjectFilesFilter::from_project(db, project); + let skip_implicit_scripts = project.metadata(db).has_uv_workspace(); let files = std::sync::Mutex::new(Vec::new()); let diagnostics = std::sync::Mutex::new(Vec::new()); @@ -259,6 +261,18 @@ impl ProjectFilesWalker { // If this returns `Err`, then the file was deleted between now and when the walk callback was called. // We can ignore this. if let Ok(file) = system_path_to_file(&*db, entry.path()) { + if entry.depth() > 0 + && skip_implicit_scripts + && script_metadata(&*db, file).is_some() + { + tracing::debug!( + "Ignoring implicitly discovered PEP 723 script `{path}` \ + while uv integration is enabled.", + path = entry.path() + ); + return WalkState::Skip; + } + files.lock().unwrap().push(file); } }