mirror of
				https://github.com/astral-sh/ruff.git
				synced 2025-10-31 20:08:19 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			276 lines
		
	
	
	
		
			8.3 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			276 lines
		
	
	
	
		
			8.3 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use anyhow::{Context, anyhow};
 | |
| use ruff_db::Upcast;
 | |
| 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| {
 | |
|             if relative_path.file_name() == Some("types.pyi") {
 | |
|                 println!(
 | |
|                     "Skipping {relative_path:?}: paths with `types.pyi` as their final segment cause a stack overflow"
 | |
|                 );
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             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: Some(PythonVersionWithSource {
 | |
|                     version: PythonVersion::latest_ty(),
 | |
|                     source: PythonVersionSource::default(),
 | |
|                 }),
 | |
|                 python_platform: PythonPlatform::default(),
 | |
|                 search_paths: SearchPathSettings::new(vec![]),
 | |
|             },
 | |
|         )
 | |
|         .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)
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl Upcast<dyn ruff_db::Db> for CorpusDb {
 | |
|     fn upcast(&self) -> &(dyn ruff_db::Db + 'static) {
 | |
|         self
 | |
|     }
 | |
|     fn upcast_mut(&mut self) -> &mut (dyn ruff_db::Db + 'static) {
 | |
|         self
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[salsa::db]
 | |
| impl ty_python_semantic::Db for CorpusDb {
 | |
|     fn is_file_open(&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 {}
 | 
