use std::{
fmt,
sync::{Arc, Mutex},
};
use ruff_db::{
diagnostic::{
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, DisplayDiagnostics,
UnifiedFile,
},
files::File,
system::SystemPathBuf,
};
use ruff_text_size::{TextRange, TextSize};
use ty_python_semantic::check_file_unwrap;
use crate::{
db::SRC_ROOT,
pool::{PooledMemoryDb, to_string},
};
pub struct SourceFile<'a> {
pub source_code: &'a str,
pub path: &'a str,
}
impl<'a> SourceFile<'a> {
#[must_use]
pub fn new(source_code: &'a str, path: &'a str) -> Self {
Self { source_code, path }
}
}
pub fn type_check(
python_source: &SourceFile<'_>,
stubs_file: Option<&SourceFile<'_>>,
) -> Result<Option<TypeCheckingDiagnostics>, String> {
let mut pooled_db = PooledMemoryDb::checkout()?;
let src_root = SystemPathBuf::from(SRC_ROOT);
let main_path = src_root.join(python_source.path);
let main_source = python_source.source_code;
let (main_file, code_offset): (File, u32) = if let Some(stubs_file) = stubs_file {
let stubs_path = src_root.join(stubs_file.path);
pooled_db.write_root_file(&stubs_path, stubs_file.source_code)?;
let stub_stem = stubs_file
.path
.split_once('.')
.map_or(stubs_file.path, |(before, _)| before);
let mut new_source = format!("from {stub_stem} import *\n");
let offset = u32::try_from(new_source.len()).map_err(to_string)?;
new_source.push_str(main_source);
let main_file = pooled_db.write_root_file(&main_path, &new_source)?;
(main_file, offset)
} else {
let main_file = pooled_db.write_root_file(&main_path, main_source)?;
(main_file, 0)
};
let mut diagnostics = check_file_unwrap(pooled_db.db(), main_file);
diagnostics.retain(filter_diagnostics);
if diagnostics.is_empty() {
Ok(None)
} else {
pooled_db.rewrite_root_file(&main_path, main_source)?;
if code_offset > 0 {
let offset = TextSize::new(code_offset);
for diagnostic in &mut diagnostics {
for ann in diagnostic.annotations_mut() {
adjust_annotation_span(ann, main_file, offset);
}
for sub in diagnostic.sub_diagnostics_mut() {
for ann in sub.annotations_mut() {
adjust_annotation_span(ann, main_file, offset);
}
}
}
}
let db = pooled_db.db();
diagnostics.sort_by(|a, b| a.rendering_sort_key(db).cmp(&b.rendering_sort_key(db)));
Ok(Some(TypeCheckingDiagnostics::new(diagnostics, pooled_db)))
}
}
fn adjust_annotation_span(ann: &mut Annotation, main_file: File, offset: TextSize) {
let span = ann.get_span();
if let UnifiedFile::Ty(span_file) = span.file()
&& *span_file == main_file
&& let Some(range) = span.range()
{
let new_range = TextRange::new(range.start() - offset, range.end() - offset);
let new_span = span.clone().with_range(new_range);
ann.set_span(new_span);
}
}
#[derive(Clone)]
pub struct TypeCheckingDiagnostics {
diagnostics: Vec<Diagnostic>,
pooled_db: Arc<Mutex<PooledMemoryDb>>,
format: DiagnosticFormat,
color: bool,
}
impl fmt::Debug for TypeCheckingDiagnostics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let config = self.config();
let pooled_db = self.pooled_db.lock().unwrap();
write!(
f,
"TypeCheckingDiagnostics:\n{}",
DisplayDiagnostics::new(pooled_db.db(), &config, &self.diagnostics)
)
}
}
#[derive(Debug)]
#[expect(dead_code)]
pub struct DebugTypeCheckingDiagnostics<'a> {
diagnostics: &'a [Diagnostic],
pooled_db: Arc<Mutex<PooledMemoryDb>>,
format: DiagnosticFormat,
color: bool,
}
impl fmt::Display for TypeCheckingDiagnostics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let pooled_db = self.pooled_db.lock().unwrap();
DisplayDiagnostics::new(pooled_db.db(), &self.config(), &self.diagnostics).fmt(f)
}
}
impl TypeCheckingDiagnostics {
fn new(diagnostics: Vec<Diagnostic>, pooled_db: PooledMemoryDb) -> Self {
Self {
diagnostics,
pooled_db: Arc::new(Mutex::new(pooled_db)),
format: DiagnosticFormat::Full,
color: false,
}
}
fn config(&self) -> DisplayDiagnosticConfig {
DisplayDiagnosticConfig::new("monty")
.format(self.format)
.color(self.color)
}
#[must_use]
pub fn debug_details(&self) -> DebugTypeCheckingDiagnostics<'_> {
DebugTypeCheckingDiagnostics {
diagnostics: &self.diagnostics,
pooled_db: self.pooled_db.clone(),
format: self.format,
color: self.color,
}
}
#[must_use]
pub fn format(self, format: DiagnosticFormat) -> Self {
Self { format, ..self }
}
pub fn format_from_str(self, format: &str) -> Result<Self, String> {
let format = match format.to_ascii_lowercase().as_str() {
"full" => DiagnosticFormat::Full,
"concise" => DiagnosticFormat::Concise,
"azure" => DiagnosticFormat::Azure,
"json" => DiagnosticFormat::Json,
"jsonlines" | "json-lines" => DiagnosticFormat::JsonLines,
"rdjson" => DiagnosticFormat::Rdjson,
"pylint" => DiagnosticFormat::Pylint,
"gitlab" => DiagnosticFormat::Gitlab,
"github" => DiagnosticFormat::Github,
_ => return Err(format!("Unknown format: {format}")),
};
Ok(Self { format, ..self })
}
#[must_use]
pub fn color(self, color: bool) -> Self {
Self { color, ..self }
}
}
fn filter_diagnostics(d: &Diagnostic) -> bool {
!(matches!(d.id(), DiagnosticId::InvalidSyntax)
&& matches!(
d.primary_message(),
"`await` statement outside of a function" | "`await` outside of an asynchronous function"
))
}