[internal] Return Messages from check_path (#16837)

Summary
--

This PR updates `check_path` in the `ruff_linter` crate to return a
`Vec<Message>` instead of a `Vec<Diagnostic>`. The main motivation for
this is to make it easier to convert semantic syntax errors directly
into `Message`s rather than `Diagnostic`s in #16106. However, this also
has the benefit of keeping the preview check on unsupported syntax
errors in `check_path`, as suggested in
https://github.com/astral-sh/ruff/pull/16429#discussion_r1974748024.

All of the interesting changes are in the first commit. The second
commit just renames variables like `diagnostics` to `messages`, and the
third commit is a tiny import fix.

I also updated the `ExpandedMessage::location` field name, which caused
a few extra commits tidying up the playground code. I thought it was
nicely symmetric with `end_location`, but I'm happy to revert that too.

Test Plan
--

Existing tests. I also tested the playground and server manually.
This commit is contained in:
Brent Westbrook 2025-03-19 10:08:07 -04:00 committed by GitHub
parent f2a9960fb3
commit 22de00de16
13 changed files with 280 additions and 294 deletions

View file

@ -353,6 +353,7 @@ impl FileCache {
fix: msg.fix.clone(), fix: msg.fix.clone(),
file: file.clone(), file: file.clone(),
noqa_offset: msg.noqa_offset, noqa_offset: msg.noqa_offset,
parent: msg.parent,
}) })
}) })
.collect() .collect()
@ -445,6 +446,7 @@ impl LintCacheData {
CacheMessage { CacheMessage {
kind: msg.kind.clone(), kind: msg.kind.clone(),
range: msg.range, range: msg.range,
parent: msg.parent,
fix: msg.fix.clone(), fix: msg.fix.clone(),
noqa_offset: msg.noqa_offset, noqa_offset: msg.noqa_offset,
} }
@ -465,6 +467,7 @@ pub(super) struct CacheMessage {
kind: DiagnosticKind, kind: DiagnosticKind,
/// Range into the message's [`FileCache::source`]. /// Range into the message's [`FileCache::source`].
range: TextRange, range: TextRange,
parent: Option<TextSize>,
fix: Option<Fix>, fix: Option<Fix>,
noqa_offset: TextSize, noqa_offset: TextSize,
} }
@ -702,7 +705,10 @@ mod tests {
.unwrap(); .unwrap();
} }
assert_eq!(expected_diagnostics, got_diagnostics); assert_eq!(
expected_diagnostics, got_diagnostics,
"left == {expected_diagnostics:#?}, right == {got_diagnostics:#?}",
);
} }
#[test] #[test]

View file

@ -592,6 +592,7 @@ fn all_lines_fit(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use ruff_source_file::SourceFileBuilder;
use test_case::test_case; use test_case::test_case;
use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_diagnostics::{Diagnostic, Edit, Fix};
@ -604,6 +605,7 @@ mod tests {
use crate::fix::edits::{ use crate::fix::edits::{
add_to_dunder_all, make_redundant_alias, next_stmt_break, trailing_semicolon, add_to_dunder_all, make_redundant_alias, next_stmt_break, trailing_semicolon,
}; };
use crate::message::DiagnosticMessage;
use crate::Locator; use crate::Locator;
/// Parse the given source using [`Mode::Module`] and return the first statement. /// Parse the given source using [`Mode::Module`] and return the first statement.
@ -735,14 +737,22 @@ x = 1 \
let diag = { let diag = {
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile; use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
let mut iter = edits.into_iter(); let mut iter = edits.into_iter();
Diagnostic::new( let diag = Diagnostic::new(
MissingNewlineAtEndOfFile, // The choice of rule here is arbitrary. MissingNewlineAtEndOfFile, // The choice of rule here is arbitrary.
TextRange::default(), TextRange::default(),
) )
.with_fix(Fix::safe_edits( .with_fix(Fix::safe_edits(
iter.next().ok_or(anyhow!("expected edits nonempty"))?, iter.next().ok_or(anyhow!("expected edits nonempty"))?,
iter, iter,
)) ));
DiagnosticMessage {
kind: diag.kind,
range: diag.range,
fix: diag.fix,
parent: diag.parent,
file: SourceFileBuilder::new("<filename>", "<code>").finish(),
noqa_offset: TextSize::default(),
}
}; };
assert_eq!(apply_fixes([diag].iter(), &locator).code, expect); assert_eq!(apply_fixes([diag].iter(), &locator).code, expect);
Ok(()) Ok(())

View file

@ -3,10 +3,11 @@ use std::collections::BTreeSet;
use itertools::Itertools; use itertools::Itertools;
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use ruff_diagnostics::{Diagnostic, Edit, Fix, IsolationLevel, SourceMap}; use ruff_diagnostics::{Edit, Fix, IsolationLevel, SourceMap};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::linter::FixTable; use crate::linter::FixTable;
use crate::message::{DiagnosticMessage, Message};
use crate::registry::{AsRule, Rule}; use crate::registry::{AsRule, Rule};
use crate::settings::types::UnsafeFixes; use crate::settings::types::UnsafeFixes;
use crate::Locator; use crate::Locator;
@ -26,16 +27,17 @@ pub(crate) struct FixResult {
/// Fix errors in a file, and write the fixed source code to disk. /// Fix errors in a file, and write the fixed source code to disk.
pub(crate) fn fix_file( pub(crate) fn fix_file(
diagnostics: &[Diagnostic], messages: &[Message],
locator: &Locator, locator: &Locator,
unsafe_fixes: UnsafeFixes, unsafe_fixes: UnsafeFixes,
) -> Option<FixResult> { ) -> Option<FixResult> {
let required_applicability = unsafe_fixes.required_applicability(); let required_applicability = unsafe_fixes.required_applicability();
let mut with_fixes = diagnostics let mut with_fixes = messages
.iter() .iter()
.filter(|diagnostic| { .filter_map(Message::as_diagnostic_message)
diagnostic .filter(|message| {
message
.fix .fix
.as_ref() .as_ref()
.is_some_and(|fix| fix.applies(required_applicability)) .is_some_and(|fix| fix.applies(required_applicability))
@ -51,7 +53,7 @@ pub(crate) fn fix_file(
/// Apply a series of fixes. /// Apply a series of fixes.
fn apply_fixes<'a>( fn apply_fixes<'a>(
diagnostics: impl Iterator<Item = &'a Diagnostic>, diagnostics: impl Iterator<Item = &'a DiagnosticMessage>,
locator: &'a Locator<'a>, locator: &'a Locator<'a>,
) -> FixResult { ) -> FixResult {
let mut output = String::with_capacity(locator.len()); let mut output = String::with_capacity(locator.len());
@ -161,22 +163,30 @@ fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Orderi
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use ruff_diagnostics::{Diagnostic, Edit, Fix, SourceMarker}; use ruff_diagnostics::{Edit, Fix, SourceMarker};
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{Ranged, TextSize}; use ruff_text_size::{Ranged, TextSize};
use crate::fix::{apply_fixes, FixResult}; use crate::fix::{apply_fixes, FixResult};
use crate::message::DiagnosticMessage;
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile; use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
use crate::Locator; use crate::Locator;
#[allow(deprecated)] #[allow(deprecated)]
fn create_diagnostics(edit: impl IntoIterator<Item = Edit>) -> Vec<Diagnostic> { fn create_diagnostics(
filename: &str,
source: &str,
edit: impl IntoIterator<Item = Edit>,
) -> Vec<DiagnosticMessage> {
edit.into_iter() edit.into_iter()
.map(|edit| Diagnostic { .map(|edit| DiagnosticMessage {
// The choice of rule here is arbitrary. // The choice of rule here is arbitrary.
kind: MissingNewlineAtEndOfFile.into(), kind: MissingNewlineAtEndOfFile.into(),
range: edit.range(), range: edit.range(),
fix: Some(Fix::safe_edit(edit)), fix: Some(Fix::safe_edit(edit)),
parent: None, parent: None,
file: SourceFileBuilder::new(filename, source).finish(),
noqa_offset: TextSize::default(),
}) })
.collect() .collect()
} }
@ -184,7 +194,7 @@ mod tests {
#[test] #[test]
fn empty_file() { fn empty_file() {
let locator = Locator::new(r""); let locator = Locator::new(r"");
let diagnostics = create_diagnostics([]); let diagnostics = create_diagnostics("<filename>", locator.contents(), []);
let FixResult { let FixResult {
code, code,
fixes, fixes,
@ -205,10 +215,14 @@ print("hello world")
"# "#
.trim(), .trim(),
); );
let diagnostics = create_diagnostics([Edit::insertion( let diagnostics = create_diagnostics(
"import sys\n".to_string(), "<filename>",
TextSize::new(10), locator.contents(),
)]); [Edit::insertion(
"import sys\n".to_string(),
TextSize::new(10),
)],
);
let FixResult { let FixResult {
code, code,
fixes, fixes,
@ -243,11 +257,15 @@ class A(object):
" "
.trim(), .trim(),
); );
let diagnostics = create_diagnostics([Edit::replacement( let diagnostics = create_diagnostics(
"Bar".to_string(), "<filename>",
TextSize::new(8), locator.contents(),
TextSize::new(14), [Edit::replacement(
)]); "Bar".to_string(),
TextSize::new(8),
TextSize::new(14),
)],
);
let FixResult { let FixResult {
code, code,
fixes, fixes,
@ -280,7 +298,11 @@ class A(object):
" "
.trim(), .trim(),
); );
let diagnostics = create_diagnostics([Edit::deletion(TextSize::new(7), TextSize::new(15))]); let diagnostics = create_diagnostics(
"<filename>",
locator.contents(),
[Edit::deletion(TextSize::new(7), TextSize::new(15))],
);
let FixResult { let FixResult {
code, code,
fixes, fixes,
@ -313,10 +335,14 @@ class A(object, object, object):
" "
.trim(), .trim(),
); );
let diagnostics = create_diagnostics([ let diagnostics = create_diagnostics(
Edit::deletion(TextSize::from(8), TextSize::from(16)), "<filename>",
Edit::deletion(TextSize::from(22), TextSize::from(30)), locator.contents(),
]); [
Edit::deletion(TextSize::from(8), TextSize::from(16)),
Edit::deletion(TextSize::from(22), TextSize::from(30)),
],
);
let FixResult { let FixResult {
code, code,
fixes, fixes,
@ -352,10 +378,14 @@ class A(object):
" "
.trim(), .trim(),
); );
let diagnostics = create_diagnostics([ let diagnostics = create_diagnostics(
Edit::deletion(TextSize::from(7), TextSize::from(15)), "<filename>",
Edit::replacement("ignored".to_string(), TextSize::from(9), TextSize::from(11)), locator.contents(),
]); [
Edit::deletion(TextSize::from(7), TextSize::from(15)),
Edit::replacement("ignored".to_string(), TextSize::from(9), TextSize::from(11)),
],
);
let FixResult { let FixResult {
code, code,
fixes, fixes,

View file

@ -58,8 +58,7 @@ pub struct FixerResult<'a> {
pub fixed: FixTable, pub fixed: FixTable,
} }
/// Generate `Diagnostic`s from the source code contents at the /// Generate [`Message`]s from the source code contents at the given `Path`.
/// given `Path`.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn check_path( pub fn check_path(
path: &Path, path: &Path,
@ -74,7 +73,7 @@ pub fn check_path(
source_type: PySourceType, source_type: PySourceType,
parsed: &Parsed<ModModule>, parsed: &Parsed<ModModule>,
target_version: PythonVersion, target_version: PythonVersion,
) -> Vec<Diagnostic> { ) -> Vec<Message> {
// Aggregate all diagnostics. // Aggregate all diagnostics.
let mut diagnostics = vec![]; let mut diagnostics = vec![];
@ -322,7 +321,20 @@ pub fn check_path(
} }
} }
diagnostics let syntax_errors = if settings.preview.is_enabled() {
parsed.unsupported_syntax_errors()
} else {
&[]
};
diagnostics_to_messages(
diagnostics,
parsed.errors(),
syntax_errors,
path,
locator,
directives,
)
} }
const MAX_ITERATIONS: usize = 100; const MAX_ITERATIONS: usize = 100;
@ -357,7 +369,7 @@ pub fn add_noqa_to_path(
); );
// Generate diagnostics, ignoring any existing `noqa` directives. // Generate diagnostics, ignoring any existing `noqa` directives.
let diagnostics = check_path( let messages = check_path(
path, path,
package, package,
&locator, &locator,
@ -376,7 +388,7 @@ pub fn add_noqa_to_path(
// TODO(dhruvmanila): Add support for Jupyter Notebooks // TODO(dhruvmanila): Add support for Jupyter Notebooks
add_noqa( add_noqa(
path, path,
&diagnostics, &messages,
&locator, &locator,
indexer.comment_ranges(), indexer.comment_ranges(),
&settings.external, &settings.external,
@ -417,7 +429,7 @@ pub fn lint_only(
); );
// Generate diagnostics. // Generate diagnostics.
let diagnostics = check_path( let messages = check_path(
path, path,
package, package,
&locator, &locator,
@ -432,21 +444,8 @@ pub fn lint_only(
target_version, target_version,
); );
let syntax_errors = if settings.preview.is_enabled() {
parsed.unsupported_syntax_errors()
} else {
&[]
};
LinterResult { LinterResult {
messages: diagnostics_to_messages( messages,
diagnostics,
parsed.errors(),
syntax_errors,
path,
&locator,
&directives,
),
has_syntax_error: parsed.has_syntax_errors(), has_syntax_error: parsed.has_syntax_errors(),
} }
} }
@ -532,7 +531,7 @@ pub fn lint_fix<'a>(
); );
// Generate diagnostics. // Generate diagnostics.
let diagnostics = check_path( let messages = check_path(
path, path,
package, package,
&locator, &locator,
@ -571,7 +570,7 @@ pub fn lint_fix<'a>(
code: fixed_contents, code: fixed_contents,
fixes: applied, fixes: applied,
source_map, source_map,
}) = fix_file(&diagnostics, &locator, unsafe_fixes) }) = fix_file(&messages, &locator, unsafe_fixes)
{ {
if iterations < MAX_ITERATIONS { if iterations < MAX_ITERATIONS {
// Count the number of fixed errors. // Count the number of fixed errors.
@ -588,25 +587,12 @@ pub fn lint_fix<'a>(
continue; continue;
} }
report_failed_to_converge_error(path, transformed.source_code(), &diagnostics); report_failed_to_converge_error(path, transformed.source_code(), &messages);
} }
let syntax_errors = if settings.preview.is_enabled() {
parsed.unsupported_syntax_errors()
} else {
&[]
};
return Ok(FixerResult { return Ok(FixerResult {
result: LinterResult { result: LinterResult {
messages: diagnostics_to_messages( messages,
diagnostics,
parsed.errors(),
syntax_errors,
path,
&locator,
&directives,
),
has_syntax_error: !is_valid_syntax, has_syntax_error: !is_valid_syntax,
}, },
transformed, transformed,
@ -625,8 +611,8 @@ fn collect_rule_codes(rules: impl IntoIterator<Item = Rule>) -> String {
} }
#[allow(clippy::print_stderr)] #[allow(clippy::print_stderr)]
fn report_failed_to_converge_error(path: &Path, transformed: &str, diagnostics: &[Diagnostic]) { fn report_failed_to_converge_error(path: &Path, transformed: &str, messages: &[Message]) {
let codes = collect_rule_codes(diagnostics.iter().map(|diagnostic| diagnostic.kind.rule())); let codes = collect_rule_codes(messages.iter().filter_map(Message::rule));
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
eprintln!( eprintln!(
"{}{} Failed to converge after {} iterations in `{}` with rule codes {}:---\n{}\n---", "{}{} Failed to converge after {} iterations in `{}` with rule codes {}:---\n{}\n---",

View file

@ -41,24 +41,25 @@ mod text;
/// Message represents either a diagnostic message corresponding to a rule violation or a syntax /// Message represents either a diagnostic message corresponding to a rule violation or a syntax
/// error message raised by the parser. /// error message raised by the parser.
#[derive(Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum Message { pub enum Message {
Diagnostic(DiagnosticMessage), Diagnostic(DiagnosticMessage),
SyntaxError(SyntaxErrorMessage), SyntaxError(SyntaxErrorMessage),
} }
/// A diagnostic message corresponding to a rule violation. /// A diagnostic message corresponding to a rule violation.
#[derive(Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiagnosticMessage { pub struct DiagnosticMessage {
pub kind: DiagnosticKind, pub kind: DiagnosticKind,
pub range: TextRange, pub range: TextRange,
pub fix: Option<Fix>, pub fix: Option<Fix>,
pub parent: Option<TextSize>,
pub file: SourceFile, pub file: SourceFile,
pub noqa_offset: TextSize, pub noqa_offset: TextSize,
} }
/// A syntax error message raised by the parser. /// A syntax error message raised by the parser.
#[derive(Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct SyntaxErrorMessage { pub struct SyntaxErrorMessage {
pub message: String, pub message: String,
pub range: TextRange, pub range: TextRange,
@ -91,6 +92,7 @@ impl Message {
range: diagnostic.range(), range: diagnostic.range(),
kind: diagnostic.kind, kind: diagnostic.kind,
fix: diagnostic.fix, fix: diagnostic.fix,
parent: diagnostic.parent,
file, file,
noqa_offset, noqa_offset,
}) })
@ -140,6 +142,18 @@ impl Message {
} }
} }
pub fn into_diagnostic_message(self) -> Option<DiagnosticMessage> {
match self {
Message::Diagnostic(m) => Some(m),
Message::SyntaxError(_) => None,
}
}
/// Returns `true` if `self` is a diagnostic message.
pub const fn is_diagnostic_message(&self) -> bool {
matches!(self, Message::Diagnostic(_))
}
/// Returns `true` if `self` is a syntax error message. /// Returns `true` if `self` is a syntax error message.
pub const fn is_syntax_error(&self) -> bool { pub const fn is_syntax_error(&self) -> bool {
matches!(self, Message::SyntaxError(_)) matches!(self, Message::SyntaxError(_))

View file

@ -9,25 +9,26 @@ use anyhow::Result;
use itertools::Itertools; use itertools::Itertools;
use log::warn; use log::warn;
use ruff_diagnostics::{Diagnostic, Edit}; use ruff_diagnostics::Edit;
use ruff_python_trivia::{indentation_at_offset, CommentRanges, Cursor}; use ruff_python_trivia::{indentation_at_offset, CommentRanges, Cursor};
use ruff_source_file::{LineEnding, LineRanges}; use ruff_source_file::{LineEnding, LineRanges};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::codes::NoqaCode; use crate::codes::NoqaCode;
use crate::fs::relativize_path; use crate::fs::relativize_path;
use crate::message::Message;
use crate::registry::{AsRule, Rule, RuleSet}; use crate::registry::{AsRule, Rule, RuleSet};
use crate::rule_redirects::get_redirect_target; use crate::rule_redirects::get_redirect_target;
use crate::Locator; use crate::Locator;
/// Generates an array of edits that matches the length of `diagnostics`. /// Generates an array of edits that matches the length of `messages`.
/// Each potential edit in the array is paired, in order, with the associated diagnostic. /// Each potential edit in the array is paired, in order, with the associated diagnostic.
/// Each edit will add a `noqa` comment to the appropriate line in the source to hide /// Each edit will add a `noqa` comment to the appropriate line in the source to hide
/// the diagnostic. These edits may conflict with each other and should not be applied /// the diagnostic. These edits may conflict with each other and should not be applied
/// simultaneously. /// simultaneously.
pub fn generate_noqa_edits( pub fn generate_noqa_edits(
path: &Path, path: &Path,
diagnostics: &[Diagnostic], messages: &[Message],
locator: &Locator, locator: &Locator,
comment_ranges: &CommentRanges, comment_ranges: &CommentRanges,
external: &[String], external: &[String],
@ -37,7 +38,7 @@ pub fn generate_noqa_edits(
let file_directives = FileNoqaDirectives::extract(locator, comment_ranges, external, path); let file_directives = FileNoqaDirectives::extract(locator, comment_ranges, external, path);
let exemption = FileExemption::from(&file_directives); let exemption = FileExemption::from(&file_directives);
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator); let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator);
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for); let comments = find_noqa_comments(messages, locator, &exemption, &directives, noqa_line_for);
build_noqa_edits_by_diagnostic(comments, locator, line_ending) build_noqa_edits_by_diagnostic(comments, locator, line_ending)
} }
@ -699,10 +700,10 @@ impl Display for LexicalError {
impl Error for LexicalError {} impl Error for LexicalError {}
/// Adds noqa comments to suppress all diagnostics of a file. /// Adds noqa comments to suppress all messages of a file.
pub(crate) fn add_noqa( pub(crate) fn add_noqa(
path: &Path, path: &Path,
diagnostics: &[Diagnostic], messages: &[Message],
locator: &Locator, locator: &Locator,
comment_ranges: &CommentRanges, comment_ranges: &CommentRanges,
external: &[String], external: &[String],
@ -711,7 +712,7 @@ pub(crate) fn add_noqa(
) -> Result<usize> { ) -> Result<usize> {
let (count, output) = add_noqa_inner( let (count, output) = add_noqa_inner(
path, path,
diagnostics, messages,
locator, locator,
comment_ranges, comment_ranges,
external, external,
@ -725,7 +726,7 @@ pub(crate) fn add_noqa(
fn add_noqa_inner( fn add_noqa_inner(
path: &Path, path: &Path,
diagnostics: &[Diagnostic], messages: &[Message],
locator: &Locator, locator: &Locator,
comment_ranges: &CommentRanges, comment_ranges: &CommentRanges,
external: &[String], external: &[String],
@ -740,7 +741,7 @@ fn add_noqa_inner(
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator); let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator);
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for); let comments = find_noqa_comments(messages, locator, &exemption, &directives, noqa_line_for);
let edits = build_noqa_edits_by_line(comments, locator, line_ending); let edits = build_noqa_edits_by_line(comments, locator, line_ending);
@ -776,7 +777,7 @@ fn build_noqa_edits_by_diagnostic(
if let Some(noqa_edit) = generate_noqa_edit( if let Some(noqa_edit) = generate_noqa_edit(
comment.directive, comment.directive,
comment.line, comment.line,
RuleSet::from_rule(comment.diagnostic.kind.rule()), RuleSet::from_rule(comment.rule),
locator, locator,
line_ending, line_ending,
) { ) {
@ -812,7 +813,7 @@ fn build_noqa_edits_by_line<'a>(
offset, offset,
matches matches
.into_iter() .into_iter()
.map(|NoqaComment { diagnostic, .. }| diagnostic.kind.rule()) .map(|NoqaComment { rule, .. }| rule)
.collect(), .collect(),
locator, locator,
line_ending, line_ending,
@ -825,22 +826,27 @@ fn build_noqa_edits_by_line<'a>(
struct NoqaComment<'a> { struct NoqaComment<'a> {
line: TextSize, line: TextSize,
diagnostic: &'a Diagnostic, rule: Rule,
directive: Option<&'a Directive<'a>>, directive: Option<&'a Directive<'a>>,
} }
fn find_noqa_comments<'a>( fn find_noqa_comments<'a>(
diagnostics: &'a [Diagnostic], messages: &'a [Message],
locator: &'a Locator, locator: &'a Locator,
exemption: &'a FileExemption, exemption: &'a FileExemption,
directives: &'a NoqaDirectives, directives: &'a NoqaDirectives,
noqa_line_for: &NoqaMapping, noqa_line_for: &NoqaMapping,
) -> Vec<Option<NoqaComment<'a>>> { ) -> Vec<Option<NoqaComment<'a>>> {
// List of noqa comments, ordered to match up with `diagnostics` // List of noqa comments, ordered to match up with `messages`
let mut comments_by_line: Vec<Option<NoqaComment<'a>>> = vec![]; let mut comments_by_line: Vec<Option<NoqaComment<'a>>> = vec![];
// Mark any non-ignored diagnostics. // Mark any non-ignored diagnostics.
for diagnostic in diagnostics { for message in messages {
let Message::Diagnostic(diagnostic) = message else {
comments_by_line.push(None);
continue;
};
match &exemption { match &exemption {
FileExemption::All(_) => { FileExemption::All(_) => {
// If the file is exempted, don't add any noqa directives. // If the file is exempted, don't add any noqa directives.
@ -876,7 +882,9 @@ fn find_noqa_comments<'a>(
} }
} }
let noqa_offset = noqa_line_for.resolve(diagnostic.start()); let noqa_offset = noqa_line_for.resolve(diagnostic.range.start());
let rule = diagnostic.kind.rule();
// Or ignored by the directive itself? // Or ignored by the directive itself?
if let Some(directive_line) = directives.find_line_with_directive(noqa_offset) { if let Some(directive_line) = directives.find_line_with_directive(noqa_offset) {
@ -886,11 +894,10 @@ fn find_noqa_comments<'a>(
continue; continue;
} }
directive @ Directive::Codes(codes) => { directive @ Directive::Codes(codes) => {
let rule = diagnostic.kind.rule();
if !codes.includes(rule) { if !codes.includes(rule) {
comments_by_line.push(Some(NoqaComment { comments_by_line.push(Some(NoqaComment {
line: directive_line.start(), line: directive_line.start(),
diagnostic, rule,
directive: Some(directive), directive: Some(directive),
})); }));
} }
@ -902,7 +909,7 @@ fn find_noqa_comments<'a>(
// There's no existing noqa directive that suppresses the diagnostic. // There's no existing noqa directive that suppresses the diagnostic.
comments_by_line.push(Some(NoqaComment { comments_by_line.push(Some(NoqaComment {
line: locator.line_start(noqa_offset), line: locator.line_start(noqa_offset),
diagnostic, rule,
directive: None, directive: None,
})); }));
} }
@ -1209,9 +1216,10 @@ mod tests {
use ruff_diagnostics::{Diagnostic, Edit}; use ruff_diagnostics::{Diagnostic, Edit};
use ruff_python_trivia::CommentRanges; use ruff_python_trivia::CommentRanges;
use ruff_source_file::LineEnding; use ruff_source_file::{LineEnding, SourceFileBuilder};
use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::message::Message;
use crate::noqa::{ use crate::noqa::{
add_noqa_inner, lex_codes, lex_file_exemption, lex_inline_noqa, Directive, LexicalError, add_noqa_inner, lex_codes, lex_file_exemption, lex_inline_noqa, Directive, LexicalError,
NoqaLexerOutput, NoqaMapping, NoqaLexerOutput, NoqaMapping,
@ -1236,6 +1244,17 @@ mod tests {
} }
} }
/// Create a [`Message`] with a placeholder filename and rule code from `diagnostic`.
fn message_from_diagnostic(
diagnostic: Diagnostic,
path: impl AsRef<Path>,
source: &str,
) -> Message {
let noqa_offset = diagnostic.start();
let file = SourceFileBuilder::new(path.as_ref().to_string_lossy(), source).finish();
Message::from_diagnostic(diagnostic, file, noqa_offset)
}
#[test] #[test]
fn noqa_lex_codes() { fn noqa_lex_codes() {
let source = " F401,,F402F403 # and so on"; let source = " F401,,F402F403 # and so on";
@ -2816,18 +2835,19 @@ mod tests {
assert_eq!(count, 0); assert_eq!(count, 0);
assert_eq!(output, format!("{contents}")); assert_eq!(output, format!("{contents}"));
let diagnostics = [Diagnostic::new( let messages = [Diagnostic::new(
UnusedVariable { UnusedVariable {
name: "x".to_string(), name: "x".to_string(),
}, },
TextRange::new(TextSize::from(0), TextSize::from(0)), TextRange::new(TextSize::from(0), TextSize::from(0)),
)]; )]
.map(|d| message_from_diagnostic(d, path, contents));
let contents = "x = 1"; let contents = "x = 1";
let noqa_line_for = NoqaMapping::default(); let noqa_line_for = NoqaMapping::default();
let (count, output) = add_noqa_inner( let (count, output) = add_noqa_inner(
path, path,
&diagnostics, &messages,
&Locator::new(contents), &Locator::new(contents),
&CommentRanges::default(), &CommentRanges::default(),
&[], &[],
@ -2837,7 +2857,7 @@ mod tests {
assert_eq!(count, 1); assert_eq!(count, 1);
assert_eq!(output, "x = 1 # noqa: F841\n"); assert_eq!(output, "x = 1 # noqa: F841\n");
let diagnostics = [ let messages = [
Diagnostic::new( Diagnostic::new(
AmbiguousVariableName("x".to_string()), AmbiguousVariableName("x".to_string()),
TextRange::new(TextSize::from(0), TextSize::from(0)), TextRange::new(TextSize::from(0), TextSize::from(0)),
@ -2848,14 +2868,15 @@ mod tests {
}, },
TextRange::new(TextSize::from(0), TextSize::from(0)), TextRange::new(TextSize::from(0), TextSize::from(0)),
), ),
]; ]
.map(|d| message_from_diagnostic(d, path, contents));
let contents = "x = 1 # noqa: E741\n"; let contents = "x = 1 # noqa: E741\n";
let noqa_line_for = NoqaMapping::default(); let noqa_line_for = NoqaMapping::default();
let comment_ranges = let comment_ranges =
CommentRanges::new(vec![TextRange::new(TextSize::from(7), TextSize::from(19))]); CommentRanges::new(vec![TextRange::new(TextSize::from(7), TextSize::from(19))]);
let (count, output) = add_noqa_inner( let (count, output) = add_noqa_inner(
path, path,
&diagnostics, &messages,
&Locator::new(contents), &Locator::new(contents),
&comment_ranges, &comment_ranges,
&[], &[],
@ -2865,7 +2886,7 @@ mod tests {
assert_eq!(count, 1); assert_eq!(count, 1);
assert_eq!(output, "x = 1 # noqa: E741, F841\n"); assert_eq!(output, "x = 1 # noqa: E741, F841\n");
let diagnostics = [ let messages = [
Diagnostic::new( Diagnostic::new(
AmbiguousVariableName("x".to_string()), AmbiguousVariableName("x".to_string()),
TextRange::new(TextSize::from(0), TextSize::from(0)), TextRange::new(TextSize::from(0), TextSize::from(0)),
@ -2876,14 +2897,15 @@ mod tests {
}, },
TextRange::new(TextSize::from(0), TextSize::from(0)), TextRange::new(TextSize::from(0), TextSize::from(0)),
), ),
]; ]
.map(|d| message_from_diagnostic(d, path, contents));
let contents = "x = 1 # noqa"; let contents = "x = 1 # noqa";
let noqa_line_for = NoqaMapping::default(); let noqa_line_for = NoqaMapping::default();
let comment_ranges = let comment_ranges =
CommentRanges::new(vec![TextRange::new(TextSize::from(7), TextSize::from(13))]); CommentRanges::new(vec![TextRange::new(TextSize::from(7), TextSize::from(13))]);
let (count, output) = add_noqa_inner( let (count, output) = add_noqa_inner(
path, path,
&diagnostics, &messages,
&Locator::new(contents), &Locator::new(contents),
&comment_ranges, &comment_ranges,
&[], &[],
@ -2907,14 +2929,15 @@ print(
) )
"#; "#;
let noqa_line_for = [TextRange::new(8.into(), 68.into())].into_iter().collect(); let noqa_line_for = [TextRange::new(8.into(), 68.into())].into_iter().collect();
let diagnostics = [Diagnostic::new( let messages = [Diagnostic::new(
PrintfStringFormatting, PrintfStringFormatting,
TextRange::new(12.into(), 79.into()), TextRange::new(12.into(), 79.into()),
)]; )]
.map(|d| message_from_diagnostic(d, path, source));
let comment_ranges = CommentRanges::default(); let comment_ranges = CommentRanges::default();
let edits = generate_noqa_edits( let edits = generate_noqa_edits(
path, path,
&diagnostics, &messages,
&Locator::new(source), &Locator::new(source),
&comment_ranges, &comment_ranges,
&[], &[],
@ -2938,15 +2961,16 @@ print(
foo; foo;
bar = bar =
"; ";
let diagnostics = [Diagnostic::new( let messages = [Diagnostic::new(
UselessSemicolon, UselessSemicolon,
TextRange::new(4.into(), 5.into()), TextRange::new(4.into(), 5.into()),
)]; )]
.map(|d| message_from_diagnostic(d, path, source));
let noqa_line_for = NoqaMapping::default(); let noqa_line_for = NoqaMapping::default();
let comment_ranges = CommentRanges::default(); let comment_ranges = CommentRanges::default();
let edits = generate_noqa_edits( let edits = generate_noqa_edits(
path, path,
&diagnostics, &messages,
&Locator::new(source), &Locator::new(source),
&comment_ranges, &comment_ranges,
&[], &[],

View file

@ -22,6 +22,7 @@ mod tests {
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::linter::check_path; use crate::linter::check_path;
use crate::message::Message;
use crate::registry::{AsRule, Linter, Rule}; use crate::registry::{AsRule, Linter, Rule};
use crate::rules::isort; use crate::rules::isort;
use crate::rules::pyflakes; use crate::rules::pyflakes;
@ -757,7 +758,7 @@ mod tests {
&locator, &locator,
&indexer, &indexer,
); );
let mut diagnostics = check_path( let mut messages = check_path(
Path::new("<filename>"), Path::new("<filename>"),
None, None,
&locator, &locator,
@ -771,9 +772,10 @@ mod tests {
&parsed, &parsed,
settings.unresolved_target_version, settings.unresolved_target_version,
); );
diagnostics.sort_by_key(Ranged::start); messages.sort_by_key(Ranged::start);
let actual = diagnostics let actual = messages
.iter() .iter()
.filter_map(Message::as_diagnostic_message)
.map(|diagnostic| diagnostic.kind.rule()) .map(|diagnostic| diagnostic.kind.rule())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!(actual, expected); assert_eq!(actual, expected);

View file

@ -9,7 +9,7 @@ use anyhow::Result;
use itertools::Itertools; use itertools::Itertools;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use ruff_diagnostics::{Applicability, Diagnostic, FixAvailability}; use ruff_diagnostics::{Applicability, FixAvailability};
use ruff_notebook::Notebook; use ruff_notebook::Notebook;
#[cfg(not(fuzzing))] #[cfg(not(fuzzing))]
use ruff_notebook::NotebookError; use ruff_notebook::NotebookError;
@ -19,7 +19,6 @@ use ruff_python_index::Indexer;
use ruff_python_parser::{ParseError, ParseOptions}; use ruff_python_parser::{ParseError, ParseOptions};
use ruff_python_trivia::textwrap::dedent; use ruff_python_trivia::textwrap::dedent;
use ruff_source_file::SourceFileBuilder; use ruff_source_file::SourceFileBuilder;
use ruff_text_size::Ranged;
use crate::fix::{fix_file, FixResult}; use crate::fix::{fix_file, FixResult};
use crate::linter::check_path; use crate::linter::check_path;
@ -124,7 +123,7 @@ pub(crate) fn test_contents<'a>(
&locator, &locator,
&indexer, &indexer,
); );
let diagnostics = check_path( let messages = check_path(
path, path,
path.parent() path.parent()
.and_then(|parent| detect_package_root(parent, &settings.namespace_packages)) .and_then(|parent| detect_package_root(parent, &settings.namespace_packages))
@ -148,25 +147,22 @@ pub(crate) fn test_contents<'a>(
let mut transformed = Cow::Borrowed(source_kind); let mut transformed = Cow::Borrowed(source_kind);
if diagnostics if messages.iter().any(|message| message.fix().is_some()) {
.iter() let mut messages = messages.clone();
.any(|diagnostic| diagnostic.fix.is_some())
{
let mut diagnostics = diagnostics.clone();
while let Some(FixResult { while let Some(FixResult {
code: fixed_contents, code: fixed_contents,
source_map, source_map,
.. ..
}) = fix_file( }) = fix_file(
&diagnostics, &messages,
&Locator::new(transformed.source_code()), &Locator::new(transformed.source_code()),
UnsafeFixes::Enabled, UnsafeFixes::Enabled,
) { ) {
if iterations < max_iterations() { if iterations < max_iterations() {
iterations += 1; iterations += 1;
} else { } else {
let output = print_diagnostics(diagnostics, path, &transformed); let output = print_diagnostics(messages, path, &transformed);
panic!( panic!(
"Failed to converge after {} iterations. This likely \ "Failed to converge after {} iterations. This likely \
@ -192,7 +188,7 @@ pub(crate) fn test_contents<'a>(
&indexer, &indexer,
); );
let fixed_diagnostics = check_path( let fixed_messages = check_path(
path, path,
None, None,
&locator, &locator,
@ -209,7 +205,7 @@ pub(crate) fn test_contents<'a>(
if parsed.has_invalid_syntax() && !source_has_errors { if parsed.has_invalid_syntax() && !source_has_errors {
// Previous fix introduced a syntax error, abort // Previous fix introduced a syntax error, abort
let fixes = print_diagnostics(diagnostics, path, source_kind); let fixes = print_diagnostics(messages, path, source_kind);
let syntax_errors = let syntax_errors =
print_syntax_errors(parsed.errors(), path, &locator, &transformed); print_syntax_errors(parsed.errors(), path, &locator, &transformed);
@ -224,7 +220,7 @@ Source with applied fixes:
); );
} }
diagnostics = fixed_diagnostics; messages = fixed_messages;
} }
} }
@ -234,9 +230,10 @@ Source with applied fixes:
) )
.finish(); .finish();
let messages = diagnostics let messages = messages
.into_iter() .into_iter()
.map(|diagnostic| { .filter_map(Message::into_diagnostic_message)
.map(|mut diagnostic| {
let rule = diagnostic.kind.rule(); let rule = diagnostic.kind.rule();
let fixable = diagnostic.fix.as_ref().is_some_and(|fix| { let fixable = diagnostic.fix.as_ref().is_some_and(|fix| {
matches!( matches!(
@ -277,9 +274,10 @@ Either ensure you always emit a fix or change `Violation::FIX_AVAILABILITY` to e
); );
// Not strictly necessary but adds some coverage for this code path // Not strictly necessary but adds some coverage for this code path
let noqa = directives.noqa_line_for.resolve(diagnostic.start()); diagnostic.noqa_offset = directives.noqa_line_for.resolve(diagnostic.range.start());
diagnostic.file = source_code.clone();
Message::from_diagnostic(diagnostic, source_code.clone(), noqa) Message::Diagnostic(diagnostic)
}) })
.chain(parsed.errors().iter().map(|parse_error| { .chain(parsed.errors().iter().map(|parse_error| {
Message::from_parse_error(parse_error, &locator, source_code.clone()) Message::from_parse_error(parse_error, &locator, source_code.clone())
@ -310,18 +308,9 @@ fn print_syntax_errors(
} }
} }
fn print_diagnostics(diagnostics: Vec<Diagnostic>, path: &Path, source: &SourceKind) -> String { /// Print the [`Message::Diagnostic`]s in `messages`.
let filename = path.file_name().unwrap().to_string_lossy(); fn print_diagnostics(mut messages: Vec<Message>, path: &Path, source: &SourceKind) -> String {
let source_file = SourceFileBuilder::new(filename.as_ref(), source.source_code()).finish(); messages.retain(Message::is_diagnostic_message);
let messages: Vec<_> = diagnostics
.into_iter()
.map(|diagnostic| {
let noqa_start = diagnostic.start();
Message::from_diagnostic(diagnostic, source_file.clone(), noqa_start)
})
.collect();
if let Some(notebook) = source.as_ipy_notebook() { if let Some(notebook) = source.as_ipy_notebook() {
print_jupyter_messages(&messages, path, notebook) print_jupyter_messages(&messages, path, notebook)

View file

@ -9,12 +9,13 @@ use crate::{
session::DocumentQuery, session::DocumentQuery,
PositionEncoding, DIAGNOSTIC_NAME, PositionEncoding, DIAGNOSTIC_NAME,
}; };
use ruff_diagnostics::{Applicability, Diagnostic, DiagnosticKind, Edit, Fix}; use ruff_diagnostics::{Applicability, DiagnosticKind, Edit, Fix};
use ruff_linter::package::PackageRoot;
use ruff_linter::{ use ruff_linter::{
directives::{extract_directives, Flags}, directives::{extract_directives, Flags},
generate_noqa_edits, generate_noqa_edits,
linter::check_path, linter::check_path,
message::{DiagnosticMessage, Message, SyntaxErrorMessage},
package::PackageRoot,
packaging::detect_package_root, packaging::detect_package_root,
registry::AsRule, registry::AsRule,
settings::flags, settings::flags,
@ -24,7 +25,7 @@ use ruff_linter::{
use ruff_notebook::Notebook; use ruff_notebook::Notebook;
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_parser::{ParseError, ParseOptions, UnsupportedSyntaxError}; use ruff_python_parser::ParseOptions;
use ruff_source_file::LineIndex; use ruff_source_file::LineIndex;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -120,7 +121,7 @@ pub(crate) fn check(
let directives = extract_directives(parsed.tokens(), Flags::all(), &locator, &indexer); let directives = extract_directives(parsed.tokens(), Flags::all(), &locator, &indexer);
// Generate checks. // Generate checks.
let diagnostics = check_path( let messages = check_path(
&query.virtual_file_path(), &query.virtual_file_path(),
package, package,
&locator, &locator,
@ -137,7 +138,7 @@ pub(crate) fn check(
let noqa_edits = generate_noqa_edits( let noqa_edits = generate_noqa_edits(
&query.virtual_file_path(), &query.virtual_file_path(),
&diagnostics, &messages,
&locator, &locator,
indexer.comment_ranges(), indexer.comment_ranges(),
&settings.linter.external, &settings.linter.external,
@ -159,51 +160,31 @@ pub(crate) fn check(
.or_default(); .or_default();
} }
let lsp_diagnostics = diagnostics let lsp_diagnostics =
.into_iter() messages
.zip(noqa_edits)
.map(|(diagnostic, noqa_edit)| {
to_lsp_diagnostic(
diagnostic,
noqa_edit,
&source_kind,
locator.to_index(),
encoding,
)
});
let unsupported_syntax_errors = if settings.linter.preview.is_enabled() {
parsed.unsupported_syntax_errors()
} else {
&[]
};
let lsp_diagnostics = lsp_diagnostics.chain(
show_syntax_errors
.then(|| {
parsed
.errors()
.iter()
.map(|parse_error| {
parse_error_to_lsp_diagnostic(
parse_error,
&source_kind,
locator.to_index(),
encoding,
)
})
.chain(unsupported_syntax_errors.iter().map(|error| {
unsupported_syntax_error_to_lsp_diagnostic(
error,
&source_kind,
locator.to_index(),
encoding,
)
}))
})
.into_iter() .into_iter()
.flatten(), .zip(noqa_edits)
); .filter_map(|(message, noqa_edit)| match message {
Message::Diagnostic(diagnostic_message) => Some(to_lsp_diagnostic(
diagnostic_message,
noqa_edit,
&source_kind,
locator.to_index(),
encoding,
)),
Message::SyntaxError(syntax_error_message) => {
if show_syntax_errors {
Some(syntax_error_to_lsp_diagnostic(
syntax_error_message,
&source_kind,
locator.to_index(),
encoding,
))
} else {
None
}
}
});
if let Some(notebook) = query.as_notebook() { if let Some(notebook) = query.as_notebook() {
for (index, diagnostic) in lsp_diagnostics { for (index, diagnostic) in lsp_diagnostics {
@ -260,13 +241,13 @@ pub(crate) fn fixes_for_diagnostics(
/// Generates an LSP diagnostic with an associated cell index for the diagnostic to go in. /// Generates an LSP diagnostic with an associated cell index for the diagnostic to go in.
/// If the source kind is a text document, the cell index will always be `0`. /// If the source kind is a text document, the cell index will always be `0`.
fn to_lsp_diagnostic( fn to_lsp_diagnostic(
diagnostic: Diagnostic, diagnostic: DiagnosticMessage,
noqa_edit: Option<Edit>, noqa_edit: Option<Edit>,
source_kind: &SourceKind, source_kind: &SourceKind,
index: &LineIndex, index: &LineIndex,
encoding: PositionEncoding, encoding: PositionEncoding,
) -> (usize, lsp_types::Diagnostic) { ) -> (usize, lsp_types::Diagnostic) {
let Diagnostic { let DiagnosticMessage {
kind, kind,
range: diagnostic_range, range: diagnostic_range,
fix, fix,
@ -339,8 +320,8 @@ fn to_lsp_diagnostic(
) )
} }
fn parse_error_to_lsp_diagnostic( fn syntax_error_to_lsp_diagnostic(
parse_error: &ParseError, syntax_error: SyntaxErrorMessage,
source_kind: &SourceKind, source_kind: &SourceKind,
index: &LineIndex, index: &LineIndex,
encoding: PositionEncoding, encoding: PositionEncoding,
@ -349,7 +330,7 @@ fn parse_error_to_lsp_diagnostic(
let cell: usize; let cell: usize;
if let Some(notebook_index) = source_kind.as_ipy_notebook().map(Notebook::index) { if let Some(notebook_index) = source_kind.as_ipy_notebook().map(Notebook::index) {
NotebookRange { cell, range } = parse_error.location.to_notebook_range( NotebookRange { cell, range } = syntax_error.range.to_notebook_range(
source_kind.source_code(), source_kind.source_code(),
index, index,
notebook_index, notebook_index,
@ -357,46 +338,7 @@ fn parse_error_to_lsp_diagnostic(
); );
} else { } else {
cell = usize::default(); cell = usize::default();
range = parse_error range = syntax_error
.location
.to_range(source_kind.source_code(), index, encoding);
}
(
cell,
lsp_types::Diagnostic {
range,
severity: Some(lsp_types::DiagnosticSeverity::ERROR),
tags: None,
code: None,
code_description: None,
source: Some(DIAGNOSTIC_NAME.into()),
message: format!("SyntaxError: {}", &parse_error.error),
related_information: None,
data: None,
},
)
}
fn unsupported_syntax_error_to_lsp_diagnostic(
unsupported_syntax_error: &UnsupportedSyntaxError,
source_kind: &SourceKind,
index: &LineIndex,
encoding: PositionEncoding,
) -> (usize, lsp_types::Diagnostic) {
let range: lsp_types::Range;
let cell: usize;
if let Some(notebook_index) = source_kind.as_ipy_notebook().map(Notebook::index) {
NotebookRange { cell, range } = unsupported_syntax_error.range.to_notebook_range(
source_kind.source_code(),
index,
notebook_index,
encoding,
);
} else {
cell = usize::default();
range = unsupported_syntax_error
.range .range
.to_range(source_kind.source_code(), index, encoding); .to_range(source_kind.source_code(), index, encoding);
} }
@ -410,7 +352,7 @@ fn unsupported_syntax_error_to_lsp_diagnostic(
code: None, code: None,
code_description: None, code_description: None,
source: Some(DIAGNOSTIC_NAME.into()), source: Some(DIAGNOSTIC_NAME.into()),
message: format!("SyntaxError: {unsupported_syntax_error}"), message: syntax_error.message,
related_information: None, related_information: None,
data: None, data: None,
}, },

View file

@ -1,6 +1,7 @@
use std::path::Path; use std::path::Path;
use js_sys::Error; use js_sys::Error;
use ruff_linter::message::{DiagnosticMessage, Message, SyntaxErrorMessage};
use ruff_linter::settings::types::PythonVersion; use ruff_linter::settings::types::PythonVersion;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
@ -31,7 +32,7 @@ const TYPES: &'static str = r#"
export interface Diagnostic { export interface Diagnostic {
code: string | null; code: string | null;
message: string; message: string;
location: { start_location: {
row: number; row: number;
column: number; column: number;
}; };
@ -60,7 +61,7 @@ export interface Diagnostic {
pub struct ExpandedMessage { pub struct ExpandedMessage {
pub code: Option<String>, pub code: Option<String>,
pub message: String, pub message: String,
pub location: SourceLocation, pub start_location: SourceLocation,
pub end_location: SourceLocation, pub end_location: SourceLocation,
pub fix: Option<ExpandedFix>, pub fix: Option<ExpandedFix>,
} }
@ -188,7 +189,7 @@ impl Workspace {
); );
// Generate checks. // Generate checks.
let diagnostics = check_path( let messages = check_path(
Path::new("<filename>"), Path::new("<filename>"),
None, None,
&locator, &locator,
@ -205,25 +206,18 @@ impl Workspace {
let source_code = locator.to_source_code(); let source_code = locator.to_source_code();
let unsupported_syntax_errors = if self.settings.linter.preview.is_enabled() { let messages: Vec<ExpandedMessage> = messages
parsed.unsupported_syntax_errors()
} else {
&[]
};
let messages: Vec<ExpandedMessage> = diagnostics
.into_iter() .into_iter()
.map(|diagnostic| { .map(|message| match message {
let start_location = source_code.source_location(diagnostic.start()); Message::Diagnostic(DiagnosticMessage {
let end_location = source_code.source_location(diagnostic.end()); kind, range, fix, ..
}) => ExpandedMessage {
ExpandedMessage { code: Some(kind.rule().noqa_code().to_string()),
code: Some(diagnostic.kind.rule().noqa_code().to_string()), message: kind.body,
message: diagnostic.kind.body, start_location: source_code.source_location(range.start()),
location: start_location, end_location: source_code.source_location(range.end()),
end_location, fix: fix.map(|fix| ExpandedFix {
fix: diagnostic.fix.map(|fix| ExpandedFix { message: kind.suggestion,
message: diagnostic.kind.suggestion,
edits: fix edits: fix
.edits() .edits()
.iter() .iter()
@ -234,32 +228,17 @@ impl Workspace {
}) })
.collect(), .collect(),
}), }),
},
Message::SyntaxError(SyntaxErrorMessage { message, range, .. }) => {
ExpandedMessage {
code: None,
message,
start_location: source_code.source_location(range.start()),
end_location: source_code.source_location(range.end()),
fix: None,
}
} }
}) })
.chain(parsed.errors().iter().map(|parse_error| {
let start_location = source_code.source_location(parse_error.location.start());
let end_location = source_code.source_location(parse_error.location.end());
ExpandedMessage {
code: None,
message: format!("SyntaxError: {}", parse_error.error),
location: start_location,
end_location,
fix: None,
}
}))
.chain(unsupported_syntax_errors.iter().map(|error| {
let start_location = source_code.source_location(error.range.start());
let end_location = source_code.source_location(error.range.end());
ExpandedMessage {
code: None,
message: format!("SyntaxError: {error}"),
location: start_location,
end_location,
fix: None,
}
}))
.collect(); .collect();
serde_wasm_bindgen::to_value(&messages).map_err(into_error) serde_wasm_bindgen::to_value(&messages).map_err(into_error)

View file

@ -27,7 +27,7 @@ fn empty_config() {
[ExpandedMessage { [ExpandedMessage {
code: Some(Rule::IfTuple.noqa_code().to_string()), code: Some(Rule::IfTuple.noqa_code().to_string()),
message: "If test is a tuple, which is always `True`".to_string(), message: "If test is a tuple, which is always `True`".to_string(),
location: SourceLocation { start_location: SourceLocation {
row: OneIndexed::from_zero_indexed(0), row: OneIndexed::from_zero_indexed(0),
column: OneIndexed::from_zero_indexed(3) column: OneIndexed::from_zero_indexed(3)
}, },
@ -48,7 +48,7 @@ fn syntax_error() {
[ExpandedMessage { [ExpandedMessage {
code: None, code: None,
message: "SyntaxError: Expected an expression".to_string(), message: "SyntaxError: Expected an expression".to_string(),
location: SourceLocation { start_location: SourceLocation {
row: OneIndexed::from_zero_indexed(0), row: OneIndexed::from_zero_indexed(0),
column: OneIndexed::from_zero_indexed(3) column: OneIndexed::from_zero_indexed(3)
}, },
@ -69,7 +69,7 @@ fn unsupported_syntax_error() {
[ExpandedMessage { [ExpandedMessage {
code: None, code: None,
message: "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)".to_string(), message: "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)".to_string(),
location: SourceLocation { start_location: SourceLocation {
row: OneIndexed::from_zero_indexed(0), row: OneIndexed::from_zero_indexed(0),
column: OneIndexed::from_zero_indexed(0) column: OneIndexed::from_zero_indexed(0)
}, },

View file

@ -17,11 +17,11 @@ export default function Diagnostics({
const diagnostics = useMemo(() => { const diagnostics = useMemo(() => {
const sorted = [...unsorted]; const sorted = [...unsorted];
sorted.sort((a, b) => { sorted.sort((a, b) => {
if (a.location.row === b.location.row) { if (a.start_location.row === b.start_location.row) {
return a.location.column - b.location.column; return a.start_location.column - b.start_location.column;
} }
return a.location.row - b.location.row; return a.start_location.row - b.start_location.row;
}); });
return sorted; return sorted;
@ -70,18 +70,22 @@ function Items({
{diagnostics.map((diagnostic, index) => { {diagnostics.map((diagnostic, index) => {
return ( return (
<li <li
key={`${diagnostic.location.row}:${diagnostic.location.column}-${diagnostic.code ?? index}`} key={`${diagnostic.start_location.row}:${diagnostic.start_location.column}-${diagnostic.code ?? index}`}
> >
<button <button
onClick={() => onClick={() =>
onGoTo(diagnostic.location.row, diagnostic.location.column) onGoTo(
diagnostic.start_location.row,
diagnostic.start_location.column,
)
} }
className="w-full text-start" className="w-full text-start"
> >
{diagnostic.message}{" "} {diagnostic.message}{" "}
<span className="text-gray-500"> <span className="text-gray-500">
{diagnostic.code != null && `(${diagnostic.code})`} [Ln{" "} {diagnostic.code != null && `(${diagnostic.code})`} [Ln{" "}
{diagnostic.location.row}, Col {diagnostic.location.column}] {diagnostic.start_location.row}, Col{" "}
{diagnostic.start_location.column}]
</span> </span>
</button> </button>
</li> </li>

View file

@ -124,7 +124,7 @@ class RuffCodeActionProvider implements CodeActionProvider {
range: Range, range: Range,
): languages.ProviderResult<languages.CodeActionList> { ): languages.ProviderResult<languages.CodeActionList> {
const actions = this.diagnostics const actions = this.diagnostics
.filter((check) => range.startLineNumber === check.location.row) .filter((check) => range.startLineNumber === check.start_location.row)
.filter(({ fix }) => fix) .filter(({ fix }) => fix)
.map((check) => ({ .map((check) => ({
title: check.fix title: check.fix
@ -173,8 +173,8 @@ function updateMarkers(monaco: Monaco, diagnostics: Array<Diagnostic>) {
model, model,
"owner", "owner",
diagnostics.map((diagnostic) => ({ diagnostics.map((diagnostic) => ({
startLineNumber: diagnostic.location.row, startLineNumber: diagnostic.start_location.row,
startColumn: diagnostic.location.column, startColumn: diagnostic.start_location.column,
endLineNumber: diagnostic.end_location.row, endLineNumber: diagnostic.end_location.row,
endColumn: diagnostic.end_location.column, endColumn: diagnostic.end_location.column,
message: diagnostic.code message: diagnostic.code