diff --git a/Cargo.lock b/Cargo.lock index a656a3f25c..4cc92e1c76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1900,6 +1900,7 @@ dependencies = [ "ruff_python_ast", "ruff_python_parser", "ruff_python_stdlib", + "ruff_source_file", "ruff_text_size", "rustc-hash 2.0.0", "salsa", @@ -1962,6 +1963,7 @@ dependencies = [ "ruff_cache", "ruff_db", "ruff_python_ast", + "ruff_text_size", "rustc-hash 2.0.0", "salsa", "thiserror", diff --git a/crates/red_knot_python_semantic/Cargo.toml b/crates/red_knot_python_semantic/Cargo.toml index 4694c9c3a6..1019ce9434 100644 --- a/crates/red_knot_python_semantic/Cargo.toml +++ b/crates/red_knot_python_semantic/Cargo.toml @@ -15,6 +15,7 @@ ruff_db = { workspace = true } ruff_index = { workspace = true } ruff_python_ast = { workspace = true } ruff_python_stdlib = { workspace = true } +ruff_source_file = { workspace = true } ruff_text_size = { workspace = true } anyhow = { workspace = true } diff --git a/crates/red_knot_python_semantic/src/semantic_model.rs b/crates/red_knot_python_semantic/src/semantic_model.rs index ee7b571e22..6b76b42b7c 100644 --- a/crates/red_knot_python_semantic/src/semantic_model.rs +++ b/crates/red_knot_python_semantic/src/semantic_model.rs @@ -1,6 +1,8 @@ -use ruff_db::files::File; +use ruff_db::files::{File, FilePath}; +use ruff_db::source::line_index; use ruff_python_ast as ast; use ruff_python_ast::{Expr, ExpressionRef, StmtClassDef}; +use ruff_source_file::LineIndex; use crate::module_name::ModuleName; use crate::module_resolver::{resolve_module, Module}; @@ -25,6 +27,14 @@ impl<'db> SemanticModel<'db> { self.db } + pub fn file_path(&self) -> &FilePath { + self.file.path(self.db) + } + + pub fn line_index(&self) -> LineIndex { + line_index(self.db.upcast(), self.file) + } + pub fn resolve_module(&self, module_name: ModuleName) -> Option { resolve_module(self.db, module_name) } diff --git a/crates/red_knot_wasm/tests/api.rs b/crates/red_knot_wasm/tests/api.rs index 66b418d038..36eda60f06 100644 --- a/crates/red_knot_wasm/tests/api.rs +++ b/crates/red_knot_wasm/tests/api.rs @@ -17,5 +17,5 @@ fn check() { let result = workspace.check_file(&test).expect("Check to succeed"); - assert_eq!(result, vec!["Unresolved import 'random22'"]); + assert_eq!(result, vec!["/test.py:1:8: Unresolved import 'random22'"]); } diff --git a/crates/red_knot_workspace/Cargo.toml b/crates/red_knot_workspace/Cargo.toml index d8d5203f6d..ba7c8bfaa3 100644 --- a/crates/red_knot_workspace/Cargo.toml +++ b/crates/red_knot_workspace/Cargo.toml @@ -17,6 +17,7 @@ red_knot_python_semantic = { workspace = true } ruff_cache = { workspace = true } ruff_db = { workspace = true, features = ["os", "cache"] } ruff_python_ast = { workspace = true } +ruff_text_size = { workspace = true } anyhow = { workspace = true } crossbeam = { workspace = true } diff --git a/crates/red_knot_workspace/src/lint.rs b/crates/red_knot_workspace/src/lint.rs index c50cebbbe1..dc16a0bccf 100644 --- a/crates/red_knot_workspace/src/lint.rs +++ b/crates/red_knot_workspace/src/lint.rs @@ -8,9 +8,10 @@ use red_knot_python_semantic::types::Type; use red_knot_python_semantic::{HasTy, ModuleName, SemanticModel}; use ruff_db::files::File; use ruff_db::parsed::{parsed_module, ParsedModule}; -use ruff_db::source::{source_text, SourceText}; +use ruff_db::source::{line_index, source_text, SourceText}; use ruff_python_ast as ast; use ruff_python_ast::visitor::{walk_expr, walk_stmt, Visitor}; +use ruff_text_size::{Ranged, TextSize}; use crate::db::Db; @@ -49,7 +50,18 @@ pub(crate) fn lint_syntax(db: &dyn Db, file_id: File) -> Diagnostics { visitor.visit_body(&ast.body); diagnostics = visitor.diagnostics; } else { - diagnostics.extend(parsed.errors().iter().map(ToString::to_string)); + let path = file_id.path(db); + let line_index = line_index(db.upcast(), file_id); + diagnostics.extend(parsed.errors().iter().map(|err| { + let source_location = line_index.source_location(err.location.start(), source.as_str()); + format!( + "{}:{}:{}: {}", + path.as_str(), + source_location.row, + source_location.column, + err, + ) + })); } Diagnostics::from(diagnostics) @@ -97,6 +109,20 @@ pub fn lint_semantic(db: &dyn Db, file_id: File) -> Diagnostics { Diagnostics::from(context.diagnostics.take()) } +fn format_diagnostic(context: &SemanticLintContext, message: &str, start: TextSize) -> String { + let source_location = context + .semantic + .line_index() + .source_location(start, context.source_text()); + format!( + "{}:{}:{}: {}", + context.semantic.file_path().as_str(), + source_location.row, + source_location.column, + message, + ) +} + fn lint_unresolved_imports(context: &SemanticLintContext, import: AnyImportRef) { match import { AnyImportRef::Import(import) => { @@ -104,7 +130,11 @@ fn lint_unresolved_imports(context: &SemanticLintContext, import: AnyImportRef) let ty = alias.ty(&context.semantic); if ty.is_unbound() { - context.push_diagnostic(format!("Unresolved import '{}'", &alias.name)); + context.push_diagnostic(format_diagnostic( + context, + &format!("Unresolved import '{}'", &alias.name), + alias.start(), + )); } } } @@ -113,7 +143,11 @@ fn lint_unresolved_imports(context: &SemanticLintContext, import: AnyImportRef) let ty = alias.ty(&context.semantic); if ty.is_unbound() { - context.push_diagnostic(format!("Unresolved import '{}'", &alias.name)); + context.push_diagnostic(format_diagnostic( + context, + &format!("Unresolved import '{}'", &alias.name), + alias.start(), + )); } } } @@ -127,12 +161,17 @@ fn lint_maybe_undefined(context: &SemanticLintContext, name: &ast::ExprName) { let semantic = &context.semantic; match name.ty(semantic) { Type::Unbound => { - context.push_diagnostic(format!("Name '{}' used when not defined.", &name.id)); + context.push_diagnostic(format_diagnostic( + context, + &format!("Name '{}' used when not defined.", &name.id), + name.start(), + )); } Type::Union(union) if union.contains(semantic.db(), Type::Unbound) => { - context.push_diagnostic(format!( - "Name '{}' used when possibly not defined.", - &name.id + context.push_diagnostic(format_diagnostic( + context, + &format!("Name '{}' used when possibly not defined.", &name.id), + name.start(), )); } _ => {} @@ -303,6 +342,15 @@ enum AnyImportRef<'a> { ImportFrom(&'a ast::StmtImportFrom), } +impl Ranged for AnyImportRef<'_> { + fn range(&self) -> ruff_text_size::TextRange { + match self { + AnyImportRef::Import(import) => import.range(), + AnyImportRef::ImportFrom(import) => import.range(), + } + } +} + #[cfg(test)] mod tests { use red_knot_python_semantic::{Program, ProgramSettings, PythonVersion, SearchPathSettings}; @@ -364,8 +412,8 @@ mod tests { assert_eq!( *messages, vec![ - "Name 'flag' used when not defined.", - "Name 'y' used when possibly not defined." + "/src/a.py:3:4: Name 'flag' used when not defined.", + "/src/a.py:5:1: Name 'y' used when possibly not defined." ] ); }