mirror of https://github.com/astral-sh/ruff
[ty] Prototype of --add-ignore CLI option
This commit is contained in:
parent
69ace00210
commit
d464344f74
|
|
@ -4332,8 +4332,10 @@ dependencies = [
|
|||
"rayon",
|
||||
"regex",
|
||||
"ruff_db",
|
||||
"ruff_diagnostics",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_trivia",
|
||||
"ruff_text_size",
|
||||
"salsa",
|
||||
"tempfile",
|
||||
"toml",
|
||||
|
|
@ -4428,6 +4430,7 @@ dependencies = [
|
|||
"regex-automata",
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
"ruff_diagnostics",
|
||||
"ruff_macros",
|
||||
"ruff_memory_usage",
|
||||
"ruff_options_metadata",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ruff_diagnostics::SourceMap;
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_source_file::LineIndex;
|
||||
|
|
@ -90,6 +91,34 @@ impl SourceText {
|
|||
pub fn read_error(&self) -> Option<&SourceTextError> {
|
||||
self.inner.read_error.as_ref()
|
||||
}
|
||||
|
||||
pub fn updated(&mut self, new_source: String, source_map: &SourceMap) {
|
||||
let inner = Arc::make_mut(&mut self.inner);
|
||||
|
||||
match &mut inner.kind {
|
||||
SourceTextKind::Text(text) => *text = new_source,
|
||||
SourceTextKind::Notebook { notebook } => {
|
||||
notebook.update(&source_map, new_source);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn to_raw_content(&self) -> std::borrow::Cow<'_, str> {
|
||||
match &self.inner.kind {
|
||||
SourceTextKind::Text(text) => text.as_str().into(),
|
||||
SourceTextKind::Notebook { notebook } => {
|
||||
let mut output = Vec::new();
|
||||
notebook
|
||||
.write(&mut output)
|
||||
.expect("Writing to a `Vec` should not fail");
|
||||
String::from_utf8(output)
|
||||
.expect(
|
||||
"Notebook should serialize to valid UTF-8 if the source was valid UTF-8",
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SourceText {
|
||||
|
|
@ -117,13 +146,13 @@ impl std::fmt::Debug for SourceText {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Eq, PartialEq, get_size2::GetSize, Clone)]
|
||||
struct SourceTextInner {
|
||||
kind: SourceTextKind,
|
||||
read_error: Option<SourceTextError>,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Eq, PartialEq, get_size2::GetSize, Clone)]
|
||||
enum SourceTextKind {
|
||||
Text(String),
|
||||
Notebook {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ license.workspace = true
|
|||
[dependencies]
|
||||
ruff_db = { workspace = true, features = ["os", "cache"] }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_diagnostics = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
ty_combine = { workspace = true }
|
||||
ty_python_semantic = { workspace = true }
|
||||
ty_project = { workspace = true, features = ["zstd"] }
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ pub(crate) struct CheckCommand {
|
|||
)]
|
||||
pub paths: Vec<SystemPathBuf>,
|
||||
|
||||
#[arg(long)]
|
||||
pub(crate) add_ignore: bool,
|
||||
|
||||
/// Run the command within the given project directory.
|
||||
///
|
||||
/// All `pyproject.toml` files will be discovered by walking up the directory tree from the given project directory,
|
||||
|
|
|
|||
|
|
@ -5,9 +5,14 @@ mod python_version;
|
|||
mod version;
|
||||
|
||||
pub use args::Cli;
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_diagnostics::{Fix, SourceMap};
|
||||
use ruff_text_size::{Ranged as _, TextLen, TextRange, TextSize};
|
||||
use ty_project::metadata::settings::TerminalSettings;
|
||||
use ty_python_semantic::suppress_all;
|
||||
use ty_static::EnvVars;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::Write;
|
||||
use std::process::{ExitCode, Termination};
|
||||
|
||||
|
|
@ -22,16 +27,17 @@ use clap::{CommandFactory, Parser};
|
|||
use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use ruff_db::Db as _;
|
||||
use ruff_db::diagnostic::{
|
||||
Diagnostic, DiagnosticId, DisplayDiagnosticConfig, DisplayDiagnostics, Severity,
|
||||
};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::files::{File, FilePath};
|
||||
use ruff_db::max_parallelism;
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use salsa::Database;
|
||||
use ty_project::metadata::options::ProjectOptionsOverrides;
|
||||
use ty_project::watch::ProjectWatcher;
|
||||
use ty_project::{CollectReporter, Db, watch};
|
||||
use ty_project::{CollectReporter, Db, suppress_all_diagnostics, watch};
|
||||
use ty_project::{ProjectDatabase, ProjectMetadata};
|
||||
use ty_server::run_server;
|
||||
|
||||
|
|
@ -111,6 +117,12 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
|||
.map(|path| SystemPath::absolute(path, &cwd))
|
||||
.collect();
|
||||
|
||||
let mode = if args.add_ignore {
|
||||
MainLoopMode::AddIgnore
|
||||
} else {
|
||||
MainLoopMode::Check
|
||||
};
|
||||
|
||||
let system = OsSystem::new(&cwd);
|
||||
let watch = args.watch;
|
||||
let exit_zero = args.exit_zero;
|
||||
|
|
@ -138,7 +150,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
|||
}
|
||||
|
||||
let (main_loop, main_loop_cancellation_token) =
|
||||
MainLoop::new(project_options_overrides, printer);
|
||||
MainLoop::new(mode, project_options_overrides, printer);
|
||||
|
||||
// Listen to Ctrl+C and abort the watch mode.
|
||||
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
|
||||
|
|
@ -209,6 +221,8 @@ impl Termination for ExitStatus {
|
|||
}
|
||||
|
||||
struct MainLoop {
|
||||
mode: MainLoopMode,
|
||||
|
||||
/// Sender that can be used to send messages to the main loop.
|
||||
sender: crossbeam_channel::Sender<MainLoopMessage>,
|
||||
|
||||
|
|
@ -226,6 +240,7 @@ struct MainLoop {
|
|||
|
||||
impl MainLoop {
|
||||
fn new(
|
||||
mode: MainLoopMode,
|
||||
project_options_overrides: ProjectOptionsOverrides,
|
||||
printer: Printer,
|
||||
) -> (Self, MainLoopCancellationToken) {
|
||||
|
|
@ -233,6 +248,7 @@ impl MainLoop {
|
|||
|
||||
(
|
||||
Self {
|
||||
mode,
|
||||
sender: sender.clone(),
|
||||
receiver,
|
||||
watcher: None,
|
||||
|
|
@ -310,13 +326,13 @@ impl MainLoop {
|
|||
result,
|
||||
revision: check_revision,
|
||||
} => {
|
||||
let terminal_settings = db.project().settings(db).terminal();
|
||||
let display_config = DisplayDiagnosticConfig::default()
|
||||
.format(terminal_settings.output_format.into())
|
||||
.color(colored::control::SHOULD_COLORIZE.should_colorize())
|
||||
.show_fix_diff(true);
|
||||
if check_revision != revision {
|
||||
tracing::debug!(
|
||||
"Discarding check result for outdated revision: current: {revision}, result revision: {check_revision}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if check_revision == revision {
|
||||
if db.project().files(db).is_empty() {
|
||||
tracing::warn!("No python files found under the given path(s)");
|
||||
}
|
||||
|
|
@ -326,8 +342,16 @@ impl MainLoop {
|
|||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
let terminal_settings = db.project().settings(db).terminal();
|
||||
let is_human_readable = terminal_settings.output_format.is_human_readable();
|
||||
|
||||
let diagnostics = match self.mode {
|
||||
MainLoopMode::Check => {
|
||||
let display_config = DisplayDiagnosticConfig::default()
|
||||
.format(terminal_settings.output_format.into())
|
||||
.color(colored::control::SHOULD_COLORIZE.should_colorize())
|
||||
.show_fix_diff(true);
|
||||
|
||||
if result.is_empty() {
|
||||
if is_human_readable {
|
||||
writeln!(
|
||||
|
|
@ -336,16 +360,10 @@ impl MainLoop {
|
|||
"All checks passed!".green().bold()
|
||||
)?;
|
||||
}
|
||||
|
||||
if self.watcher.is_none() {
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
} else {
|
||||
let diagnostics_count = result.len();
|
||||
|
||||
let mut stdout = self.printer.stream_for_details().lock();
|
||||
let exit_status =
|
||||
exit_status_from_diagnostics(&result, terminal_settings);
|
||||
|
||||
// Only render diagnostics if they're going to be displayed, since doing
|
||||
// so is expensive.
|
||||
|
|
@ -365,6 +383,38 @@ impl MainLoop {
|
|||
if diagnostics_count > 1 { "s" } else { "" }
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
MainLoopMode::AddIgnore => {
|
||||
let result = suppress_all_diagnostics(db, result);
|
||||
|
||||
if is_human_readable {
|
||||
writeln!(
|
||||
self.printer.stream_for_failure_summary(),
|
||||
"Ignored {} diagnostic{}",
|
||||
result.count,
|
||||
if result.count > 1 { "s" } else { "" }
|
||||
)?;
|
||||
}
|
||||
|
||||
result.diagnostics
|
||||
}
|
||||
};
|
||||
|
||||
if self.watcher.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let exit_status = if diagnostics.is_empty() {
|
||||
ExitStatus::Success
|
||||
} else {
|
||||
let exit_status =
|
||||
exit_status_from_diagnostics(&diagnostics, terminal_settings);
|
||||
|
||||
exit_status
|
||||
};
|
||||
|
||||
if exit_status.is_internal_error() {
|
||||
tracing::warn!(
|
||||
|
|
@ -372,16 +422,8 @@ impl MainLoop {
|
|||
);
|
||||
}
|
||||
|
||||
if self.watcher.is_none() {
|
||||
return Ok(exit_status);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"Discarding check result for outdated revision: current: {revision}, result revision: {check_revision}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MainLoopMessage::ApplyChanges(changes) => {
|
||||
revision += 1;
|
||||
|
|
@ -406,6 +448,12 @@ impl MainLoop {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum MainLoopMode {
|
||||
Check,
|
||||
AddIgnore,
|
||||
}
|
||||
|
||||
fn exit_status_from_diagnostics(
|
||||
diagnostics: &[Diagnostic],
|
||||
terminal_settings: &TerminalSettings,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use ruff_db::{files::File, parsed::parsed_module};
|
|||
use ruff_diagnostics::Edit;
|
||||
use ruff_text_size::TextRange;
|
||||
use ty_project::Db;
|
||||
use ty_python_semantic::create_suppression_fix;
|
||||
use ty_python_semantic::suppress_single;
|
||||
use ty_python_semantic::types::UNRESOLVED_REFERENCE;
|
||||
|
||||
/// A `QuickFix` Code Action
|
||||
|
|
@ -36,7 +36,7 @@ pub fn code_actions(
|
|||
|
||||
actions.push(QuickFix {
|
||||
title: format!("Ignore '{}' for this line", lint_id.name()),
|
||||
edits: create_suppression_fix(db, file, lint_id, diagnostic_range).into_edits(),
|
||||
edits: suppress_single(db, file, lint_id, diagnostic_range).into_edits(),
|
||||
preferred: false,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ license.workspace = true
|
|||
[dependencies]
|
||||
ruff_cache = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["cache", "serde"] }
|
||||
ruff_diagnostics = { workspace = true }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_memory_usage = { workspace = true }
|
||||
ruff_options_metadata = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use ruff_db::{
|
||||
diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span},
|
||||
files::{File, FilePath},
|
||||
source::source_text,
|
||||
};
|
||||
use ruff_diagnostics::{Fix, SourceMap};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use ty_python_semantic::suppress_all;
|
||||
|
||||
use crate::Db;
|
||||
|
||||
pub struct SuppressAllResult {
|
||||
/// The non-lint diagnostics that can't be suppressed.
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
|
||||
/// The number of diagnostics that were suppressed.
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
/// Suppress all
|
||||
pub fn suppress_all_diagnostics(db: &dyn Db, diagnostics: Vec<Diagnostic>) -> SuppressAllResult {
|
||||
let system = db
|
||||
.system()
|
||||
.as_writable()
|
||||
.expect("System should be writable");
|
||||
|
||||
let mut non_lint_diagnostics = diagnostics;
|
||||
let mut by_file: BTreeMap<File, Vec<_>> = BTreeMap::new();
|
||||
|
||||
non_lint_diagnostics.retain(|diagnostic| {
|
||||
let DiagnosticId::Lint(lint_id) = diagnostic.id() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let Some(span) = diagnostic.primary_span() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let Some(range) = span.range() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
by_file
|
||||
.entry(span.expect_ty_file())
|
||||
.or_default()
|
||||
.push((lint_id, range));
|
||||
|
||||
false
|
||||
});
|
||||
|
||||
let mut count = 0usize;
|
||||
for (file, to_suppress) in by_file {
|
||||
let FilePath::System(path) = file.path(db) else {
|
||||
tracing::debug!(
|
||||
"Skipping file `{}` with non-system path because vendored and system virtual file paths are read-only",
|
||||
file.path(db)
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut source = source_text(db, file);
|
||||
|
||||
let count_current_file = to_suppress.len();
|
||||
|
||||
let fixes = suppress_all(db, file, to_suppress);
|
||||
let (new_source, source_map) = apply_fixes(db, file, fixes);
|
||||
|
||||
source.updated(new_source, &source_map);
|
||||
|
||||
// Create new source from applying fixes
|
||||
if let Err(err) = system.write_file(path, &*source.to_raw_content()) {
|
||||
let mut diag = Diagnostic::new(
|
||||
DiagnosticId::Io,
|
||||
Severity::Error,
|
||||
format_args!("Failed to write fixes: {err}"),
|
||||
);
|
||||
diag.annotate(Annotation::primary(Span::from(file)));
|
||||
non_lint_diagnostics.push(diag);
|
||||
continue;
|
||||
}
|
||||
|
||||
count += count_current_file;
|
||||
}
|
||||
|
||||
SuppressAllResult {
|
||||
diagnostics: non_lint_diagnostics,
|
||||
count,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a series of fixes to `File` and returns the updated source code along with the source map.
|
||||
fn apply_fixes(db: &dyn Db, file: File, mut fixes: Vec<Fix>) -> (String, SourceMap) {
|
||||
let source = source_text(db, file);
|
||||
let source = source.as_str();
|
||||
|
||||
let mut output = String::with_capacity(source.len());
|
||||
let mut last_pos: Option<TextSize> = None;
|
||||
|
||||
let mut source_map = SourceMap::default();
|
||||
|
||||
fixes.sort_unstable_by_key(|fix| fix.min_start());
|
||||
|
||||
for fix in fixes {
|
||||
let mut edits = fix.edits().iter().peekable();
|
||||
|
||||
// If the fix contains at least one new edit, enforce isolation and positional requirements.
|
||||
if let Some(first) = edits.peek() {
|
||||
// If this fix overlaps with a fix we've already applied, skip it.
|
||||
if last_pos.is_some_and(|last_pos| last_pos >= first.start()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let mut applied_edits = Vec::with_capacity(fix.edits().len());
|
||||
for edit in edits {
|
||||
// Add all contents from `last_pos` to `fix.location`.
|
||||
let slice = &source[TextRange::new(last_pos.unwrap_or_default(), edit.start())];
|
||||
output.push_str(slice);
|
||||
|
||||
// Add the start source marker for the patch.
|
||||
source_map.push_start_marker(edit, output.text_len());
|
||||
|
||||
// Add the patch itself.
|
||||
output.push_str(edit.content().unwrap_or_default());
|
||||
|
||||
// Add the end source marker for the added patch.
|
||||
source_map.push_end_marker(edit, output.text_len());
|
||||
|
||||
// Track that the edit was applied.
|
||||
last_pos = Some(edit.end());
|
||||
applied_edits.push(edit);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the remaining content.
|
||||
let slice = &source[last_pos.unwrap_or_default().to_usize()..];
|
||||
output.push_str(slice);
|
||||
|
||||
(output, source_map)
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ use crate::walk::{ProjectFilesFilter, ProjectFilesWalker};
|
|||
pub use db::tests::TestDb;
|
||||
pub use db::{ChangeResult, CheckMode, Db, ProjectDatabase, SalsaMemoryDump};
|
||||
use files::{Index, Indexed, IndexedFiles};
|
||||
pub use fixes::suppress_all_diagnostics;
|
||||
use metadata::settings::Settings;
|
||||
pub use metadata::{ProjectMetadata, ProjectMetadataError};
|
||||
use ruff_db::diagnostic::{
|
||||
|
|
@ -34,6 +35,7 @@ use ty_python_semantic::types::check_types;
|
|||
|
||||
mod db;
|
||||
mod files;
|
||||
mod fixes;
|
||||
mod glob;
|
||||
pub mod metadata;
|
||||
mod walk;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ pub use semantic_model::{
|
|||
Completion, HasDefinition, HasType, MemberDefinition, NameKind, SemanticModel,
|
||||
};
|
||||
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
|
||||
pub use suppression::create_suppression_fix;
|
||||
pub use suppression::{suppress_all, suppress_single};
|
||||
pub use types::DisplaySettings;
|
||||
pub use types::ide_support::{
|
||||
ImportAliasResolution, ResolvedDefinition, definitions_for_attribute, definitions_for_bin_op,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
use ruff_db::diagnostic::LintName;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
|
@ -375,15 +379,142 @@ fn check_unused_suppressions(context: &mut CheckSuppressionsContext) {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn suppress_all<I>(db: &dyn Db, file: File, ids_with_range: I) -> Vec<Fix>
|
||||
where
|
||||
I: IntoIterator<Item = (LintName, TextRange)>,
|
||||
{
|
||||
let grouped = group_by_suppression_range(db, file, ids_with_range);
|
||||
create_all_fixes(db, file, grouped)
|
||||
}
|
||||
|
||||
/// Creates a fix to suppress a single lint.
|
||||
pub fn suppress_single(db: &dyn Db, file: File, id: LintId, range: TextRange) -> Fix {
|
||||
let suppression_range = suppression_range(db, file, range);
|
||||
create_suppression_fix(db, file, id.name(), suppression_range)
|
||||
}
|
||||
|
||||
fn create_all_fixes(
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
grouped: BTreeMap<SuppressionRange, BTreeSet<LintName>>,
|
||||
) -> Vec<Fix> {
|
||||
let mut fixes = Vec::new();
|
||||
|
||||
for (range, lints) in grouped {
|
||||
for lint in lints.into_iter().rev() {
|
||||
let fix = create_suppression_fix(db, file, lint, range);
|
||||
fixes.push(fix);
|
||||
}
|
||||
}
|
||||
|
||||
fixes
|
||||
}
|
||||
|
||||
fn group_by_suppression_range<I>(
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
ids_with_range: I,
|
||||
) -> BTreeMap<SuppressionRange, BTreeSet<LintName>>
|
||||
where
|
||||
I: IntoIterator<Item = (LintName, TextRange)>,
|
||||
{
|
||||
let mut map: BTreeMap<SuppressionRange, BTreeSet<LintName>> = BTreeMap::new();
|
||||
for (id, range) in ids_with_range {
|
||||
let full_range = suppression_range(db, file, range);
|
||||
map.entry(full_range).or_default().insert(id);
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
/// Returns the suppression range for the given `range`.
|
||||
///
|
||||
/// The suppression range is defined as:
|
||||
///
|
||||
/// * `start`: The `end` of the preceding `Newline` or `NonLogicalLine` token.
|
||||
/// * `end`: The `start` of the first `NonLogicalLine` or `Newline` token coming after the range.
|
||||
///
|
||||
/// For most ranges, this means the suppression range starts at the beginning of the physical line
|
||||
/// and ends at the end of the physical line containing `range`. The exceptions to this are:
|
||||
///
|
||||
/// * If `range` is within a single-line interpolated expression, then the start and end are extended to the start and end of the enclosing interpolated string.
|
||||
/// * If there's a line continuation, then the suppression range is extended to include the following line too.
|
||||
/// * If there's a multiline string, then the suppression range is extended to cover the starting and ending line of the multiline string.
|
||||
fn suppression_range(db: &dyn Db, file: File, range: TextRange) -> SuppressionRange {
|
||||
let parsed = parsed_module(db, file).load(db);
|
||||
|
||||
let before_tokens = parsed.tokens().before(range.start());
|
||||
let line_start = before_tokens
|
||||
.iter()
|
||||
.rfind(|token| {
|
||||
matches!(
|
||||
token.kind(),
|
||||
TokenKind::Newline | TokenKind::NonLogicalNewline
|
||||
)
|
||||
})
|
||||
.map(Ranged::end)
|
||||
.unwrap_or(TextSize::default());
|
||||
|
||||
let after_tokens = parsed.tokens().after(range.end());
|
||||
let line_end = after_tokens
|
||||
.iter()
|
||||
.find(|token| {
|
||||
matches!(
|
||||
token.kind(),
|
||||
TokenKind::Newline | TokenKind::NonLogicalNewline
|
||||
)
|
||||
})
|
||||
.map(Ranged::start)
|
||||
.unwrap_or(range.end());
|
||||
|
||||
SuppressionRange(TextRange::new(line_start, line_end))
|
||||
}
|
||||
|
||||
/// The range of the suppression.
|
||||
///
|
||||
/// Guranteed to start at the start of a line and
|
||||
/// ends at the end of a line (right before the `\n`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct SuppressionRange(TextRange);
|
||||
|
||||
impl SuppressionRange {
|
||||
fn text_range(&self) -> TextRange {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn line_end(&self) -> TextSize {
|
||||
self.0.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for SuppressionRange {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for SuppressionRange {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.0.ordering(other.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a fix for adding a suppression comment to suppress `lint` for `range`.
|
||||
///
|
||||
/// The fix prefers adding the code to an existing `ty: ignore[]` comment over
|
||||
/// adding a new suppression comment.
|
||||
pub fn create_suppression_fix(db: &dyn Db, file: File, id: LintId, range: TextRange) -> Fix {
|
||||
fn create_suppression_fix(
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
name: LintName,
|
||||
suppression_range: SuppressionRange,
|
||||
) -> Fix {
|
||||
let suppressions = suppressions(db, file);
|
||||
let source = source_text(db, file);
|
||||
|
||||
let mut existing_suppressions = suppressions.line_suppressions(range).filter(|suppression| {
|
||||
let mut existing_suppressions = suppressions
|
||||
.line_suppressions(suppression_range.text_range())
|
||||
.filter(|suppression| {
|
||||
matches!(
|
||||
suppression.target,
|
||||
SuppressionTarget::Lint(_) | SuppressionTarget::Empty,
|
||||
|
|
@ -398,9 +529,9 @@ pub fn create_suppression_fix(db: &dyn Db, file: File, id: LintId, range: TextRa
|
|||
let up_to_last_code = before_closing_paren.trim_end();
|
||||
|
||||
let insertion = if up_to_last_code.ends_with(',') {
|
||||
format!(" {id}", id = id.name())
|
||||
format!(" {name}")
|
||||
} else {
|
||||
format!(", {id}", id = id.name())
|
||||
format!(", {name}")
|
||||
};
|
||||
|
||||
let relative_offset_from_end = comment_text.text_len() - up_to_last_code.text_len();
|
||||
|
|
@ -414,28 +545,13 @@ pub fn create_suppression_fix(db: &dyn Db, file: File, id: LintId, range: TextRa
|
|||
|
||||
// Always insert a new suppression at the end of the range to avoid having to deal with multiline strings
|
||||
// etc.
|
||||
let parsed = parsed_module(db, file).load(db);
|
||||
let tokens_after = parsed.tokens().after(range.end());
|
||||
|
||||
// Same as for `line_end` when building up the `suppressions`: Ignore newlines
|
||||
// in multiline-strings, inside f-strings, or after a line continuation because we can't
|
||||
// place a comment on those lines.
|
||||
let line_end = tokens_after
|
||||
.iter()
|
||||
.find(|token| {
|
||||
matches!(
|
||||
token.kind(),
|
||||
TokenKind::Newline | TokenKind::NonLogicalNewline
|
||||
)
|
||||
})
|
||||
.map(Ranged::start)
|
||||
.unwrap_or(source.text_len());
|
||||
|
||||
let line_end = suppression_range.line_end();
|
||||
let up_to_line_end = &source[..line_end.to_usize()];
|
||||
let up_to_first_content = up_to_line_end.trim_end();
|
||||
let trailing_whitespace_len = up_to_line_end.text_len() - up_to_first_content.text_len();
|
||||
|
||||
let insertion = format!(" # ty:ignore[{id}]", id = id.name());
|
||||
let insertion = format!(" # ty:ignore[{name}]");
|
||||
|
||||
Fix::safe_edit(if trailing_whitespace_len == TextSize::ZERO {
|
||||
Edit::insertion(insertion, line_end)
|
||||
|
|
@ -613,7 +729,7 @@ impl Suppressions {
|
|||
// Don't use intersect to avoid that suppressions on inner-expression
|
||||
// ignore errors for outer expressions
|
||||
suppression.suppressed_range.contains(range.start())
|
||||
|| suppression.suppressed_range.contains(range.end())
|
||||
|| suppression.suppressed_range.contains_inclusive(range.end())
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue