mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-03 07:04:53 +00:00

## 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
173 lines
5.2 KiB
Rust
173 lines
5.2 KiB
Rust
use std::collections::hash_map::DefaultHasher;
|
|
use std::collections::HashSet;
|
|
use std::hash::{Hash, Hasher};
|
|
use std::io::Write;
|
|
|
|
use serde::ser::SerializeSeq;
|
|
use serde::{Serialize, Serializer};
|
|
use serde_json::json;
|
|
|
|
use crate::fs::{relativize_path, relativize_path_to};
|
|
use crate::message::{Emitter, EmitterContext, Message};
|
|
|
|
/// Generate JSON with violations in GitLab CI format
|
|
// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
|
|
pub struct GitlabEmitter {
|
|
project_dir: Option<String>,
|
|
}
|
|
|
|
impl Default for GitlabEmitter {
|
|
fn default() -> Self {
|
|
Self {
|
|
project_dir: std::env::var("CI_PROJECT_DIR").ok(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Emitter for GitlabEmitter {
|
|
fn emit(
|
|
&mut self,
|
|
writer: &mut dyn Write,
|
|
messages: &[Message],
|
|
context: &EmitterContext,
|
|
) -> anyhow::Result<()> {
|
|
serde_json::to_writer_pretty(
|
|
writer,
|
|
&SerializedMessages {
|
|
messages,
|
|
context,
|
|
project_dir: self.project_dir.as_deref(),
|
|
},
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
struct SerializedMessages<'a> {
|
|
messages: &'a [Message],
|
|
context: &'a EmitterContext<'a>,
|
|
project_dir: Option<&'a str>,
|
|
}
|
|
|
|
impl Serialize for SerializedMessages<'_> {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
let mut s = serializer.serialize_seq(Some(self.messages.len()))?;
|
|
let mut fingerprints = HashSet::<u64>::with_capacity(self.messages.len());
|
|
|
|
for message in self.messages {
|
|
let start_location = message.compute_start_location();
|
|
let end_location = message.compute_end_location();
|
|
|
|
let lines = if self.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
|
|
json!({
|
|
"begin": 1,
|
|
"end": 1
|
|
})
|
|
} else {
|
|
json!({
|
|
"begin": start_location.row,
|
|
"end": end_location.row
|
|
})
|
|
};
|
|
|
|
let path = self.project_dir.as_ref().map_or_else(
|
|
|| relativize_path(message.filename()),
|
|
|project_dir| relativize_path_to(message.filename(), project_dir),
|
|
);
|
|
|
|
let mut message_fingerprint = fingerprint(message, &path, 0);
|
|
|
|
// Make sure that we do not get a fingerprint that is already in use
|
|
// by adding in the previously generated one.
|
|
while fingerprints.contains(&message_fingerprint) {
|
|
message_fingerprint = fingerprint(message, &path, message_fingerprint);
|
|
}
|
|
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": description,
|
|
"severity": "major",
|
|
"fingerprint": format!("{:x}", message_fingerprint),
|
|
"location": {
|
|
"path": path,
|
|
"lines": lines
|
|
}
|
|
});
|
|
|
|
s.serialize_element(&value)?;
|
|
}
|
|
|
|
s.end()
|
|
}
|
|
}
|
|
|
|
/// Generate a unique fingerprint to identify a violation.
|
|
fn fingerprint(message: &Message, project_path: &str, salt: u64) -> u64 {
|
|
let mut hasher = DefaultHasher::new();
|
|
|
|
salt.hash(&mut hasher);
|
|
message.name().hash(&mut hasher);
|
|
project_path.hash(&mut hasher);
|
|
|
|
hasher.finish()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use insta::assert_snapshot;
|
|
|
|
use crate::message::tests::{
|
|
capture_emitter_output, create_messages, create_syntax_error_messages,
|
|
};
|
|
use crate::message::GitlabEmitter;
|
|
|
|
#[test]
|
|
fn output() {
|
|
let mut emitter = GitlabEmitter::default();
|
|
let content = capture_emitter_output(&mut emitter, &create_messages());
|
|
|
|
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": ""#;
|
|
|
|
let mut output = String::with_capacity(content.len());
|
|
let mut last = 0;
|
|
|
|
for (start, _) in content.match_indices(FINGERPRINT_HAY_KEY) {
|
|
let fingerprint_hash_start = start + FINGERPRINT_HAY_KEY.len();
|
|
output.push_str(&content[last..fingerprint_hash_start]);
|
|
output.push_str("<redacted>");
|
|
last = fingerprint_hash_start
|
|
+ content[fingerprint_hash_start..]
|
|
.find('"')
|
|
.expect("Expected terminating quote");
|
|
}
|
|
|
|
output.push_str(&content[last..]);
|
|
|
|
output
|
|
}
|
|
}
|