From e7b49694a795e3347ffc6f499245dfcbbb4b28ed Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 27 Jun 2024 07:51:32 +0530 Subject: [PATCH] Remove `E999` as a rule, disallow any disablement methods for syntax error (#11901) ## Summary This PR updates the way syntax errors are handled throughout the linter. The main change is that it's now not considered as a rule which involves the following changes: * Update `Message` to be an enum with two variants - one for diagnostic message and the other for syntax error message * Provide methods on the new message enum to query information required by downstream usages This means that the syntax errors cannot be hidden / disabled via any disablement methods. These are: 1. Configuration via `select`, `ignore`, `per-file-ignores`, and their `extend-*` variants ```console $ cargo run -- check ~/playground/ruff/src/lsp.py --extend-select=E999 --no-preview --no-cache Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s Running `target/debug/ruff check /Users/dhruv/playground/ruff/src/lsp.py --extend-select=E999 --no-preview --no-cache` warning: Rule `E999` is deprecated and will be removed in a future release. Syntax errors will always be shown regardless of whether this rule is selected or not. /Users/dhruv/playground/ruff/src/lsp.py:1:8: F401 [*] `abc` imported but unused | 1 | import abc | ^^^ F401 2 | from pathlib import Path 3 | import os | = help: Remove unused import: `abc` ``` 3. Command-line flags via `--select`, `--ignore`, `--per-file-ignores`, and their `--extend-*` variants ```console $ cargo run -- check ~/playground/ruff/src/lsp.py --no-cache --config=~/playground/ruff/pyproject.toml Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s Running `target/debug/ruff check /Users/dhruv/playground/ruff/src/lsp.py --no-cache --config=/Users/dhruv/playground/ruff/pyproject.toml` warning: Rule `E999` is deprecated and will be removed in a future release. Syntax errors will always be shown regardless of whether this rule is selected or not. /Users/dhruv/playground/ruff/src/lsp.py:1:8: F401 [*] `abc` imported but unused | 1 | import abc | ^^^ F401 2 | from pathlib import Path 3 | import os | = help: Remove unused import: `abc` ``` This also means that the **output format** needs to be updated: 1. The `code`, `noqa_row`, `url` fields in the JSON output is optional (`null` for syntax errors) 2. Other formats are changed accordingly For each format, a new test case specific to syntax errors have been added. Please refer to the snapshot output for the exact format for syntax error message. The output of the `--statistics` flag will have a blank entry for syntax errors: ``` 315 F821 [ ] undefined-name 119 [ ] syntax-error 103 F811 [ ] redefined-while-unused ``` The **language server** is updated to consider the syntax errors by convert them into LSP diagnostic format separately. ### Preview There are no quick fixes provided to disable syntax errors. This will automatically work for `ruff-lsp` because the `noqa_row` field will be `null` in that case. Screenshot 2024-06-26 at 14 57 08 Even with `noqa` comment, the syntax error is displayed: Screenshot 2024-06-26 at 14 59 51 Rule documentation page: Screenshot 2024-06-26 at 16 48 07 ## Test Plan - [x] Disablement methods via config shows a warning - [x] `select`, `extend-select` - [ ] ~`ignore`~ _doesn't show any message_ - [ ] ~`per-file-ignores`, `extend-per-file-ignores`~ _doesn't show any message_ - [x] Disablement methods via command-line flag shows a warning - [x] `--select`, `--extend-select` - [ ] ~`--ignore`~ _doesn't show any message_ - [ ] ~`--per-file-ignores`, `--extend-per-file-ignores`~ _doesn't show any message_ - [x] File with syntax errors should exit with code 1 - [x] Language server - [x] Should show diagnostics for syntax errors - [x] Should not recommend a quick fix edit for adding `noqa` comment - [x] Same for `ruff-lsp` resolves: #8447 --- crates/ruff/src/cache.rs | 30 +-- crates/ruff/src/diagnostics.rs | 92 ++++---- crates/ruff/src/printer.rs | 63 +++--- crates/ruff/tests/integration_test.rs | 56 ++++- .../test/fixtures/flake8_commas/COM81.py | 2 +- .../test/fixtures/pycodestyle/E999.py | 4 - crates/ruff_linter/src/codes.rs | 2 +- crates/ruff_linter/src/lib.rs | 2 +- crates/ruff_linter/src/linter.rs | 141 +++++-------- crates/ruff_linter/src/message/azure.rs | 21 +- crates/ruff_linter/src/message/diff.rs | 4 +- crates/ruff_linter/src/message/github.rs | 30 ++- crates/ruff_linter/src/message/gitlab.rs | 31 +-- crates/ruff_linter/src/message/grouped.rs | 12 +- crates/ruff_linter/src/message/json.rs | 31 ++- crates/ruff_linter/src/message/json_lines.rs | 10 +- crates/ruff_linter/src/message/junit.rs | 23 +- crates/ruff_linter/src/message/mod.rs | 199 ++++++++++++++++-- crates/ruff_linter/src/message/pylint.rs | 27 ++- crates/ruff_linter/src/message/rdjson.rs | 29 ++- crates/ruff_linter/src/message/sarif.rs | 28 ++- ..._message__azure__tests__syntax_errors.snap | 6 + ...message__github__tests__syntax_errors.snap | 6 + ...message__gitlab__tests__syntax_errors.snap | 30 +++ ...essage__grouped__tests__syntax_errors.snap | 7 + ...__message__json__tests__syntax_errors.snap | 40 ++++ ...age__json_lines__tests__syntax_errors.snap | 6 + ..._message__junit__tests__syntax_errors.snap | 15 ++ ...message__pylint__tests__syntax_errors.snap | 6 + ...message__rdjson__tests__syntax_errors.snap | 53 +++++ ...__message__text__tests__syntax_errors.snap | 22 ++ crates/ruff_linter/src/message/text.rs | 57 +++-- crates/ruff_linter/src/noqa.rs | 34 ++- ...rules__flake8_commas__tests__COM81.py.snap | 18 +- .../ruff_linter/src/rules/pycodestyle/mod.rs | 1 - .../src/rules/pycodestyle/rules/errors.rs | 36 +--- .../src/rules/pycodestyle/rules/mod.rs | 2 +- ...ules__pycodestyle__tests__E111_E11.py.snap | 38 ++++ ...ules__pycodestyle__tests__E112_E11.py.snap | 40 +++- ...ules__pycodestyle__tests__E113_E11.py.snap | 38 ++++ ...ules__pycodestyle__tests__E114_E11.py.snap | 40 +++- ...ules__pycodestyle__tests__E115_E11.py.snap | 40 +++- ...ules__pycodestyle__tests__E116_E11.py.snap | 40 +++- ...ules__pycodestyle__tests__E117_E11.py.snap | 40 +++- ...les__pycodestyle__tests__E999_E999.py.snap | 9 - ...ules__pycodestyle__tests__W191_W19.py.snap | 16 ++ ...hite_space_syntax_error_compatibility.snap | 6 +- crates/ruff_linter/src/rules/pyflakes/mod.rs | 14 +- crates/ruff_linter/src/test.rs | 55 +++-- crates/ruff_server/src/lint.rs | 45 +++- crates/ruff_workspace/src/configuration.rs | 10 +- fuzz/fuzz_targets/ruff_formatter_validity.rs | 8 +- 52 files changed, 1235 insertions(+), 380 deletions(-) delete mode 100644 crates/ruff_linter/resources/test/fixtures/pycodestyle/E999.py create mode 100644 crates/ruff_linter/src/message/snapshots/ruff_linter__message__azure__tests__syntax_errors.snap create mode 100644 crates/ruff_linter/src/message/snapshots/ruff_linter__message__github__tests__syntax_errors.snap create mode 100644 crates/ruff_linter/src/message/snapshots/ruff_linter__message__gitlab__tests__syntax_errors.snap create mode 100644 crates/ruff_linter/src/message/snapshots/ruff_linter__message__grouped__tests__syntax_errors.snap create mode 100644 crates/ruff_linter/src/message/snapshots/ruff_linter__message__json__tests__syntax_errors.snap create mode 100644 crates/ruff_linter/src/message/snapshots/ruff_linter__message__json_lines__tests__syntax_errors.snap create mode 100644 crates/ruff_linter/src/message/snapshots/ruff_linter__message__junit__tests__syntax_errors.snap create mode 100644 crates/ruff_linter/src/message/snapshots/ruff_linter__message__pylint__tests__syntax_errors.snap create mode 100644 crates/ruff_linter/src/message/snapshots/ruff_linter__message__rdjson__tests__syntax_errors.snap create mode 100644 crates/ruff_linter/src/message/snapshots/ruff_linter__message__text__tests__syntax_errors.snap delete mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E999_E999.py.snap diff --git a/crates/ruff/src/cache.rs b/crates/ruff/src/cache.rs index 136555fb1b9c2..6c126e8a97ed4 100644 --- a/crates/ruff/src/cache.rs +++ b/crates/ruff/src/cache.rs @@ -19,7 +19,7 @@ use tempfile::NamedTempFile; use ruff_cache::{CacheKey, CacheKeyHasher}; use ruff_diagnostics::{DiagnosticKind, Fix}; -use ruff_linter::message::Message; +use ruff_linter::message::{DiagnosticMessage, Message}; use ruff_linter::{warn_user, VERSION}; use ruff_macros::CacheKey; use ruff_notebook::NotebookIndex; @@ -333,12 +333,14 @@ impl FileCache { let file = SourceFileBuilder::new(path.to_string_lossy(), &*lint.source).finish(); lint.messages .iter() - .map(|msg| Message { - kind: msg.kind.clone(), - range: msg.range, - fix: msg.fix.clone(), - file: file.clone(), - noqa_offset: msg.noqa_offset, + .map(|msg| { + Message::Diagnostic(DiagnosticMessage { + kind: msg.kind.clone(), + range: msg.range, + fix: msg.fix.clone(), + file: file.clone(), + noqa_offset: msg.noqa_offset, + }) }) .collect() }; @@ -412,18 +414,19 @@ impl LintCacheData { notebook_index: Option, ) -> Self { let source = if let Some(msg) = messages.first() { - msg.file.source_text().to_owned() + msg.source_file().source_text().to_owned() } else { String::new() // No messages, no need to keep the source! }; let messages = messages .iter() + .filter_map(|message| message.as_diagnostic_message()) .map(|msg| { // Make sure that all message use the same source file. assert_eq!( - msg.file, - messages.first().unwrap().file, + &msg.file, + messages.first().unwrap().source_file(), "message uses a different source file" ); CacheMessage { @@ -571,6 +574,7 @@ mod tests { use test_case::test_case; use ruff_cache::CACHE_DIR_NAME; + use ruff_linter::message::Message; use ruff_linter::settings::flags; use ruff_linter::settings::types::UnsafeFixes; use ruff_python_ast::PySourceType; @@ -633,11 +637,7 @@ mod tests { UnsafeFixes::Enabled, ) .unwrap(); - if diagnostics - .messages - .iter() - .any(|m| m.kind.name == "SyntaxError") - { + if diagnostics.messages.iter().any(Message::is_syntax_error) { parse_errors.push(path.clone()); } paths.push(path); diff --git a/crates/ruff/src/diagnostics.rs b/crates/ruff/src/diagnostics.rs index dcd5e2890e57f..db24952b7a70f 100644 --- a/crates/ruff/src/diagnostics.rs +++ b/crates/ruff/src/diagnostics.rs @@ -10,18 +10,18 @@ use std::path::Path; use anyhow::{Context, Result}; use colored::Colorize; use log::{debug, error, warn}; +use ruff_linter::codes::Rule; use rustc_hash::FxHashMap; use ruff_diagnostics::Diagnostic; use ruff_linter::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult, ParseSource}; use ruff_linter::logging::DisplayParseError; -use ruff_linter::message::Message; +use ruff_linter::message::{Message, SyntaxErrorMessage}; use ruff_linter::pyproject_toml::lint_pyproject_toml; -use ruff_linter::registry::AsRule; use ruff_linter::settings::types::UnsafeFixes; use ruff_linter::settings::{flags, LinterSettings}; use ruff_linter::source_kind::{SourceError, SourceKind}; -use ruff_linter::{fs, IOError, SyntaxError}; +use ruff_linter::{fs, IOError}; use ruff_notebook::{Notebook, NotebookError, NotebookIndex}; use ruff_python_ast::{PySourceType, SourceType, TomlSourceType}; use ruff_source_file::SourceFileBuilder; @@ -55,57 +55,61 @@ impl Diagnostics { path: Option<&Path>, settings: &LinterSettings, ) -> Self { - let diagnostic = match err { + match err { // IO errors. SourceError::Io(_) | SourceError::Notebook(NotebookError::Io(_) | NotebookError::Json(_)) => { - Diagnostic::new( - IOError { - message: err.to_string(), - }, - TextRange::default(), - ) + if settings.rules.enabled(Rule::IOError) { + let name = path.map_or_else(|| "-".into(), Path::to_string_lossy); + let source_file = SourceFileBuilder::new(name, "").finish(); + Self::new( + vec![Message::from_diagnostic( + Diagnostic::new( + IOError { + message: err.to_string(), + }, + TextRange::default(), + ), + source_file, + TextSize::default(), + )], + FxHashMap::default(), + ) + } else { + match path { + Some(path) => { + warn!( + "{}{}{} {err}", + "Failed to lint ".bold(), + fs::relativize_path(path).bold(), + ":".bold() + ); + } + None => { + warn!("{}{} {err}", "Failed to lint".bold(), ":".bold()); + } + } + + Self::default() + } } // Syntax errors. SourceError::Notebook( NotebookError::InvalidJson(_) | NotebookError::InvalidSchema(_) | NotebookError::InvalidFormat(_), - ) => Diagnostic::new( - SyntaxError { - message: err.to_string(), - }, - TextRange::default(), - ), - }; - - if settings.rules.enabled(diagnostic.kind.rule()) { - let name = path.map_or_else(|| "-".into(), Path::to_string_lossy); - let dummy = SourceFileBuilder::new(name, "").finish(); - Self::new( - vec![Message::from_diagnostic( - diagnostic, - dummy, - TextSize::default(), - )], - FxHashMap::default(), - ) - } else { - match path { - Some(path) => { - warn!( - "{}{}{} {err}", - "Failed to lint ".bold(), - fs::relativize_path(path).bold(), - ":".bold() - ); - } - None => { - warn!("{}{} {err}", "Failed to lint".bold(), ":".bold()); - } + ) => { + let name = path.map_or_else(|| "-".into(), Path::to_string_lossy); + let dummy = SourceFileBuilder::new(name, "").finish(); + Self::new( + vec![Message::SyntaxError(SyntaxErrorMessage { + message: err.to_string(), + range: TextRange::default(), + file: dummy, + })], + FxHashMap::default(), + ) } - - Self::default() } } } diff --git a/crates/ruff/src/printer.rs b/crates/ruff/src/printer.rs index cef5596df8b4e..588b9e179a686 100644 --- a/crates/ruff/src/printer.rs +++ b/crates/ruff/src/printer.rs @@ -13,11 +13,11 @@ use ruff_linter::fs::relativize_path; use ruff_linter::logging::LogLevel; use ruff_linter::message::{ AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter, - JsonEmitter, JsonLinesEmitter, JunitEmitter, PylintEmitter, RdjsonEmitter, SarifEmitter, - TextEmitter, + JsonEmitter, JsonLinesEmitter, JunitEmitter, Message, MessageKind, PylintEmitter, + RdjsonEmitter, SarifEmitter, TextEmitter, }; use ruff_linter::notify_user; -use ruff_linter::registry::{AsRule, Rule}; +use ruff_linter::registry::Rule; use ruff_linter::settings::flags::{self}; use ruff_linter::settings::types::{OutputFormat, UnsafeFixes}; @@ -37,12 +37,13 @@ bitflags! { #[derive(Serialize)] struct ExpandedStatistics { - code: SerializeRuleAsCode, - name: SerializeRuleAsTitle, + code: Option, + name: SerializeMessageKindAsTitle, count: usize, fixable: bool, } +#[derive(Copy, Clone)] struct SerializeRuleAsCode(Rule); impl Serialize for SerializeRuleAsCode { @@ -66,26 +67,26 @@ impl From for SerializeRuleAsCode { } } -struct SerializeRuleAsTitle(Rule); +struct SerializeMessageKindAsTitle(MessageKind); -impl Serialize for SerializeRuleAsTitle { +impl Serialize for SerializeMessageKindAsTitle { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { - serializer.serialize_str(self.0.as_ref()) + serializer.serialize_str(self.0.as_str()) } } -impl Display for SerializeRuleAsTitle { +impl Display for SerializeMessageKindAsTitle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0.as_ref()) + f.write_str(self.0.as_str()) } } -impl From for SerializeRuleAsTitle { - fn from(rule: Rule) -> Self { - Self(rule) +impl From for SerializeMessageKindAsTitle { + fn from(kind: MessageKind) -> Self { + Self(kind) } } @@ -341,24 +342,23 @@ impl Printer { let statistics: Vec = diagnostics .messages .iter() - .map(|message| (message.kind.rule(), message.fix.is_some())) - .sorted() - .fold(vec![], |mut acc, (rule, fixable)| { - if let Some((prev_rule, _, count)) = acc.last_mut() { - if *prev_rule == rule { + .sorted_by_key(|message| (message.rule(), message.fixable())) + .fold(vec![], |mut acc: Vec<(&Message, usize)>, message| { + if let Some((prev_message, count)) = acc.last_mut() { + if prev_message.rule() == message.rule() { *count += 1; return acc; } } - acc.push((rule, fixable, 1)); + acc.push((message, 1)); acc }) .iter() - .map(|(rule, fixable, count)| ExpandedStatistics { - code: (*rule).into(), - name: (*rule).into(), - count: *count, - fixable: *fixable, + .map(|&(message, count)| ExpandedStatistics { + code: message.rule().map(std::convert::Into::into), + name: message.kind().into(), + count, + fixable: message.fixable(), }) .sorted_by_key(|statistic| Reverse(statistic.count)) .collect(); @@ -381,7 +381,12 @@ impl Printer { ); let code_width = statistics .iter() - .map(|statistic| statistic.code.to_string().len()) + .map(|statistic| { + statistic + .code + .map_or_else(String::new, |rule| rule.to_string()) + .len() + }) .max() .unwrap(); let any_fixable = statistics.iter().any(|statistic| statistic.fixable); @@ -395,7 +400,11 @@ impl Printer { writer, "{:>count_width$}\t{: unpack.py <== def function( diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E999.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E999.py deleted file mode 100644 index 8c4b6d1f63551..0000000000000 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E999.py +++ /dev/null @@ -1,4 +0,0 @@ - -def x(): - - diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index c0c63d02fe7f4..945cd2fe88fe0 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -125,7 +125,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pycodestyle, "E742") => (RuleGroup::Stable, rules::pycodestyle::rules::AmbiguousClassName), (Pycodestyle, "E743") => (RuleGroup::Stable, rules::pycodestyle::rules::AmbiguousFunctionName), (Pycodestyle, "E902") => (RuleGroup::Stable, rules::pycodestyle::rules::IOError), - (Pycodestyle, "E999") => (RuleGroup::Stable, rules::pycodestyle::rules::SyntaxError), + (Pycodestyle, "E999") => (RuleGroup::Deprecated, rules::pycodestyle::rules::SyntaxError), // pycodestyle warnings (Pycodestyle, "W191") => (RuleGroup::Stable, rules::pycodestyle::rules::TabIndentation), diff --git a/crates/ruff_linter/src/lib.rs b/crates/ruff_linter/src/lib.rs index e01601ed2581e..4fb40d4a8d215 100644 --- a/crates/ruff_linter/src/lib.rs +++ b/crates/ruff_linter/src/lib.rs @@ -11,7 +11,7 @@ pub use registry::clap_completion::RuleParser; #[cfg(feature = "clap")] pub use rule_selector::clap_completion::RuleSelectorParser; pub use rule_selector::RuleSelector; -pub use rules::pycodestyle::rules::{IOError, SyntaxError}; +pub use rules::pycodestyle::rules::IOError; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 08192b55b6fb4..bc97ac87e62ca 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -30,7 +30,6 @@ use crate::logging::DisplayParseError; use crate::message::Message; use crate::noqa::add_noqa; use crate::registry::{AsRule, Rule, RuleSet}; -use crate::rules::pycodestyle; #[cfg(any(feature = "test-rules", test))] use crate::rules::ruff::rules::test_rules::{self, TestRule, TEST_RULES}; use crate::settings::types::UnsafeFixes; @@ -85,7 +84,6 @@ pub fn check_path( ) -> LinterResult> { // Aggregate all diagnostics. let mut diagnostics = vec![]; - let mut error = None; let tokens = parsed.tokens(); let comment_ranges = indexer.comment_ranges(); @@ -142,67 +140,53 @@ pub fn check_path( )); } - // Run the AST-based rules. - let use_ast = settings - .rules - .iter_enabled() - .any(|rule_code| rule_code.lint_source().is_ast()); - let use_imports = !directives.isort.skip_file - && settings + // Run the AST-based rules only if there are no syntax errors. + if parsed.is_valid() { + let use_ast = settings .rules .iter_enabled() - .any(|rule_code| rule_code.lint_source().is_imports()); - if use_ast || use_imports || use_doc_lines { - match parsed.as_result() { - Ok(parsed) => { - let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets); - let notebook_index = source_kind.as_ipy_notebook().map(Notebook::index); - if use_ast { - diagnostics.extend(check_ast( - parsed, - locator, - stylist, - indexer, - &directives.noqa_line_for, - settings, - noqa, - path, - package, - source_type, - cell_offsets, - notebook_index, - )); - } - if use_imports { - let import_diagnostics = check_imports( - parsed, - locator, - indexer, - &directives.isort, - settings, - stylist, - package, - source_type, - cell_offsets, - ); - - diagnostics.extend(import_diagnostics); - } - if use_doc_lines { - doc_lines.extend(doc_lines_from_ast(parsed.suite(), locator)); - } + .any(|rule_code| rule_code.lint_source().is_ast()); + let use_imports = !directives.isort.skip_file + && settings + .rules + .iter_enabled() + .any(|rule_code| rule_code.lint_source().is_imports()); + if use_ast || use_imports || use_doc_lines { + let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets); + let notebook_index = source_kind.as_ipy_notebook().map(Notebook::index); + if use_ast { + diagnostics.extend(check_ast( + parsed, + locator, + stylist, + indexer, + &directives.noqa_line_for, + settings, + noqa, + path, + package, + source_type, + cell_offsets, + notebook_index, + )); } - Err(parse_errors) => { - // Always add a diagnostic for the syntax error, regardless of whether - // `Rule::SyntaxError` is enabled. We avoid propagating the syntax error - // if it's disabled via any of the usual mechanisms (e.g., `noqa`, - // `per-file-ignores`), and the easiest way to detect that suppression is - // to see if the diagnostic persists to the end of the function. - for parse_error in parse_errors { - pycodestyle::rules::syntax_error(&mut diagnostics, parse_error, locator); - } - // TODO(dhruvmanila): Remove this clone - error = parse_errors.iter().next().cloned(); + if use_imports { + let import_diagnostics = check_imports( + parsed, + locator, + indexer, + &directives.isort, + settings, + stylist, + package, + source_type, + cell_offsets, + ); + + diagnostics.extend(import_diagnostics); + } + if use_doc_lines { + doc_lines.extend(doc_lines_from_ast(parsed.suite(), locator)); } } } @@ -305,7 +289,7 @@ pub fn check_path( locator, comment_ranges, &directives.noqa_line_for, - error.is_none(), + parsed.is_valid(), &per_file_ignores, settings, ); @@ -316,23 +300,6 @@ pub fn check_path( } } - // If there was a syntax error, check if it should be discarded. - if error.is_some() { - // If the syntax error was removed by _any_ of the above disablement methods (e.g., a - // `noqa` directive, or a `per-file-ignore`), discard it. - if !diagnostics - .iter() - .any(|diagnostic| diagnostic.kind.rule() == Rule::SyntaxError) - { - error = None; - } - - // If the syntax error _diagnostic_ is disabled, discard the _diagnostic_. - if !settings.rules.enabled(Rule::SyntaxError) { - diagnostics.retain(|diagnostic| diagnostic.kind.rule() != Rule::SyntaxError); - } - } - // Remove fixes for any rules marked as unfixable. for diagnostic in &mut diagnostics { if !settings.rules.should_fix(diagnostic.kind.rule()) { @@ -352,7 +319,7 @@ pub fn check_path( } } - LinterResult::new(diagnostics, error) + LinterResult::new(diagnostics, parsed.errors().iter().next().cloned()) } const MAX_ITERATIONS: usize = 100; @@ -474,12 +441,15 @@ pub fn lint_only( &parsed, ); - result.map(|diagnostics| diagnostics_to_messages(diagnostics, path, &locator, &directives)) + result.map(|diagnostics| { + diagnostics_to_messages(diagnostics, parsed.errors(), path, &locator, &directives) + }) } /// Convert from diagnostics to messages. fn diagnostics_to_messages( diagnostics: Vec, + parse_errors: &[ParseError], path: &Path, locator: &Locator, directives: &Directives, @@ -495,12 +465,13 @@ fn diagnostics_to_messages( builder.finish() }); - diagnostics - .into_iter() - .map(|diagnostic| { + parse_errors + .iter() + .map(|parse_error| Message::from_parse_error(parse_error, locator, file.deref().clone())) + .chain(diagnostics.into_iter().map(|diagnostic| { let noqa_offset = directives.noqa_line_for.resolve(diagnostic.start()); Message::from_diagnostic(diagnostic, file.deref().clone(), noqa_offset) - }) + })) .collect() } @@ -609,7 +580,7 @@ pub fn lint_fix<'a>( return Ok(FixerResult { result: result.map(|diagnostics| { - diagnostics_to_messages(diagnostics, path, &locator, &directives) + diagnostics_to_messages(diagnostics, parsed.errors(), path, &locator, &directives) }), transformed, fixed, diff --git a/crates/ruff_linter/src/message/azure.rs b/crates/ruff_linter/src/message/azure.rs index 76245ca16dda4..c7d6049eac049 100644 --- a/crates/ruff_linter/src/message/azure.rs +++ b/crates/ruff_linter/src/message/azure.rs @@ -3,7 +3,6 @@ use std::io::Write; use ruff_source_file::SourceLocation; use crate::message::{Emitter, EmitterContext, Message}; -use crate::registry::AsRule; /// Generate error logging commands for Azure Pipelines format. /// See [documentation](https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#logissue-log-an-error-or-warning) @@ -29,12 +28,14 @@ impl Emitter for AzureEmitter { writeln!( writer, "##vso[task.logissue type=error\ - ;sourcepath={filename};linenumber={line};columnnumber={col};code={code};]{body}", + ;sourcepath={filename};linenumber={line};columnnumber={col};{code}]{body}", filename = message.filename(), line = location.row, col = location.column, - code = message.kind.rule().noqa_code(), - body = message.kind.body, + code = message + .rule() + .map_or_else(String::new, |rule| format!("code={};", rule.noqa_code())), + body = message.body(), )?; } @@ -46,7 +47,9 @@ impl Emitter for AzureEmitter { mod tests { use insta::assert_snapshot; - use crate::message::tests::{capture_emitter_output, create_messages}; + use crate::message::tests::{ + capture_emitter_output, create_messages, create_syntax_error_messages, + }; use crate::message::AzureEmitter; #[test] @@ -56,4 +59,12 @@ mod tests { assert_snapshot!(content); } + + #[test] + fn syntax_errors() { + let mut emitter = AzureEmitter; + let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + + assert_snapshot!(content); + } } diff --git a/crates/ruff_linter/src/message/diff.rs b/crates/ruff_linter/src/message/diff.rs index 0ba2765014500..e3b3d99290c82 100644 --- a/crates/ruff_linter/src/message/diff.rs +++ b/crates/ruff_linter/src/message/diff.rs @@ -27,8 +27,8 @@ pub(super) struct Diff<'a> { impl<'a> Diff<'a> { pub(crate) fn from_message(message: &'a Message) -> Option { - message.fix.as_ref().map(|fix| Diff { - source_code: &message.file, + message.fix().map(|fix| Diff { + source_code: message.source_file(), fix, }) } diff --git a/crates/ruff_linter/src/message/github.rs b/crates/ruff_linter/src/message/github.rs index 7ce6dee159415..9fd0a5ee6b912 100644 --- a/crates/ruff_linter/src/message/github.rs +++ b/crates/ruff_linter/src/message/github.rs @@ -4,7 +4,6 @@ use ruff_source_file::SourceLocation; use crate::fs::relativize_path; use crate::message::{Emitter, EmitterContext, Message}; -use crate::registry::AsRule; /// Generate error workflow command in GitHub Actions format. /// See: [GitHub documentation](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message) @@ -32,9 +31,8 @@ impl Emitter for GithubEmitter { write!( writer, - "::error title=Ruff \ - ({code}),file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::", - code = message.kind.rule().noqa_code(), + "::error title=Ruff{code},file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::", + code = message.rule().map_or_else(String::new, |rule| format!(" ({})", rule.noqa_code())), file = message.filename(), row = source_location.row, column = source_location.column, @@ -42,15 +40,19 @@ impl Emitter for GithubEmitter { end_column = end_location.column, )?; - writeln!( + write!( writer, - "{path}:{row}:{column}: {code} {body}", + "{path}:{row}:{column}:", path = relativize_path(message.filename()), row = location.row, column = location.column, - code = message.kind.rule().noqa_code(), - body = message.kind.body, )?; + + if let Some(rule) = message.rule() { + write!(writer, " {}", rule.noqa_code())?; + } + + writeln!(writer, " {}", message.body())?; } Ok(()) @@ -61,7 +63,9 @@ impl Emitter for GithubEmitter { mod tests { use insta::assert_snapshot; - use crate::message::tests::{capture_emitter_output, create_messages}; + use crate::message::tests::{ + capture_emitter_output, create_messages, create_syntax_error_messages, + }; use crate::message::GithubEmitter; #[test] @@ -71,4 +75,12 @@ mod tests { assert_snapshot!(content); } + + #[test] + fn syntax_errors() { + let mut emitter = GithubEmitter; + let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + + assert_snapshot!(content); + } } diff --git a/crates/ruff_linter/src/message/gitlab.rs b/crates/ruff_linter/src/message/gitlab.rs index dcf9ab1615331..15e72832a2e61 100644 --- a/crates/ruff_linter/src/message/gitlab.rs +++ b/crates/ruff_linter/src/message/gitlab.rs @@ -9,7 +9,6 @@ use serde_json::json; use crate::fs::{relativize_path, relativize_path_to}; use crate::message::{Emitter, EmitterContext, Message}; -use crate::registry::AsRule; /// Generate JSON with violations in GitLab CI format // https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool @@ -91,8 +90,14 @@ impl Serialize for SerializedMessages<'_> { } fingerprints.insert(message_fingerprint); + let description = if let Some(rule) = message.rule() { + format!("({}) {}", rule.noqa_code(), message.body()) + } else { + message.body().to_string() + }; + let value = json!({ - "description": format!("({}) {}", message.kind.rule().noqa_code(), message.kind.body), + "description": description, "severity": "major", "fingerprint": format!("{:x}", message_fingerprint), "location": { @@ -110,18 +115,10 @@ impl Serialize for SerializedMessages<'_> { /// Generate a unique fingerprint to identify a violation. fn fingerprint(message: &Message, project_path: &str, salt: u64) -> u64 { - let Message { - kind, - range: _, - fix: _fix, - file: _, - noqa_offset: _, - } = message; - let mut hasher = DefaultHasher::new(); salt.hash(&mut hasher); - kind.name.hash(&mut hasher); + message.name().hash(&mut hasher); project_path.hash(&mut hasher); hasher.finish() @@ -131,7 +128,9 @@ fn fingerprint(message: &Message, project_path: &str, salt: u64) -> u64 { mod tests { use insta::assert_snapshot; - use crate::message::tests::{capture_emitter_output, create_messages}; + use crate::message::tests::{ + capture_emitter_output, create_messages, create_syntax_error_messages, + }; use crate::message::GitlabEmitter; #[test] @@ -142,6 +141,14 @@ mod tests { assert_snapshot!(redact_fingerprint(&content)); } + #[test] + fn syntax_errors() { + let mut emitter = GitlabEmitter::default(); + let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + + assert_snapshot!(redact_fingerprint(&content)); + } + // Redact the fingerprint because the default hasher isn't stable across platforms. fn redact_fingerprint(content: &str) -> String { static FINGERPRINT_HAY_KEY: &str = r#""fingerprint": ""#; diff --git a/crates/ruff_linter/src/message/grouped.rs b/crates/ruff_linter/src/message/grouped.rs index 0445c1746193b..1dfa5d15e6b2b 100644 --- a/crates/ruff_linter/src/message/grouped.rs +++ b/crates/ruff_linter/src/message/grouped.rs @@ -205,7 +205,9 @@ impl std::fmt::Write for PadAdapter<'_> { mod tests { use insta::assert_snapshot; - use crate::message::tests::{capture_emitter_output, create_messages}; + use crate::message::tests::{ + capture_emitter_output, create_messages, create_syntax_error_messages, + }; use crate::message::GroupedEmitter; use crate::settings::types::UnsafeFixes; @@ -217,6 +219,14 @@ mod tests { assert_snapshot!(content); } + #[test] + fn syntax_errors() { + let mut emitter = GroupedEmitter::default(); + let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + + assert_snapshot!(content); + } + #[test] fn show_source() { let mut emitter = GroupedEmitter::default().with_show_source(true); diff --git a/crates/ruff_linter/src/message/json.rs b/crates/ruff_linter/src/message/json.rs index 7c3b9764f3831..eaa968c167b5b 100644 --- a/crates/ruff_linter/src/message/json.rs +++ b/crates/ruff_linter/src/message/json.rs @@ -10,7 +10,6 @@ use ruff_source_file::{OneIndexed, SourceCode, SourceLocation}; use ruff_text_size::Ranged; use crate::message::{Emitter, EmitterContext, Message}; -use crate::registry::AsRule; #[derive(Default)] pub struct JsonEmitter; @@ -50,20 +49,22 @@ impl Serialize for ExpandedMessages<'_> { } pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext) -> Value { - let source_code = message.file.to_source_code(); + let source_code = message.source_file().to_source_code(); let notebook_index = context.notebook_index(message.filename()); - let fix = message.fix.as_ref().map(|fix| { + let fix = message.fix().map(|fix| { json!({ "applicability": fix.applicability(), - "message": message.kind.suggestion.as_deref(), + "message": message.suggestion(), "edits": &ExpandedEdits { edits: fix.edits(), source_code: &source_code, notebook_index }, }) }); let mut start_location = source_code.source_location(message.start()); let mut end_location = source_code.source_location(message.end()); - let mut noqa_location = source_code.source_location(message.noqa_offset); + let mut noqa_location = message + .noqa_offset() + .map(|offset| source_code.source_location(offset)); let mut notebook_cell_index = None; if let Some(notebook_index) = notebook_index { @@ -74,19 +75,19 @@ pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext) ); start_location = notebook_index.translate_location(&start_location); end_location = notebook_index.translate_location(&end_location); - noqa_location = notebook_index.translate_location(&noqa_location); + noqa_location = noqa_location.map(|location| notebook_index.translate_location(&location)); } json!({ - "code": message.kind.rule().noqa_code().to_string(), - "url": message.kind.rule().url(), - "message": message.kind.body, + "code": message.rule().map(|rule| rule.noqa_code().to_string()), + "url": message.rule().and_then(|rule| rule.url()), + "message": message.body(), "fix": fix, "cell": notebook_cell_index, "location": start_location, "end_location": end_location, "filename": message.filename(), - "noqa_row": noqa_location.row + "noqa_row": noqa_location.map(|location| location.row) }) } @@ -170,7 +171,7 @@ mod tests { use crate::message::tests::{ capture_emitter_notebook_output, capture_emitter_output, create_messages, - create_notebook_messages, + create_notebook_messages, create_syntax_error_messages, }; use crate::message::JsonEmitter; @@ -182,6 +183,14 @@ mod tests { assert_snapshot!(content); } + #[test] + fn syntax_errors() { + let mut emitter = JsonEmitter; + let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + + assert_snapshot!(content); + } + #[test] fn notebook_output() { let mut emitter = JsonEmitter; diff --git a/crates/ruff_linter/src/message/json_lines.rs b/crates/ruff_linter/src/message/json_lines.rs index 25b8cc1d5b32b..f939f921dc0f0 100644 --- a/crates/ruff_linter/src/message/json_lines.rs +++ b/crates/ruff_linter/src/message/json_lines.rs @@ -29,7 +29,7 @@ mod tests { use crate::message::json_lines::JsonLinesEmitter; use crate::message::tests::{ capture_emitter_notebook_output, capture_emitter_output, create_messages, - create_notebook_messages, + create_notebook_messages, create_syntax_error_messages, }; #[test] @@ -40,6 +40,14 @@ mod tests { assert_snapshot!(content); } + #[test] + fn syntax_errors() { + let mut emitter = JsonLinesEmitter; + let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + + assert_snapshot!(content); + } + #[test] fn notebook_output() { let mut emitter = JsonLinesEmitter; diff --git a/crates/ruff_linter/src/message/junit.rs b/crates/ruff_linter/src/message/junit.rs index 8d4697f3f80ea..05a48e0a0db78 100644 --- a/crates/ruff_linter/src/message/junit.rs +++ b/crates/ruff_linter/src/message/junit.rs @@ -8,7 +8,6 @@ use ruff_source_file::SourceLocation; use crate::message::{ group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation, }; -use crate::registry::AsRule; #[derive(Default)] pub struct JunitEmitter; @@ -44,7 +43,7 @@ impl Emitter for JunitEmitter { start_location, } = message; let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure); - status.set_message(message.kind.body.clone()); + status.set_message(message.body()); let location = if context.is_notebook(message.filename()) { // We can't give a reasonable location for the structured formats, // so we show one that's clearly a fallback @@ -57,10 +56,14 @@ impl Emitter for JunitEmitter { "line {row}, col {col}, {body}", row = location.row, col = location.column, - body = message.kind.body + body = message.body() )); let mut case = TestCase::new( - format!("org.ruff.{}", message.kind.rule().noqa_code()), + if let Some(rule) = message.rule() { + format!("org.ruff.{}", rule.noqa_code()) + } else { + "org.ruff".to_string() + }, status, ); let file_path = Path::new(filename); @@ -88,7 +91,9 @@ impl Emitter for JunitEmitter { mod tests { use insta::assert_snapshot; - use crate::message::tests::{capture_emitter_output, create_messages}; + use crate::message::tests::{ + capture_emitter_output, create_messages, create_syntax_error_messages, + }; use crate::message::JunitEmitter; #[test] @@ -98,4 +103,12 @@ mod tests { assert_snapshot!(content); } + + #[test] + fn syntax_errors() { + let mut emitter = JunitEmitter; + let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + + assert_snapshot!(content); + } } diff --git a/crates/ruff_linter/src/message/mod.rs b/crates/ruff_linter/src/message/mod.rs index 7e95fc9d14cba..4c6068f3761a8 100644 --- a/crates/ruff_linter/src/message/mod.rs +++ b/crates/ruff_linter/src/message/mod.rs @@ -14,13 +14,18 @@ pub use json_lines::JsonLinesEmitter; pub use junit::JunitEmitter; pub use pylint::PylintEmitter; pub use rdjson::RdjsonEmitter; -use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix}; -use ruff_notebook::NotebookIndex; -use ruff_source_file::{SourceFile, SourceLocation}; -use ruff_text_size::{Ranged, TextRange, TextSize}; pub use sarif::SarifEmitter; pub use text::TextEmitter; +use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix}; +use ruff_notebook::NotebookIndex; +use ruff_python_parser::ParseError; +use ruff_source_file::{Locator, SourceFile, SourceLocation}; +use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; + +use crate::logging::DisplayParseErrorType; +use crate::registry::{AsRule, Rule}; + mod azure; mod diff; mod github; @@ -34,8 +39,17 @@ mod rdjson; mod sarif; mod text; +/// Message represents either a diagnostic message corresponding to a rule violation or a syntax +/// error message raised by the parser. #[derive(Debug, PartialEq, Eq)] -pub struct Message { +pub enum Message { + Diagnostic(DiagnosticMessage), + SyntaxError(SyntaxErrorMessage), +} + +/// A diagnostic message corresponding to a rule violation. +#[derive(Debug, PartialEq, Eq)] +pub struct DiagnosticMessage { pub kind: DiagnosticKind, pub range: TextRange, pub fix: Option, @@ -43,37 +57,174 @@ pub struct Message { pub noqa_offset: TextSize, } +/// A syntax error message raised by the parser. +#[derive(Debug, PartialEq, Eq)] +pub struct SyntaxErrorMessage { + pub message: String, + pub range: TextRange, + pub file: SourceFile, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum MessageKind { + Diagnostic(Rule), + SyntaxError, +} + +impl MessageKind { + pub fn as_str(&self) -> &str { + match self { + MessageKind::Diagnostic(rule) => rule.as_ref(), + MessageKind::SyntaxError => "syntax-error", + } + } +} + impl Message { + /// Create a [`Message`] from the given [`Diagnostic`] corresponding to a rule violation. pub fn from_diagnostic( diagnostic: Diagnostic, file: SourceFile, noqa_offset: TextSize, - ) -> Self { - Self { + ) -> Message { + Message::Diagnostic(DiagnosticMessage { range: diagnostic.range(), kind: diagnostic.kind, fix: diagnostic.fix, file, noqa_offset, + }) + } + + /// Create a [`Message`] from the given [`ParseError`]. + pub fn from_parse_error( + parse_error: &ParseError, + locator: &Locator, + file: SourceFile, + ) -> Message { + // Try to create a non-empty range so that the diagnostic can print a caret at the right + // position. This requires that we retrieve the next character, if any, and take its length + // to maintain char-boundaries. + let len = locator + .after(parse_error.location.start()) + .chars() + .next() + .map_or(TextSize::new(0), TextLen::text_len); + + Message::SyntaxError(SyntaxErrorMessage { + message: format!( + "SyntaxError: {}", + DisplayParseErrorType::new(&parse_error.error) + ), + range: TextRange::at(parse_error.location.start(), len), + file, + }) + } + + pub const fn as_diagnostic_message(&self) -> Option<&DiagnosticMessage> { + match self { + Message::Diagnostic(m) => Some(m), + Message::SyntaxError(_) => None, + } + } + + /// Returns `true` if `self` is a syntax error message. + pub const fn is_syntax_error(&self) -> bool { + matches!(self, Message::SyntaxError(_)) + } + + /// Returns a message kind. + pub fn kind(&self) -> MessageKind { + match self { + Message::Diagnostic(m) => MessageKind::Diagnostic(m.kind.rule()), + Message::SyntaxError(_) => MessageKind::SyntaxError, + } + } + + /// Returns the name used to represent the diagnostic. + pub fn name(&self) -> &str { + match self { + Message::Diagnostic(m) => &m.kind.name, + Message::SyntaxError(_) => "SyntaxError", + } + } + + /// Returns the message body to display to the user. + pub fn body(&self) -> &str { + match self { + Message::Diagnostic(m) => &m.kind.body, + Message::SyntaxError(m) => &m.message, + } + } + + /// Returns the fix suggestion for the violation. + pub fn suggestion(&self) -> Option<&str> { + match self { + Message::Diagnostic(m) => m.kind.suggestion.as_deref(), + Message::SyntaxError(_) => None, + } + } + + /// Returns the offset at which the `noqa` comment will be placed if it's a diagnostic message. + pub fn noqa_offset(&self) -> Option { + match self { + Message::Diagnostic(m) => Some(m.noqa_offset), + Message::SyntaxError(_) => None, + } + } + + /// Returns the [`Fix`] for the message, if there is any. + pub fn fix(&self) -> Option<&Fix> { + match self { + Message::Diagnostic(m) => m.fix.as_ref(), + Message::SyntaxError(_) => None, + } + } + + /// Returns `true` if the message contains a [`Fix`]. + pub fn fixable(&self) -> bool { + self.fix().is_some() + } + + /// Returns the [`Rule`] corresponding to the diagnostic message. + pub fn rule(&self) -> Option { + match self { + Message::Diagnostic(m) => Some(m.kind.rule()), + Message::SyntaxError(_) => None, } } + /// Returns the filename for the message. pub fn filename(&self) -> &str { - self.file.name() + self.source_file().name() } + /// Computes the start source location for the message. pub fn compute_start_location(&self) -> SourceLocation { - self.file.to_source_code().source_location(self.start()) + self.source_file() + .to_source_code() + .source_location(self.start()) } + /// Computes the end source location for the message. pub fn compute_end_location(&self) -> SourceLocation { - self.file.to_source_code().source_location(self.end()) + self.source_file() + .to_source_code() + .source_location(self.end()) + } + + /// Returns the [`SourceFile`] which the message belongs to. + pub fn source_file(&self) -> &SourceFile { + match self { + Message::Diagnostic(m) => &m.file, + Message::SyntaxError(m) => &m.file, + } } } impl Ord for Message { fn cmp(&self, other: &Self) -> Ordering { - (&self.file, self.start()).cmp(&(&other.file, other.start())) + (self.source_file(), self.start()).cmp(&(other.source_file(), other.start())) } } @@ -85,7 +236,10 @@ impl PartialOrd for Message { impl Ranged for Message { fn range(&self) -> TextRange { - self.range + match self { + Message::Diagnostic(m) => m.range, + Message::SyntaxError(m) => m.range, + } } } @@ -155,11 +309,30 @@ mod tests { use ruff_diagnostics::{Diagnostic, DiagnosticKind, Edit, Fix}; use ruff_notebook::NotebookIndex; - use ruff_source_file::{OneIndexed, SourceFileBuilder}; + use ruff_python_parser::{parse_unchecked, Mode}; + use ruff_source_file::{Locator, OneIndexed, SourceFileBuilder}; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::message::{Emitter, EmitterContext, Message}; + pub(super) fn create_syntax_error_messages() -> Vec { + let source = r"from os import + +if call(foo + def bar(): + pass +"; + let locator = Locator::new(source); + let source_file = SourceFileBuilder::new("syntax_errors.py", source).finish(); + parse_unchecked(source, Mode::Module) + .errors() + .iter() + .map(|parse_error| { + Message::from_parse_error(parse_error, &locator, source_file.clone()) + }) + .collect() + } + pub(super) fn create_messages() -> Vec { let fib = r#"import os diff --git a/crates/ruff_linter/src/message/pylint.rs b/crates/ruff_linter/src/message/pylint.rs index 18f4517650bbc..10b1f81f1076d 100644 --- a/crates/ruff_linter/src/message/pylint.rs +++ b/crates/ruff_linter/src/message/pylint.rs @@ -4,7 +4,6 @@ use ruff_source_file::OneIndexed; use crate::fs::relativize_path; use crate::message::{Emitter, EmitterContext, Message}; -use crate::registry::AsRule; /// Generate violations in Pylint format. /// See: [Flake8 documentation](https://flake8.pycqa.org/en/latest/internal/formatters.html#pylint-formatter) @@ -27,12 +26,20 @@ impl Emitter for PylintEmitter { message.compute_start_location().row }; + let body = if let Some(rule) = message.rule() { + format!( + "[{code}] {body}", + code = rule.noqa_code(), + body = message.body() + ) + } else { + message.body().to_string() + }; + writeln!( writer, - "{path}:{row}: [{code}] {body}", + "{path}:{row}: {body}", path = relativize_path(message.filename()), - code = message.kind.rule().noqa_code(), - body = message.kind.body, )?; } @@ -44,7 +51,9 @@ impl Emitter for PylintEmitter { mod tests { use insta::assert_snapshot; - use crate::message::tests::{capture_emitter_output, create_messages}; + use crate::message::tests::{ + capture_emitter_output, create_messages, create_syntax_error_messages, + }; use crate::message::PylintEmitter; #[test] @@ -54,4 +63,12 @@ mod tests { assert_snapshot!(content); } + + #[test] + fn syntax_errors() { + let mut emitter = PylintEmitter; + let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + + assert_snapshot!(content); + } } diff --git a/crates/ruff_linter/src/message/rdjson.rs b/crates/ruff_linter/src/message/rdjson.rs index 9d3ff50411f2a..99b3fc481e257 100644 --- a/crates/ruff_linter/src/message/rdjson.rs +++ b/crates/ruff_linter/src/message/rdjson.rs @@ -9,7 +9,6 @@ use ruff_source_file::SourceCode; use ruff_text_size::Ranged; use crate::message::{Emitter, EmitterContext, Message, SourceLocation}; -use crate::registry::AsRule; #[derive(Default)] pub struct RdjsonEmitter; @@ -58,34 +57,34 @@ impl Serialize for ExpandedMessages<'_> { } fn message_to_rdjson_value(message: &Message) -> Value { - let source_code = message.file.to_source_code(); + let source_code = message.source_file().to_source_code(); let start_location = source_code.source_location(message.start()); let end_location = source_code.source_location(message.end()); - if let Some(fix) = message.fix.as_ref() { + if let Some(fix) = message.fix() { json!({ - "message": message.kind.body, + "message": message.body(), "location": { "path": message.filename(), "range": rdjson_range(&start_location, &end_location), }, "code": { - "value": message.kind.rule().noqa_code().to_string(), - "url": message.kind.rule().url(), + "value": message.rule().map(|rule| rule.noqa_code().to_string()), + "url": message.rule().and_then(|rule| rule.url()), }, "suggestions": rdjson_suggestions(fix.edits(), &source_code), }) } else { json!({ - "message": message.kind.body, + "message": message.body(), "location": { "path": message.filename(), "range": rdjson_range(&start_location, &end_location), }, "code": { - "value": message.kind.rule().noqa_code().to_string(), - "url": message.kind.rule().url(), + "value": message.rule().map(|rule| rule.noqa_code().to_string()), + "url": message.rule().and_then(|rule| rule.url()), }, }) } @@ -125,7 +124,9 @@ fn rdjson_range(start: &SourceLocation, end: &SourceLocation) -> Value { mod tests { use insta::assert_snapshot; - use crate::message::tests::{capture_emitter_output, create_messages}; + use crate::message::tests::{ + capture_emitter_output, create_messages, create_syntax_error_messages, + }; use crate::message::RdjsonEmitter; #[test] @@ -135,4 +136,12 @@ mod tests { assert_snapshot!(content); } + + #[test] + fn syntax_errors() { + let mut emitter = RdjsonEmitter; + let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + + assert_snapshot!(content); + } } diff --git a/crates/ruff_linter/src/message/sarif.rs b/crates/ruff_linter/src/message/sarif.rs index 3517c0eee335a..0c53478425a03 100644 --- a/crates/ruff_linter/src/message/sarif.rs +++ b/crates/ruff_linter/src/message/sarif.rs @@ -3,17 +3,16 @@ use std::io::Write; use anyhow::Result; use serde::{Serialize, Serializer}; use serde_json::json; +use strum::IntoEnumIterator; use ruff_source_file::OneIndexed; use crate::codes::Rule; use crate::fs::normalize_path; use crate::message::{Emitter, EmitterContext, Message}; -use crate::registry::{AsRule, Linter, RuleNamespace}; +use crate::registry::{Linter, RuleNamespace}; use crate::VERSION; -use strum::IntoEnumIterator; - pub struct SarifEmitter; impl Emitter for SarifEmitter { @@ -103,7 +102,7 @@ impl Serialize for SarifRule<'_> { #[derive(Debug)] struct SarifResult { - rule: Rule, + rule: Option, level: String, message: String, uri: String, @@ -120,9 +119,9 @@ impl SarifResult { let end_location = message.compute_end_location(); let path = normalize_path(message.filename()); Ok(Self { - rule: message.kind.rule(), + rule: message.rule(), level: "error".to_string(), - message: message.kind.name.clone(), + message: message.name().to_string(), uri: url::Url::from_file_path(&path) .map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display()))? .to_string(), @@ -140,9 +139,9 @@ impl SarifResult { let end_location = message.compute_end_location(); let path = normalize_path(message.filename()); Ok(Self { - rule: message.kind.rule(), + rule: message.rule(), level: "error".to_string(), - message: message.kind.name.clone(), + message: message.name().to_string(), uri: path.display().to_string(), start_line: start_location.row, start_column: start_location.column, @@ -175,7 +174,7 @@ impl Serialize for SarifResult { } } }], - "ruleId": self.rule.noqa_code().to_string(), + "ruleId": self.rule.map(|rule| rule.noqa_code().to_string()), }) .serialize(serializer) } @@ -184,7 +183,9 @@ impl Serialize for SarifResult { #[cfg(test)] mod tests { - use crate::message::tests::{capture_emitter_output, create_messages}; + use crate::message::tests::{ + capture_emitter_output, create_messages, create_syntax_error_messages, + }; use crate::message::SarifEmitter; fn get_output() -> String { @@ -198,6 +199,13 @@ mod tests { serde_json::from_str::(&content).unwrap(); } + #[test] + fn valid_syntax_error_json() { + let mut emitter = SarifEmitter {}; + let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + serde_json::from_str::(&content).unwrap(); + } + #[test] fn test_results() { let content = get_output(); diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__azure__tests__syntax_errors.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__azure__tests__syntax_errors.snap new file mode 100644 index 0000000000000..8c57205239b4a --- /dev/null +++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__azure__tests__syntax_errors.snap @@ -0,0 +1,6 @@ +--- +source: crates/ruff_linter/src/message/azure.rs +expression: content +--- +##vso[task.logissue type=error;sourcepath=syntax_errors.py;linenumber=1;columnnumber=15;]SyntaxError: Expected one or more symbol names after import +##vso[task.logissue type=error;sourcepath=syntax_errors.py;linenumber=3;columnnumber=12;]SyntaxError: Expected ')', found newline diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__github__tests__syntax_errors.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__github__tests__syntax_errors.snap new file mode 100644 index 0000000000000..e7444371c6797 --- /dev/null +++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__github__tests__syntax_errors.snap @@ -0,0 +1,6 @@ +--- +source: crates/ruff_linter/src/message/github.rs +expression: content +--- +::error title=Ruff,file=syntax_errors.py,line=1,col=15,endLine=2,endColumn=1::syntax_errors.py:1:15: SyntaxError: Expected one or more symbol names after import +::error title=Ruff,file=syntax_errors.py,line=3,col=12,endLine=4,endColumn=1::syntax_errors.py:3:12: SyntaxError: Expected ')', found newline diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__gitlab__tests__syntax_errors.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__gitlab__tests__syntax_errors.snap new file mode 100644 index 0000000000000..27c3c4a3ac3e9 --- /dev/null +++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__gitlab__tests__syntax_errors.snap @@ -0,0 +1,30 @@ +--- +source: crates/ruff_linter/src/message/gitlab.rs +expression: redact_fingerprint(&content) +--- +[ + { + "description": "SyntaxError: Expected one or more symbol names after import", + "fingerprint": "", + "location": { + "lines": { + "begin": 1, + "end": 2 + }, + "path": "syntax_errors.py" + }, + "severity": "major" + }, + { + "description": "SyntaxError: Expected ')', found newline", + "fingerprint": "", + "location": { + "lines": { + "begin": 3, + "end": 4 + }, + "path": "syntax_errors.py" + }, + "severity": "major" + } +] diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__grouped__tests__syntax_errors.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__grouped__tests__syntax_errors.snap new file mode 100644 index 0000000000000..17e7e59a3e9b0 --- /dev/null +++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__grouped__tests__syntax_errors.snap @@ -0,0 +1,7 @@ +--- +source: crates/ruff_linter/src/message/grouped.rs +expression: content +--- +syntax_errors.py: + 1:15 SyntaxError: Expected one or more symbol names after import + 3:12 SyntaxError: Expected ')', found newline diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__json__tests__syntax_errors.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__json__tests__syntax_errors.snap new file mode 100644 index 0000000000000..1a7fa5b1e4f7b --- /dev/null +++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__json__tests__syntax_errors.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff_linter/src/message/json.rs +expression: content +--- +[ + { + "cell": null, + "code": null, + "end_location": { + "column": 1, + "row": 2 + }, + "filename": "syntax_errors.py", + "fix": null, + "location": { + "column": 15, + "row": 1 + }, + "message": "SyntaxError: Expected one or more symbol names after import", + "noqa_row": null, + "url": null + }, + { + "cell": null, + "code": null, + "end_location": { + "column": 1, + "row": 4 + }, + "filename": "syntax_errors.py", + "fix": null, + "location": { + "column": 12, + "row": 3 + }, + "message": "SyntaxError: Expected ')', found newline", + "noqa_row": null, + "url": null + } +] diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__json_lines__tests__syntax_errors.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__json_lines__tests__syntax_errors.snap new file mode 100644 index 0000000000000..326913dcdfb10 --- /dev/null +++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__json_lines__tests__syntax_errors.snap @@ -0,0 +1,6 @@ +--- +source: crates/ruff_linter/src/message/json_lines.rs +expression: content +--- +{"cell":null,"code":null,"end_location":{"column":1,"row":2},"filename":"syntax_errors.py","fix":null,"location":{"column":15,"row":1},"message":"SyntaxError: Expected one or more symbol names after import","noqa_row":null,"url":null} +{"cell":null,"code":null,"end_location":{"column":1,"row":4},"filename":"syntax_errors.py","fix":null,"location":{"column":12,"row":3},"message":"SyntaxError: Expected ')', found newline","noqa_row":null,"url":null} diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__junit__tests__syntax_errors.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__junit__tests__syntax_errors.snap new file mode 100644 index 0000000000000..c8015a9d5e300 --- /dev/null +++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__junit__tests__syntax_errors.snap @@ -0,0 +1,15 @@ +--- +source: crates/ruff_linter/src/message/junit.rs +expression: content +--- + + + + + line 1, col 15, SyntaxError: Expected one or more symbol names after import + + + line 3, col 12, SyntaxError: Expected ')', found newline + + + diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__pylint__tests__syntax_errors.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__pylint__tests__syntax_errors.snap new file mode 100644 index 0000000000000..bc6b33b48e284 --- /dev/null +++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__pylint__tests__syntax_errors.snap @@ -0,0 +1,6 @@ +--- +source: crates/ruff_linter/src/message/pylint.rs +expression: content +--- +syntax_errors.py:1: SyntaxError: Expected one or more symbol names after import +syntax_errors.py:3: SyntaxError: Expected ')', found newline diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__rdjson__tests__syntax_errors.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__rdjson__tests__syntax_errors.snap new file mode 100644 index 0000000000000..c73c784b19b44 --- /dev/null +++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__rdjson__tests__syntax_errors.snap @@ -0,0 +1,53 @@ +--- +source: crates/ruff_linter/src/message/rdjson.rs +expression: content +--- +{ + "diagnostics": [ + { + "code": { + "url": null, + "value": null + }, + "location": { + "path": "syntax_errors.py", + "range": { + "end": { + "column": 1, + "line": 2 + }, + "start": { + "column": 15, + "line": 1 + } + } + }, + "message": "SyntaxError: Expected one or more symbol names after import" + }, + { + "code": { + "url": null, + "value": null + }, + "location": { + "path": "syntax_errors.py", + "range": { + "end": { + "column": 1, + "line": 4 + }, + "start": { + "column": 12, + "line": 3 + } + } + }, + "message": "SyntaxError: Expected ')', found newline" + } + ], + "severity": "warning", + "source": { + "name": "ruff", + "url": "https://docs.astral.sh/ruff" + } +} diff --git a/crates/ruff_linter/src/message/snapshots/ruff_linter__message__text__tests__syntax_errors.snap b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__text__tests__syntax_errors.snap new file mode 100644 index 0000000000000..618774276b3b5 --- /dev/null +++ b/crates/ruff_linter/src/message/snapshots/ruff_linter__message__text__tests__syntax_errors.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff_linter/src/message/text.rs +expression: content +--- +syntax_errors.py:1:15: SyntaxError: Expected one or more symbol names after import + | +1 | from os import + | ^ +2 | +3 | if call(foo +4 | def bar(): + | + +syntax_errors.py:3:12: SyntaxError: Expected ')', found newline + | +1 | from os import +2 | +3 | if call(foo + | ^ +4 | def bar(): +5 | pass + | diff --git a/crates/ruff_linter/src/message/text.rs b/crates/ruff_linter/src/message/text.rs index 6e104e49af2a5..ed74f5a495bb6 100644 --- a/crates/ruff_linter/src/message/text.rs +++ b/crates/ruff_linter/src/message/text.rs @@ -15,7 +15,6 @@ use crate::fs::relativize_path; use crate::line_width::{IndentWidth, LineWidthBuilder}; use crate::message::diff::Diff; use crate::message::{Emitter, EmitterContext, Message}; -use crate::registry::AsRule; use crate::settings::types::UnsafeFixes; use crate::text_helpers::ShowNonprinting; @@ -146,28 +145,33 @@ pub(super) struct RuleCodeAndBody<'a> { impl Display for RuleCodeAndBody<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let kind = &self.message.kind; if self.show_fix_status { - if let Some(fix) = self.message.fix.as_ref() { + if let Some(fix) = self.message.fix() { // Do not display an indicator for unapplicable fixes if fix.applies(self.unsafe_fixes.required_applicability()) { + if let Some(rule) = self.message.rule() { + write!(f, "{} ", rule.noqa_code().to_string().red().bold())?; + } return write!( f, - "{code} {fix}{body}", - code = kind.rule().noqa_code().to_string().red().bold(), + "{fix}{body}", fix = format_args!("[{}] ", "*".cyan()), - body = kind.body, + body = self.message.body(), ); } } }; - write!( - f, - "{code} {body}", - code = kind.rule().noqa_code().to_string().red().bold(), - body = kind.body, - ) + if let Some(rule) = self.message.rule() { + write!( + f, + "{code} {body}", + code = rule.noqa_code().to_string().red().bold(), + body = self.message.body(), + ) + } else { + f.write_str(self.message.body()) + } } } @@ -178,11 +182,7 @@ pub(super) struct MessageCodeFrame<'a> { impl Display for MessageCodeFrame<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let Message { - kind, file, range, .. - } = self.message; - - let suggestion = kind.suggestion.as_deref(); + let suggestion = self.message.suggestion(); let footer = if suggestion.is_some() { vec![Annotation { id: None, @@ -193,9 +193,9 @@ impl Display for MessageCodeFrame<'_> { Vec::new() }; - let source_code = file.to_source_code(); + let source_code = self.message.source_file().to_source_code(); - let content_start_index = source_code.line_index(range.start()); + let content_start_index = source_code.line_index(self.message.start()); let mut start_index = content_start_index.saturating_sub(2); // If we're working with a Jupyter Notebook, skip the lines which are @@ -218,7 +218,7 @@ impl Display for MessageCodeFrame<'_> { start_index = start_index.saturating_add(1); } - let content_end_index = source_code.line_index(range.end()); + let content_end_index = source_code.line_index(self.message.end()); let mut end_index = content_end_index .saturating_add(2) .min(OneIndexed::from_zero_indexed(source_code.line_count())); @@ -249,7 +249,7 @@ impl Display for MessageCodeFrame<'_> { let source = replace_whitespace( source_code.slice(TextRange::new(start_offset, end_offset)), - range - start_offset, + self.message.range() - start_offset, ); let source_text = source.text.show_nonprinting(); @@ -260,7 +260,10 @@ impl Display for MessageCodeFrame<'_> { let char_length = source.text[source.annotation_range].chars().count(); - let label = kind.rule().noqa_code().to_string(); + let label = self + .message + .rule() + .map_or_else(String::new, |rule| rule.noqa_code().to_string()); let snippet = Snippet { title: None, @@ -356,7 +359,7 @@ mod tests { use crate::message::tests::{ capture_emitter_notebook_output, capture_emitter_output, create_messages, - create_notebook_messages, + create_notebook_messages, create_syntax_error_messages, }; use crate::message::TextEmitter; use crate::settings::types::UnsafeFixes; @@ -401,4 +404,12 @@ mod tests { assert_snapshot!(content); } + + #[test] + fn syntax_errors() { + let mut emitter = TextEmitter::default().with_show_source(true); + let content = capture_emitter_output(&mut emitter, &create_syntax_error_messages()); + + assert_snapshot!(content); + } } diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index dae0b0cf66651..1653d3f3f5538 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -1063,7 +1063,7 @@ mod tests { use crate::generate_noqa_edits; use crate::noqa::{add_noqa_inner, Directive, NoqaMapping, ParsedFileExemption}; - use crate::rules::pycodestyle::rules::AmbiguousVariableName; + use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon}; use crate::rules::pyflakes::rules::UnusedVariable; use crate::rules::pyupgrade::rules::PrintfStringFormatting; @@ -1380,4 +1380,36 @@ print( ))] ); } + + #[test] + fn syntax_error() { + let path = Path::new("/tmp/foo.txt"); + let source = "\ +foo; +bar = +"; + let diagnostics = [Diagnostic::new( + UselessSemicolon, + TextRange::new(4.into(), 5.into()), + )]; + let noqa_line_for = NoqaMapping::default(); + let comment_ranges = CommentRanges::default(); + let edits = generate_noqa_edits( + path, + &diagnostics, + &Locator::new(source), + &comment_ranges, + &[], + &noqa_line_for, + LineEnding::Lf, + ); + assert_eq!( + edits, + vec![Some(Edit::replacement( + " # noqa: E703\n".to_string(), + 4.into(), + 5.into() + ))] + ); + } } diff --git a/crates/ruff_linter/src/rules/flake8_commas/snapshots/ruff_linter__rules__flake8_commas__tests__COM81.py.snap b/crates/ruff_linter/src/rules/flake8_commas/snapshots/ruff_linter__rules__flake8_commas__tests__COM81.py.snap index 3a23eb602a2d6..51b3ede78fff1 100644 --- a/crates/ruff_linter/src/rules/flake8_commas/snapshots/ruff_linter__rules__flake8_commas__tests__COM81.py.snap +++ b/crates/ruff_linter/src/rules/flake8_commas/snapshots/ruff_linter__rules__flake8_commas__tests__COM81.py.snap @@ -601,7 +601,7 @@ COM81.py:511:10: COM819 [*] Trailing comma prohibited 511 | image[:,:,] | ^ COM819 512 | -513 | lambda x, : +513 | lambda x, : x | = help: Remove trailing comma @@ -612,14 +612,14 @@ COM81.py:511:10: COM819 [*] Trailing comma prohibited 511 |-image[:,:,] 511 |+image[:,:] 512 512 | -513 513 | lambda x, : +513 513 | lambda x, : x 514 514 | COM81.py:513:9: COM819 [*] Trailing comma prohibited | 511 | image[:,:,] 512 | -513 | lambda x, : +513 | lambda x, : x | ^ COM819 514 | 515 | # ==> unpack.py <== @@ -630,8 +630,8 @@ COM81.py:513:9: COM819 [*] Trailing comma prohibited 510 510 | 511 511 | image[:,:,] 512 512 | -513 |-lambda x, : - 513 |+lambda x : +513 |-lambda x, : x + 513 |+lambda x : x 514 514 | 515 515 | # ==> unpack.py <== 516 516 | def function( @@ -798,6 +798,14 @@ COM81.py:565:13: COM812 [*] Trailing comma missing 567 567 | 568 568 | ( +COM81.py:569:5: SyntaxError: Starred expression cannot be used here + | +568 | ( +569 | *args + | ^ +570 | ) + | + COM81.py:573:10: COM812 [*] Trailing comma missing | 572 | { diff --git a/crates/ruff_linter/src/rules/pycodestyle/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/mod.rs index c220c24a61681..556b640b66ee9 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/mod.rs @@ -50,7 +50,6 @@ mod tests { #[test_case(Rule::NoneComparison, Path::new("E711.py"))] #[test_case(Rule::NotInTest, Path::new("E713.py"))] #[test_case(Rule::NotIsTest, Path::new("E714.py"))] - #[test_case(Rule::SyntaxError, Path::new("E999.py"))] #[test_case(Rule::TabIndentation, Path::new("W19.py"))] #[test_case(Rule::TrailingWhitespace, Path::new("W29.py"))] #[test_case(Rule::TrailingWhitespace, Path::new("W291.py"))] diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/errors.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/errors.rs index 5ca8e790b081a..de26f88488904 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/errors.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/errors.rs @@ -1,11 +1,5 @@ -use ruff_python_parser::ParseError; -use ruff_text_size::{TextLen, TextRange, TextSize}; - -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; -use ruff_source_file::Locator; - -use crate::logging::DisplayParseErrorType; /// ## What it does /// This is not a regular diagnostic; instead, it's raised when a file cannot be read @@ -43,6 +37,10 @@ impl Violation for IOError { } } +/// ## Deprecated +/// This rule has been deprecated and will be removed in a future release. Syntax errors will +/// always be shown regardless of whether this rule is selected or not. +/// /// ## What it does /// Checks for code that contains syntax errors. /// @@ -74,27 +72,3 @@ impl Violation for SyntaxError { format!("SyntaxError: {message}") } } - -/// E901 -pub(crate) fn syntax_error( - diagnostics: &mut Vec, - parse_error: &ParseError, - locator: &Locator, -) { - let rest = locator.after(parse_error.location.start()); - - // Try to create a non-empty range so that the diagnostic can print a caret at the - // right position. This requires that we retrieve the next character, if any, and take its length - // to maintain char-boundaries. - let len = rest - .chars() - .next() - .map_or(TextSize::new(0), TextLen::text_len); - - diagnostics.push(Diagnostic::new( - SyntaxError { - message: format!("{}", DisplayParseErrorType::new(&parse_error.error)), - }, - TextRange::at(parse_error.location.start(), len), - )); -} diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/mod.rs index 178dd13b5be43..8d5914d8a4ec6 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/mod.rs @@ -5,8 +5,8 @@ pub(crate) use bare_except::*; pub(crate) use blank_lines::*; pub(crate) use compound_statements::*; pub(crate) use doc_line_too_long::*; +pub use errors::IOError; pub(crate) use errors::*; -pub use errors::{IOError, SyntaxError}; pub(crate) use invalid_escape_sequence::*; pub(crate) use lambda_assignment::*; pub(crate) use line_too_long::*; diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E111_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E111_E11.py.snap index 5ff96325eeb6b..97ce990df6d73 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E111_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E111_E11.py.snap @@ -21,4 +21,42 @@ E11.py:6:1: E111 Indentation is not a multiple of 4 8 | if False: | +E11.py:9:1: SyntaxError: Expected an indented block after `if` statement + | + 7 | #: E112 + 8 | if False: + 9 | print() + | ^ +10 | #: E113 +11 | print() + | +E11.py:12:1: SyntaxError: Unexpected indentation + | +10 | #: E113 +11 | print() +12 | print() + | ^ +13 | #: E114 E116 +14 | mimetype = 'application/x-directory' + | + +E11.py:14:1: SyntaxError: Expected a statement + | +12 | print() +13 | #: E114 E116 +14 | mimetype = 'application/x-directory' + | ^ +15 | # 'httpd/unix-directory' +16 | create_date = False + | + +E11.py:45:1: SyntaxError: Expected an indented block after `if` statement + | +43 | #: E112 +44 | if False: # +45 | print() + | ^ +46 | #: +47 | if False: + | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap index ea4f5346a8c89..70373df6f6ddc 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap @@ -11,6 +11,36 @@ E11.py:9:1: E112 Expected an indented block 11 | print() | +E11.py:9:1: SyntaxError: Expected an indented block after `if` statement + | + 7 | #: E112 + 8 | if False: + 9 | print() + | ^ +10 | #: E113 +11 | print() + | + +E11.py:12:1: SyntaxError: Unexpected indentation + | +10 | #: E113 +11 | print() +12 | print() + | ^ +13 | #: E114 E116 +14 | mimetype = 'application/x-directory' + | + +E11.py:14:1: SyntaxError: Expected a statement + | +12 | print() +13 | #: E114 E116 +14 | mimetype = 'application/x-directory' + | ^ +15 | # 'httpd/unix-directory' +16 | create_date = False + | + E11.py:45:1: E112 Expected an indented block | 43 | #: E112 @@ -21,4 +51,12 @@ E11.py:45:1: E112 Expected an indented block 47 | if False: | - +E11.py:45:1: SyntaxError: Expected an indented block after `if` statement + | +43 | #: E112 +44 | if False: # +45 | print() + | ^ +46 | #: +47 | if False: + | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E113_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E113_E11.py.snap index fe41aac33f533..295b8830d3c2f 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E113_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E113_E11.py.snap @@ -1,6 +1,16 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- +E11.py:9:1: SyntaxError: Expected an indented block after `if` statement + | + 7 | #: E112 + 8 | if False: + 9 | print() + | ^ +10 | #: E113 +11 | print() + | + E11.py:12:1: E113 Unexpected indentation | 10 | #: E113 @@ -11,4 +21,32 @@ E11.py:12:1: E113 Unexpected indentation 14 | mimetype = 'application/x-directory' | +E11.py:12:1: SyntaxError: Unexpected indentation + | +10 | #: E113 +11 | print() +12 | print() + | ^ +13 | #: E114 E116 +14 | mimetype = 'application/x-directory' + | + +E11.py:14:1: SyntaxError: Expected a statement + | +12 | print() +13 | #: E114 E116 +14 | mimetype = 'application/x-directory' + | ^ +15 | # 'httpd/unix-directory' +16 | create_date = False + | +E11.py:45:1: SyntaxError: Expected an indented block after `if` statement + | +43 | #: E112 +44 | if False: # +45 | print() + | ^ +46 | #: +47 | if False: + | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E114_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E114_E11.py.snap index 3fcbbc03f884b..46722a22f4c40 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E114_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E114_E11.py.snap @@ -1,6 +1,36 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- +E11.py:9:1: SyntaxError: Expected an indented block after `if` statement + | + 7 | #: E112 + 8 | if False: + 9 | print() + | ^ +10 | #: E113 +11 | print() + | + +E11.py:12:1: SyntaxError: Unexpected indentation + | +10 | #: E113 +11 | print() +12 | print() + | ^ +13 | #: E114 E116 +14 | mimetype = 'application/x-directory' + | + +E11.py:14:1: SyntaxError: Expected a statement + | +12 | print() +13 | #: E114 E116 +14 | mimetype = 'application/x-directory' + | ^ +15 | # 'httpd/unix-directory' +16 | create_date = False + | + E11.py:15:1: E114 Indentation is not a multiple of 4 (comment) | 13 | #: E114 E116 @@ -11,4 +41,12 @@ E11.py:15:1: E114 Indentation is not a multiple of 4 (comment) 17 | #: E116 E116 E116 | - +E11.py:45:1: SyntaxError: Expected an indented block after `if` statement + | +43 | #: E112 +44 | if False: # +45 | print() + | ^ +46 | #: +47 | if False: + | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap index 34e21b201ed0c..e51700d1090cd 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap @@ -1,6 +1,36 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- +E11.py:9:1: SyntaxError: Expected an indented block after `if` statement + | + 7 | #: E112 + 8 | if False: + 9 | print() + | ^ +10 | #: E113 +11 | print() + | + +E11.py:12:1: SyntaxError: Unexpected indentation + | +10 | #: E113 +11 | print() +12 | print() + | ^ +13 | #: E114 E116 +14 | mimetype = 'application/x-directory' + | + +E11.py:14:1: SyntaxError: Expected a statement + | +12 | print() +13 | #: E114 E116 +14 | mimetype = 'application/x-directory' + | ^ +15 | # 'httpd/unix-directory' +16 | create_date = False + | + E11.py:30:1: E115 Expected an indented block (comment) | 28 | def start(self): @@ -61,4 +91,12 @@ E11.py:35:1: E115 Expected an indented block (comment) 37 | #: E117 | - +E11.py:45:1: SyntaxError: Expected an indented block after `if` statement + | +43 | #: E112 +44 | if False: # +45 | print() + | ^ +46 | #: +47 | if False: + | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E116_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E116_E11.py.snap index 8f1a719473b12..5a378f63e5f4c 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E116_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E116_E11.py.snap @@ -1,6 +1,36 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- +E11.py:9:1: SyntaxError: Expected an indented block after `if` statement + | + 7 | #: E112 + 8 | if False: + 9 | print() + | ^ +10 | #: E113 +11 | print() + | + +E11.py:12:1: SyntaxError: Unexpected indentation + | +10 | #: E113 +11 | print() +12 | print() + | ^ +13 | #: E114 E116 +14 | mimetype = 'application/x-directory' + | + +E11.py:14:1: SyntaxError: Expected a statement + | +12 | print() +13 | #: E114 E116 +14 | mimetype = 'application/x-directory' + | ^ +15 | # 'httpd/unix-directory' +16 | create_date = False + | + E11.py:15:1: E116 Unexpected indentation (comment) | 13 | #: E114 E116 @@ -41,4 +71,12 @@ E11.py:26:1: E116 Unexpected indentation (comment) 28 | def start(self): | - +E11.py:45:1: SyntaxError: Expected an indented block after `if` statement + | +43 | #: E112 +44 | if False: # +45 | print() + | ^ +46 | #: +47 | if False: + | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E117_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E117_E11.py.snap index fc9e939039071..22ca83d0b0a07 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E117_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E117_E11.py.snap @@ -11,6 +11,36 @@ E11.py:6:1: E117 Over-indented 8 | if False: | +E11.py:9:1: SyntaxError: Expected an indented block after `if` statement + | + 7 | #: E112 + 8 | if False: + 9 | print() + | ^ +10 | #: E113 +11 | print() + | + +E11.py:12:1: SyntaxError: Unexpected indentation + | +10 | #: E113 +11 | print() +12 | print() + | ^ +13 | #: E114 E116 +14 | mimetype = 'application/x-directory' + | + +E11.py:14:1: SyntaxError: Expected a statement + | +12 | print() +13 | #: E114 E116 +14 | mimetype = 'application/x-directory' + | ^ +15 | # 'httpd/unix-directory' +16 | create_date = False + | + E11.py:39:1: E117 Over-indented | 37 | #: E117 @@ -31,4 +61,12 @@ E11.py:42:1: E117 Over-indented 44 | if False: # | - +E11.py:45:1: SyntaxError: Expected an indented block after `if` statement + | +43 | #: E112 +44 | if False: # +45 | print() + | ^ +46 | #: +47 | if False: + | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E999_E999.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E999_E999.py.snap deleted file mode 100644 index ac712ec6c2fef..0000000000000 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E999_E999.py.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/pycodestyle/mod.rs ---- -E999.py:2:9: E999 SyntaxError: Expected an indented block after function definition - | -2 | def x(): - | ^ E999 -3 | - | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W191_W19.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W191_W19.py.snap index 5e8fe375665fb..a47fc90aff8bb 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W191_W19.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W191_W19.py.snap @@ -8,6 +8,22 @@ W19.py:1:1: W191 Indentation contains tabs 2 | multiline string with tab in it''' | +W19.py:1:1: SyntaxError: Unexpected indentation + | +1 | '''File starts with a tab + | ^^^^ +2 | multiline string with tab in it''' + | + +W19.py:5:1: SyntaxError: Expected a statement + | +4 | #: W191 +5 | if False: + | ^ +6 | print # indented with 1 tab +7 | #: + | + W19.py:6:1: W191 Indentation contains tabs | 4 | #: W191 diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__white_space_syntax_error_compatibility.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__white_space_syntax_error_compatibility.snap index 6dcc4546f11f9..73c770d6cb3b3 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__white_space_syntax_error_compatibility.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__white_space_syntax_error_compatibility.snap @@ -1,4 +1,8 @@ --- source: crates/ruff_linter/src/rules/pycodestyle/mod.rs --- - +E2_syntax_error.py:1:10: SyntaxError: Expected an expression + | +1 | a = (1 or) + | ^ + | diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index f2b1e6d114e1b..764d2afa21afc 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -2414,7 +2414,7 @@ mod tests { fn used_in_lambda() { flakes( r"import fu; - lambda: fu +lambda: fu ", &[], ); @@ -2433,7 +2433,7 @@ mod tests { fn used_in_slice_obj() { flakes( r#"import fu; - "meow"[::fu] +"meow"[::fu] "#, &[], ); @@ -3040,16 +3040,6 @@ mod tests { &[], ); - flakes( - r#" - from interior import decorate - @decorate('value", &[]); - def f(): - return "hello" - "#, - &[], - ); - flakes( r#" @decorate diff --git a/crates/ruff_linter/src/test.rs b/crates/ruff_linter/src/test.rs index 31fae2bdaa887..6def9fc83bde0 100644 --- a/crates/ruff_linter/src/test.rs +++ b/crates/ruff_linter/src/test.rs @@ -16,6 +16,7 @@ use ruff_notebook::NotebookError; use ruff_python_ast::PySourceType; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; +use ruff_python_parser::ParseError; use ruff_python_trivia::textwrap::dedent; use ruff_source_file::{Locator, SourceFileBuilder}; use ruff_text_size::Ranged; @@ -26,7 +27,6 @@ use crate::linter::{check_path, LinterResult}; use crate::message::{Emitter, EmitterContext, Message, TextEmitter}; use crate::packaging::detect_package_root; use crate::registry::AsRule; -use crate::rules::pycodestyle::rules::syntax_error; use crate::settings::types::UnsafeFixes; use crate::settings::{flags, LinterSettings}; use crate::source_kind::SourceKind; @@ -188,7 +188,7 @@ pub(crate) fn test_contents<'a>( let LinterResult { data: fixed_diagnostics, - error: fixed_error, + .. } = check_path( path, None, @@ -203,25 +203,21 @@ pub(crate) fn test_contents<'a>( &parsed, ); - if let Some(fixed_error) = fixed_error { - if !source_has_errors { - // Previous fix introduced a syntax error, abort - let fixes = print_diagnostics(diagnostics, path, source_kind); + if !parsed.is_valid() && !source_has_errors { + // Previous fix introduced a syntax error, abort + let fixes = print_diagnostics(diagnostics, path, source_kind); + let syntax_errors = + print_syntax_errors(parsed.errors(), path, &locator, &transformed); - let mut syntax_diagnostics = Vec::new(); - syntax_error(&mut syntax_diagnostics, &fixed_error, &locator); - let syntax_errors = print_diagnostics(syntax_diagnostics, path, &transformed); - - panic!( - r#"Fixed source has a syntax error where the source document does not. This is a bug in one of the generated fixes: + panic!( + r#"Fixed source has a syntax error where the source document does not. This is a bug in one of the generated fixes: {syntax_errors} Last generated fixes: {fixes} Source with applied fixes: {}"#, - transformed.source_code() - ); - } + transformed.source_code() + ); } diagnostics = fixed_diagnostics; @@ -260,11 +256,40 @@ Source with applied fixes: Message::from_diagnostic(diagnostic, source_code.clone(), noqa) }) + .chain( + parsed + .errors() + .iter() + .map(|parse_error| { + Message::from_parse_error(parse_error, &locator, source_code.clone()) + }) + ) .sorted() .collect(); (messages, transformed) } +fn print_syntax_errors( + errors: &[ParseError], + path: &Path, + locator: &Locator, + source: &SourceKind, +) -> String { + let filename = path.file_name().unwrap().to_string_lossy(); + let source_file = SourceFileBuilder::new(filename.as_ref(), source.source_code()).finish(); + + let messages: Vec<_> = errors + .iter() + .map(|parse_error| Message::from_parse_error(parse_error, locator, source_file.clone())) + .collect(); + + if let Some(notebook) = source.as_ipy_notebook() { + print_jupyter_messages(&messages, path, notebook) + } else { + print_messages(&messages) + } +} + fn print_diagnostics(diagnostics: Vec, path: &Path, source: &SourceKind) -> String { let filename = path.file_name().unwrap().to_string_lossy(); let source_file = SourceFileBuilder::new(filename.as_ref(), source.source_code()).finish(); diff --git a/crates/ruff_server/src/lint.rs b/crates/ruff_server/src/lint.rs index 294ded142e4ed..297d3206d2af6 100644 --- a/crates/ruff_server/src/lint.rs +++ b/crates/ruff_server/src/lint.rs @@ -1,5 +1,6 @@ //! Access to the Ruff linting API for the LSP +use ruff_python_parser::ParseError; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; @@ -153,7 +154,10 @@ pub(crate) fn check(query: &DocumentQuery, encoding: PositionEncoding) -> Diagno .zip(noqa_edits) .map(|(diagnostic, noqa_edit)| { to_lsp_diagnostic(diagnostic, &noqa_edit, &source_kind, &index, encoding) - }); + }) + .chain(parsed.errors().iter().map(|parse_error| { + parse_error_to_lsp_diagnostic(parse_error, &source_kind, &index, encoding) + })); if let Some(notebook) = query.as_notebook() { for (index, diagnostic) in lsp_diagnostics { @@ -287,6 +291,45 @@ fn to_lsp_diagnostic( ) } +fn parse_error_to_lsp_diagnostic( + parse_error: &ParseError, + 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 } = parse_error.location.to_notebook_range( + source_kind.source_code(), + index, + notebook_index, + encoding, + ); + } else { + cell = usize::default(); + range = parse_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 diagnostic_edit_range( range: TextRange, source_kind: &SourceKind, diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index e825a714a6d7e..85f7b91056649 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -968,9 +968,13 @@ impl LintConfiguration { if preview.mode.is_disabled() { for selection in deprecated_selectors.iter().sorted() { let (prefix, code) = selection.prefix_and_code(); - warn_user_once_by_message!( - "Rule `{prefix}{code}` is deprecated and will be removed in a future release.", - ); + let rule = format!("{prefix}{code}"); + let mut message = + format!("Rule `{rule}` is deprecated and will be removed in a future release."); + if matches!(rule.as_str(), "E999") { + message.push_str(" Syntax errors will always be shown regardless of whether this rule is selected or not."); + } + warn_user_once_by_message!("{message}"); } } else { let deprecated_selectors = deprecated_selectors.iter().sorted().collect::>(); diff --git a/fuzz/fuzz_targets/ruff_formatter_validity.rs b/fuzz/fuzz_targets/ruff_formatter_validity.rs index 3f8f7d886d4c1..2495f15a58dad 100644 --- a/fuzz/fuzz_targets/ruff_formatter_validity.rs +++ b/fuzz/fuzz_targets/ruff_formatter_validity.rs @@ -43,8 +43,8 @@ fn do_fuzz(case: &[u8]) -> Corpus { let mut warnings = HashMap::new(); - for msg in linter_results.data { - let count: &mut usize = warnings.entry(msg.kind.name).or_default(); + for msg in &linter_results.data { + let count: &mut usize = warnings.entry(msg.name()).or_default(); *count += 1; } @@ -67,8 +67,8 @@ fn do_fuzz(case: &[u8]) -> Corpus { "formatter introduced a parse error" ); - for msg in linter_results.data { - if let Some(count) = warnings.get_mut(&msg.kind.name) { + for msg in &linter_results.data { + if let Some(count) = warnings.get_mut(msg.name()) { if let Some(decremented) = count.checked_sub(1) { *count = decremented; } else {