mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 10:49:50 +00:00

## Summary This PR updates the server to keep track of open files both system and virtual files. This is done by updating the project by adding the file in the open file set in `didOpen` notification and removing it in `didClose` notification. This does mean that for workspace diagnostics, ty will only check open files because the behavior of different diagnostic builder is to first check `is_file_open` and only add diagnostics for open files. So, this required updating the `is_file_open` model to be `should_check_file` model which validates whether the file needs to be checked based on the `CheckMode`. If the check mode is open files only then it will check whether the file is open. If it's all files then it'll return `true` by default. Closes: astral-sh/ty#619 ## Test Plan ### Before There are two files in the project: `__init__.py` and `diagnostics.py`. In the video, I'm demonstrating the old behavior where making changes to the (open) `diagnostics.py` file results in re-parsing the file: https://github.com/user-attachments/assets/c2ac0ecd-9c77-42af-a924-c3744b146045 ### After Same setup as above. In the video, I'm demonstrating the new behavior where making changes to the (open) `diagnostics.py` file doesn't result in re-parting the file: https://github.com/user-attachments/assets/7b82fe92-f330-44c7-b527-c841c4545f8f
261 lines
7.9 KiB
Rust
261 lines
7.9 KiB
Rust
use anyhow::{Context, anyhow};
|
|
use ruff_db::Db;
|
|
use ruff_db::files::{File, Files, system_path_to_file};
|
|
use ruff_db::system::{DbWithTestSystem, System, SystemPath, SystemPathBuf, TestSystem};
|
|
use ruff_db::vendored::VendoredFileSystem;
|
|
use ruff_python_ast::PythonVersion;
|
|
|
|
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
|
|
use ty_python_semantic::pull_types::pull_types;
|
|
use ty_python_semantic::{
|
|
Program, ProgramSettings, PythonPlatform, PythonVersionSource, PythonVersionWithSource,
|
|
SearchPathSettings, default_lint_registry,
|
|
};
|
|
|
|
use test_case::test_case;
|
|
|
|
fn get_cargo_workspace_root() -> anyhow::Result<SystemPathBuf> {
|
|
Ok(SystemPathBuf::from(String::from_utf8(
|
|
std::process::Command::new("cargo")
|
|
.args(["locate-project", "--workspace", "--message-format", "plain"])
|
|
.output()?
|
|
.stdout,
|
|
)?)
|
|
.parent()
|
|
.unwrap()
|
|
.to_owned())
|
|
}
|
|
|
|
/// Test that all snippets in testcorpus can be checked without panic (except for [`KNOWN_FAILURES`])
|
|
#[test]
|
|
fn corpus_no_panic() -> anyhow::Result<()> {
|
|
let crate_root = String::from(env!("CARGO_MANIFEST_DIR"));
|
|
run_corpus_tests(&format!("{crate_root}/resources/corpus/**/*.py"))
|
|
}
|
|
|
|
#[test]
|
|
fn parser_no_panic() -> anyhow::Result<()> {
|
|
let workspace_root = get_cargo_workspace_root()?;
|
|
run_corpus_tests(&format!(
|
|
"{workspace_root}/crates/ruff_python_parser/resources/**/*.py"
|
|
))
|
|
}
|
|
|
|
#[test_case("a-e")]
|
|
#[test_case("f")]
|
|
#[test_case("g-o")]
|
|
#[test_case("p")]
|
|
#[test_case("q-z")]
|
|
#[test_case("!a-z")]
|
|
fn linter_no_panic(range: &str) -> anyhow::Result<()> {
|
|
let workspace_root = get_cargo_workspace_root()?;
|
|
run_corpus_tests(&format!(
|
|
"{workspace_root}/crates/ruff_linter/resources/test/fixtures/[{range}]*/**/*.py"
|
|
))
|
|
}
|
|
|
|
#[test]
|
|
fn linter_stubs_no_panic() -> anyhow::Result<()> {
|
|
let workspace_root = get_cargo_workspace_root()?;
|
|
run_corpus_tests(&format!(
|
|
"{workspace_root}/crates/ruff_linter/resources/test/fixtures/**/*.pyi"
|
|
))
|
|
}
|
|
|
|
#[test_case("a-e")]
|
|
#[test_case("f-k")]
|
|
#[test_case("l-p")]
|
|
#[test_case("q-z")]
|
|
#[test_case("!a-z")]
|
|
fn typeshed_no_panic(range: &str) -> anyhow::Result<()> {
|
|
let workspace_root = get_cargo_workspace_root()?;
|
|
run_corpus_tests(&format!(
|
|
"{workspace_root}/crates/ty_vendored/vendor/typeshed/stdlib/[{range}]*.pyi"
|
|
))
|
|
}
|
|
|
|
#[expect(clippy::print_stdout)]
|
|
fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> {
|
|
let root = SystemPathBuf::from("/src");
|
|
|
|
let mut db = CorpusDb::new();
|
|
db.memory_file_system()
|
|
.create_directory_all(root.as_ref())?;
|
|
|
|
let workspace_root = get_cargo_workspace_root()?;
|
|
let workspace_root = workspace_root.to_string();
|
|
|
|
let corpus = glob::glob(pattern).context("Failed to compile pattern")?;
|
|
|
|
for path in corpus {
|
|
let path = path.context("Failed to glob path")?;
|
|
let path = SystemPathBuf::from_path_buf(path).map_err(|path| {
|
|
anyhow!(
|
|
"Failed to convert path '{path}' to system path",
|
|
path = path.display()
|
|
)
|
|
})?;
|
|
|
|
let relative_path = path.strip_prefix(&workspace_root)?;
|
|
|
|
let (py_expected_to_fail, pyi_expected_to_fail) = KNOWN_FAILURES
|
|
.iter()
|
|
.find_map(|(path, py_fail, pyi_fail)| {
|
|
if *path == relative_path.as_str().replace('\\', "/") {
|
|
Some((*py_fail, *pyi_fail))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.unwrap_or((false, false));
|
|
|
|
let source = path.as_path();
|
|
let source_filename = source.file_name().unwrap();
|
|
|
|
let code = std::fs::read_to_string(source)
|
|
.with_context(|| format!("Failed to read test file: {path}"))?;
|
|
|
|
let mut check_with_file_name = |path: &SystemPath| {
|
|
db.memory_file_system().write_file_all(path, &code).unwrap();
|
|
File::sync_path(&mut db, path);
|
|
|
|
// this test is only asserting that we can pull every expression type without a panic
|
|
// (and some non-expressions that clearly define a single type)
|
|
let file = system_path_to_file(&db, path).unwrap();
|
|
|
|
let result = std::panic::catch_unwind(|| pull_types(&db, file));
|
|
|
|
let expected_to_fail = if path.extension().map(|e| e == "pyi").unwrap_or(false) {
|
|
pyi_expected_to_fail
|
|
} else {
|
|
py_expected_to_fail
|
|
};
|
|
if let Err(err) = result {
|
|
if !expected_to_fail {
|
|
println!(
|
|
"Check failed for {relative_path:?}. Consider fixing it or adding it to KNOWN_FAILURES"
|
|
);
|
|
std::panic::resume_unwind(err);
|
|
}
|
|
} else {
|
|
assert!(
|
|
!expected_to_fail,
|
|
"Expected to panic, but did not. Consider removing this path from KNOWN_FAILURES"
|
|
);
|
|
}
|
|
|
|
db.memory_file_system().remove_file(path).unwrap();
|
|
file.sync(&mut db);
|
|
};
|
|
|
|
if source.extension() == Some("pyi") {
|
|
println!("checking {relative_path}");
|
|
let pyi_dest = root.join(source_filename);
|
|
check_with_file_name(&pyi_dest);
|
|
} else {
|
|
println!("checking {relative_path}");
|
|
let py_dest = root.join(source_filename);
|
|
check_with_file_name(&py_dest);
|
|
|
|
let pyi_dest = root.join(format!("{source_filename}i"));
|
|
println!("re-checking as stub file: {pyi_dest}");
|
|
check_with_file_name(&pyi_dest);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Whether or not the .py/.pyi version of this file is expected to fail
|
|
#[rustfmt::skip]
|
|
const KNOWN_FAILURES: &[(&str, bool, bool)] = &[
|
|
// Fails with too-many-cycle-iterations due to a self-referential
|
|
// type alias, see https://github.com/astral-sh/ty/issues/256
|
|
("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_34.py", true, true),
|
|
];
|
|
|
|
#[salsa::db]
|
|
#[derive(Clone)]
|
|
pub struct CorpusDb {
|
|
storage: salsa::Storage<Self>,
|
|
files: Files,
|
|
rule_selection: RuleSelection,
|
|
system: TestSystem,
|
|
vendored: VendoredFileSystem,
|
|
}
|
|
|
|
impl CorpusDb {
|
|
#[expect(clippy::new_without_default)]
|
|
pub fn new() -> Self {
|
|
let db = Self {
|
|
storage: salsa::Storage::new(None),
|
|
system: TestSystem::default(),
|
|
vendored: ty_vendored::file_system().clone(),
|
|
rule_selection: RuleSelection::from_registry(default_lint_registry()),
|
|
files: Files::default(),
|
|
};
|
|
|
|
Program::from_settings(
|
|
&db,
|
|
ProgramSettings {
|
|
python_version: PythonVersionWithSource {
|
|
version: PythonVersion::latest_ty(),
|
|
source: PythonVersionSource::default(),
|
|
},
|
|
python_platform: PythonPlatform::default(),
|
|
search_paths: SearchPathSettings::new(vec![])
|
|
.to_search_paths(db.system(), db.vendored())
|
|
.unwrap(),
|
|
},
|
|
);
|
|
|
|
db
|
|
}
|
|
}
|
|
|
|
impl DbWithTestSystem for CorpusDb {
|
|
fn test_system(&self) -> &TestSystem {
|
|
&self.system
|
|
}
|
|
|
|
fn test_system_mut(&mut self) -> &mut TestSystem {
|
|
&mut self.system
|
|
}
|
|
}
|
|
|
|
#[salsa::db]
|
|
impl ruff_db::Db for CorpusDb {
|
|
fn vendored(&self) -> &VendoredFileSystem {
|
|
&self.vendored
|
|
}
|
|
|
|
fn system(&self) -> &dyn System {
|
|
&self.system
|
|
}
|
|
|
|
fn files(&self) -> &Files {
|
|
&self.files
|
|
}
|
|
|
|
fn python_version(&self) -> PythonVersion {
|
|
Program::get(self).python_version(self)
|
|
}
|
|
}
|
|
|
|
#[salsa::db]
|
|
impl ty_python_semantic::Db for CorpusDb {
|
|
fn should_check_file(&self, file: File) -> bool {
|
|
!file.path(self).is_vendored_path()
|
|
}
|
|
|
|
fn rule_selection(&self, _file: File) -> &RuleSelection {
|
|
&self.rule_selection
|
|
}
|
|
|
|
fn lint_registry(&self) -> &LintRegistry {
|
|
default_lint_registry()
|
|
}
|
|
}
|
|
|
|
#[salsa::db]
|
|
impl salsa::Database for CorpusDb {}
|