mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
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. <img width="772" alt="Screenshot 2024-06-26 at 14 57 08" src="aaac827e
-4777-4ac8-8c68-eaf9f2c36774"> Even with `noqa` comment, the syntax error is displayed: <img width="763" alt="Screenshot 2024-06-26 at 14 59 51" src="ba1afb68
-7eaf-4b44-91af-6d93246475e2"> Rule documentation page: <img width="1371" alt="Screenshot 2024-06-26 at 16 48 07" src="524f01df
-d91f-4ac0-86cc-40e76b318b24"> ## 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
This commit is contained in:
parent
c98d8a040f
commit
e7b49694a7
52 changed files with 1235 additions and 380 deletions
|
@ -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,13 +333,15 @@ impl FileCache {
|
|||
let file = SourceFileBuilder::new(path.to_string_lossy(), &*lint.source).finish();
|
||||
lint.messages
|
||||
.iter()
|
||||
.map(|msg| Message {
|
||||
.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()
|
||||
};
|
||||
let notebook_indexes = if let Some(notebook_index) = lint.notebook_index.as_ref() {
|
||||
|
@ -412,18 +414,19 @@ impl LintCacheData {
|
|||
notebook_index: Option<NotebookIndex>,
|
||||
) -> 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);
|
||||
|
|
|
@ -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,37 +55,22 @@ impl Diagnostics {
|
|||
path: Option<&Path>,
|
||||
settings: &LinterSettings,
|
||||
) -> Self {
|
||||
let diagnostic = match err {
|
||||
match err {
|
||||
// IO errors.
|
||||
SourceError::Io(_)
|
||||
| SourceError::Notebook(NotebookError::Io(_) | NotebookError::Json(_)) => {
|
||||
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(),
|
||||
)
|
||||
}
|
||||
// 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,
|
||||
source_file,
|
||||
TextSize::default(),
|
||||
)],
|
||||
FxHashMap::default(),
|
||||
|
@ -108,6 +93,25 @@ impl Diagnostics {
|
|||
Self::default()
|
||||
}
|
||||
}
|
||||
// Syntax errors.
|
||||
SourceError::Notebook(
|
||||
NotebookError::InvalidJson(_)
|
||||
| NotebookError::InvalidSchema(_)
|
||||
| NotebookError::InvalidFormat(_),
|
||||
) => {
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Diagnostics {
|
||||
|
|
|
@ -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<SerializeRuleAsCode>,
|
||||
name: SerializeMessageKindAsTitle,
|
||||
count: usize,
|
||||
fixable: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct SerializeRuleAsCode(Rule);
|
||||
|
||||
impl Serialize for SerializeRuleAsCode {
|
||||
|
@ -66,26 +67,26 @@ impl From<Rule> for SerializeRuleAsCode {
|
|||
}
|
||||
}
|
||||
|
||||
struct SerializeRuleAsTitle(Rule);
|
||||
struct SerializeMessageKindAsTitle(MessageKind);
|
||||
|
||||
impl Serialize for SerializeRuleAsTitle {
|
||||
impl Serialize for SerializeMessageKindAsTitle {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
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<Rule> for SerializeRuleAsTitle {
|
||||
fn from(rule: Rule) -> Self {
|
||||
Self(rule)
|
||||
impl From<MessageKind> for SerializeMessageKindAsTitle {
|
||||
fn from(kind: MessageKind) -> Self {
|
||||
Self(kind)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -341,24 +342,23 @@ impl Printer {
|
|||
let statistics: Vec<ExpandedStatistics> = 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{:<code_width$}\t{}{}",
|
||||
statistic.count.to_string().bold(),
|
||||
statistic.code.to_string().red().bold(),
|
||||
statistic
|
||||
.code
|
||||
.map_or_else(String::new, |rule| rule.to_string())
|
||||
.red()
|
||||
.bold(),
|
||||
if any_fixable {
|
||||
if statistic.fixable {
|
||||
&fixable
|
||||
|
@ -545,7 +554,7 @@ impl FixableStatistics {
|
|||
let mut unapplicable_unsafe = 0;
|
||||
|
||||
for message in &diagnostics.messages {
|
||||
if let Some(fix) = &message.fix {
|
||||
if let Some(fix) = message.fix() {
|
||||
if fix.applies(unsafe_fixes.required_applicability()) {
|
||||
applicable += 1;
|
||||
} else {
|
||||
|
|
|
@ -798,10 +798,10 @@ fn stdin_parse_error() {
|
|||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:16: E999 SyntaxError: Expected one or more symbol names after import
|
||||
-:1:16: SyntaxError: Expected one or more symbol names after import
|
||||
|
|
||||
1 | from foo import
|
||||
| ^ E999
|
||||
| ^
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
|
@ -819,18 +819,18 @@ fn stdin_multiple_parse_error() {
|
|||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:16: E999 SyntaxError: Expected one or more symbol names after import
|
||||
-:1:16: SyntaxError: Expected one or more symbol names after import
|
||||
|
|
||||
1 | from foo import
|
||||
| ^ E999
|
||||
| ^
|
||||
2 | bar =
|
||||
|
|
||||
|
||||
-:2:6: E999 SyntaxError: Expected an expression
|
||||
-:2:6: SyntaxError: Expected an expression
|
||||
|
|
||||
1 | from foo import
|
||||
2 | bar =
|
||||
| ^ E999
|
||||
| ^
|
||||
|
|
||||
|
||||
Found 2 errors.
|
||||
|
@ -840,6 +840,50 @@ fn stdin_multiple_parse_error() {
|
|||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_error_not_included() {
|
||||
// Select any rule except for `E999`, syntax error should still be shown.
|
||||
let mut cmd = RuffCheck::default().args(["--select=I"]).build();
|
||||
assert_cmd_snapshot!(cmd
|
||||
.pass_stdin("foo =\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:6: SyntaxError: Expected an expression
|
||||
|
|
||||
1 | foo =
|
||||
| ^
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
error: Failed to parse at 1:6: Expected an expression
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deprecated_parse_error_selection() {
|
||||
let mut cmd = RuffCheck::default().args(["--select=E999"]).build();
|
||||
assert_cmd_snapshot!(cmd
|
||||
.pass_stdin("foo =\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:6: SyntaxError: Expected an expression
|
||||
|
|
||||
1 | foo =
|
||||
| ^
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
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.
|
||||
error: Failed to parse at 1:6: Expected an expression
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_output_preview() {
|
||||
let mut cmd = RuffCheck::default().args(["--preview"]).build();
|
||||
|
|
|
@ -510,7 +510,7 @@ image[:,]
|
|||
|
||||
image[:,:,]
|
||||
|
||||
lambda x, :
|
||||
lambda x, : x
|
||||
|
||||
# ==> unpack.py <==
|
||||
def function(
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
|
||||
def x():
|
||||
|
||||
|
|
@ -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),
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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<Vec<Diagnostic>> {
|
||||
// Aggregate all diagnostics.
|
||||
let mut diagnostics = vec![];
|
||||
let mut error = None;
|
||||
|
||||
let tokens = parsed.tokens();
|
||||
let comment_ranges = indexer.comment_ranges();
|
||||
|
@ -142,7 +140,8 @@ pub fn check_path(
|
|||
));
|
||||
}
|
||||
|
||||
// Run the AST-based rules.
|
||||
// Run the AST-based rules only if there are no syntax errors.
|
||||
if parsed.is_valid() {
|
||||
let use_ast = settings
|
||||
.rules
|
||||
.iter_enabled()
|
||||
|
@ -153,8 +152,6 @@ pub fn check_path(
|
|||
.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 {
|
||||
|
@ -192,19 +189,6 @@ pub fn check_path(
|
|||
doc_lines.extend(doc_lines_from_ast(parsed.suite(), locator));
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate and reorder any doc lines.
|
||||
|
@ -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<Diagnostic>,
|
||||
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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,8 +27,8 @@ pub(super) struct Diff<'a> {
|
|||
|
||||
impl<'a> Diff<'a> {
|
||||
pub(crate) fn from_message(message: &'a Message) -> Option<Diff> {
|
||||
message.fix.as_ref().map(|fix| Diff {
|
||||
source_code: &message.file,
|
||||
message.fix().map(|fix| Diff {
|
||||
source_code: message.source_file(),
|
||||
fix,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": ""#;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Fix>,
|
||||
|
@ -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<TextSize> {
|
||||
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<Rule> {
|
||||
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<Message> {
|
||||
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<Message> {
|
||||
let fib = r#"import os
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Rule>,
|
||||
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::<serde_json::Value>(&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::<serde_json::Value>(&content).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_results() {
|
||||
let content = get_output();
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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": "<redacted>",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 1,
|
||||
"end": 2
|
||||
},
|
||||
"path": "syntax_errors.py"
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
{
|
||||
"description": "SyntaxError: Expected ')', found newline",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 3,
|
||||
"end": 4
|
||||
},
|
||||
"path": "syntax_errors.py"
|
||||
},
|
||||
"severity": "major"
|
||||
}
|
||||
]
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
]
|
|
@ -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}
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/message/junit.rs
|
||||
expression: content
|
||||
---
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites name="ruff" tests="2" failures="2" errors="0">
|
||||
<testsuite name="syntax_errors.py" tests="2" disabled="0" errors="0" failures="2" package="org.ruff">
|
||||
<testcase name="org.ruff" classname="syntax_errors" line="1" column="15">
|
||||
<failure message="SyntaxError: Expected one or more symbol names after import">line 1, col 15, SyntaxError: Expected one or more symbol names after import</failure>
|
||||
</testcase>
|
||||
<testcase name="org.ruff" classname="syntax_errors" line="3" column="12">
|
||||
<failure message="SyntaxError: Expected ')', found newline">line 3, col 12, SyntaxError: Expected ')', found newline</failure>
|
||||
</testcase>
|
||||
</testsuite>
|
||||
</testsuites>
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(rule) = self.message.rule() {
|
||||
write!(
|
||||
f,
|
||||
"{code} {body}",
|
||||
code = kind.rule().noqa_code().to_string().red().bold(),
|
||||
body = kind.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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
))]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 | {
|
||||
|
|
|
@ -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"))]
|
||||
|
|
|
@ -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<Diagnostic>,
|
||||
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),
|
||||
));
|
||||
}
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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:
|
||||
|
|
||||
|
|
|
@ -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:
|
||||
|
|
||||
|
|
|
@ -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:
|
||||
|
|
||||
|
|
|
@ -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:
|
||||
|
|
||||
|
|
|
@ -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:
|
||||
|
|
||||
|
|
|
@ -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:
|
||||
|
|
||||
|
|
|
@ -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:
|
||||
|
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
| ^
|
||||
|
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,14 +203,11 @@ pub(crate) fn test_contents<'a>(
|
|||
&parsed,
|
||||
);
|
||||
|
||||
if let Some(fixed_error) = fixed_error {
|
||||
if !source_has_errors {
|
||||
if !parsed.is_valid() && !source_has_errors {
|
||||
// Previous fix introduced a syntax error, abort
|
||||
let fixes = print_diagnostics(diagnostics, path, source_kind);
|
||||
|
||||
let mut syntax_diagnostics = Vec::new();
|
||||
syntax_error(&mut syntax_diagnostics, &fixed_error, &locator);
|
||||
let syntax_errors = print_diagnostics(syntax_diagnostics, path, &transformed);
|
||||
let syntax_errors =
|
||||
print_syntax_errors(parsed.errors(), path, &locator, &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:
|
||||
|
@ -222,7 +219,6 @@ Source with applied fixes:
|
|||
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<Diagnostic>, 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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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::<Vec<_>>();
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue