Respect file exclusions in ruff server (#11590)

## Summary

Closes https://github.com/astral-sh/ruff/issues/11587.

## Test Plan

- Added a lint error to `test_server.py` in `vscode-ruff`.
- Validated that, prior to this change, diagnostics appeared in the
file.
- Validated that, with this change, no diagnostics were shown.
- Validated that, with this change, no diagnostics were fixed on-save.
This commit is contained in:
Charlie Marsh 2024-05-28 22:58:36 -04:00 committed by GitHub
parent 531ae5227c
commit 204c59e353
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 235 additions and 43 deletions

View file

@ -1,3 +1,7 @@
use std::borrow::Cow;
use rustc_hash::FxHashMap;
use ruff_linter::{
linter::{FixerResult, LinterResult},
packaging::detect_package_root,
@ -5,8 +9,8 @@ use ruff_linter::{
};
use ruff_notebook::SourceValue;
use ruff_source_file::LineIndex;
use rustc_hash::FxHashMap;
use std::borrow::Cow;
use ruff_workspace::resolver::match_any_exclusion;
use ruff_workspace::FileResolverSettings;
use crate::{
edit::{Replacement, ToRangeExt},
@ -20,12 +24,29 @@ pub(crate) type Fixes = FxHashMap<lsp_types::Url, Vec<lsp_types::TextEdit>>;
pub(crate) fn fix_all(
query: &DocumentQuery,
file_resolver_settings: &FileResolverSettings,
linter_settings: &LinterSettings,
encoding: PositionEncoding,
) -> crate::Result<Fixes> {
let document_path = query.file_path();
let source_kind = query.make_source_kind();
// If the document is excluded, return an empty list of fixes.
if let Some(exclusion) = match_any_exclusion(
document_path,
&file_resolver_settings.exclude,
&file_resolver_settings.extend_exclude,
Some(&linter_settings.exclude),
None,
) {
tracing::debug!(
"Ignored path via `{}`: {}",
exclusion,
document_path.display()
);
return Ok(Fixes::default());
}
let package = detect_package_root(
document_path
.parent()

View file

@ -16,6 +16,8 @@ use ruff_python_index::Indexer;
use ruff_python_parser::AsMode;
use ruff_source_file::{LineIndex, Locator};
use ruff_text_size::{Ranged, TextRange};
use ruff_workspace::resolver::match_any_exclusion;
use ruff_workspace::FileResolverSettings;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
@ -60,12 +62,29 @@ pub(crate) type Diagnostics = FxHashMap<lsp_types::Url, Vec<lsp_types::Diagnosti
pub(crate) fn check(
query: &DocumentQuery,
file_resolver_settings: &FileResolverSettings,
linter_settings: &LinterSettings,
encoding: PositionEncoding,
) -> Diagnostics {
let document_path = query.file_path();
let source_kind = query.make_source_kind();
// If the document is excluded, return an empty list of diagnostics.
if let Some(exclusion) = match_any_exclusion(
document_path,
&file_resolver_settings.exclude,
&file_resolver_settings.extend_exclude,
Some(&linter_settings.exclude),
None,
) {
tracing::debug!(
"Ignored path via `{}`: {}",
exclusion,
document_path.display()
);
return Diagnostics::default();
}
let package = detect_package_root(
document_path
.parent()

View file

@ -10,6 +10,7 @@ pub(super) fn generate_diagnostics(snapshot: &DocumentSnapshot) -> Diagnostics {
if snapshot.client_settings().lint() {
crate::lint::check(
snapshot.query(),
snapshot.query().settings().file_resolver(),
snapshot.query().settings().linter(),
snapshot.encoding(),
)

View file

@ -1,3 +1,8 @@
use lsp_server::ErrorCode;
use lsp_types::{self as types, request as req};
use rustc_hash::FxHashSet;
use types::{CodeActionKind, CodeActionOrCommand};
use crate::edit::WorkspaceEditTracker;
use crate::lint::{fixes_for_diagnostics, DiagnosticFix};
use crate::server::api::LSPResult;
@ -5,10 +10,6 @@ use crate::server::SupportedCodeAction;
use crate::server::{client::Notifier, Result};
use crate::session::DocumentSnapshot;
use crate::DIAGNOSTIC_NAME;
use lsp_server::ErrorCode;
use lsp_types::{self as types, request as req};
use rustc_hash::FxHashSet;
use types::{CodeActionKind, CodeActionOrCommand};
use super::code_action_resolve::{resolve_edit_for_fix_all, resolve_edit_for_organize_imports};
@ -156,6 +157,7 @@ fn fix_all(snapshot: &DocumentSnapshot) -> crate::Result<CodeActionOrCommand> {
Some(resolve_edit_for_fix_all(
document,
snapshot.resolved_client_capabilities(),
snapshot.query().settings().file_resolver(),
snapshot.query().settings().linter(),
snapshot.encoding(),
)?),
@ -192,6 +194,7 @@ fn notebook_fix_all(snapshot: &DocumentSnapshot) -> crate::Result<CodeActionOrCo
Some(resolve_edit_for_fix_all(
document,
snapshot.resolved_client_capabilities(),
snapshot.query().settings().file_resolver(),
snapshot.query().settings().linter(),
snapshot.encoding(),
)?),
@ -228,6 +231,7 @@ fn organize_imports(snapshot: &DocumentSnapshot) -> crate::Result<CodeActionOrCo
Some(resolve_edit_for_organize_imports(
document,
snapshot.resolved_client_capabilities(),
snapshot.query().settings().file_resolver(),
snapshot.query().settings().linter(),
snapshot.encoding(),
)?),
@ -264,6 +268,7 @@ fn notebook_organize_imports(snapshot: &DocumentSnapshot) -> crate::Result<CodeA
Some(resolve_edit_for_organize_imports(
document,
snapshot.resolved_client_capabilities(),
snapshot.query().settings().file_resolver(),
snapshot.query().settings().linter(),
snapshot.encoding(),
)?),

View file

@ -1,5 +1,12 @@
use std::borrow::Cow;
use lsp_server::ErrorCode;
use lsp_types::{self as types, request as req};
use ruff_linter::codes::Rule;
use ruff_linter::settings::LinterSettings;
use ruff_workspace::FileResolverSettings;
use crate::edit::WorkspaceEditTracker;
use crate::fix::Fixes;
use crate::server::api::LSPResult;
@ -7,10 +14,6 @@ use crate::server::SupportedCodeAction;
use crate::server::{client::Notifier, Result};
use crate::session::{DocumentQuery, DocumentSnapshot, ResolvedClientCapabilities};
use crate::PositionEncoding;
use lsp_server::ErrorCode;
use lsp_types::{self as types, request as req};
use ruff_linter::codes::Rule;
use ruff_linter::settings::LinterSettings;
pub(crate) struct CodeActionResolve;
@ -22,7 +25,7 @@ impl super::BackgroundDocumentRequestHandler for CodeActionResolve {
fn document_url(params: &types::CodeAction) -> Cow<types::Url> {
let uri: lsp_types::Url = serde_json::from_value(params.data.clone().unwrap_or_default())
.expect("code actions should have a URI in their data fields");
std::borrow::Cow::Owned(uri)
Cow::Owned(uri)
}
fn run_with_snapshot(
snapshot: DocumentSnapshot,
@ -54,6 +57,7 @@ impl super::BackgroundDocumentRequestHandler for CodeActionResolve {
resolve_edit_for_fix_all(
query,
snapshot.resolved_client_capabilities(),
query.settings().file_resolver(),
query.settings().linter(),
snapshot.encoding(),
)
@ -64,6 +68,7 @@ impl super::BackgroundDocumentRequestHandler for CodeActionResolve {
resolve_edit_for_organize_imports(
query,
snapshot.resolved_client_capabilities(),
query.settings().file_resolver(),
query.settings().linter(),
snapshot.encoding(),
)
@ -84,12 +89,13 @@ impl super::BackgroundDocumentRequestHandler for CodeActionResolve {
pub(super) fn resolve_edit_for_fix_all(
query: &DocumentQuery,
client_capabilities: &ResolvedClientCapabilities,
file_resolver_settings: &FileResolverSettings,
linter_settings: &LinterSettings,
encoding: PositionEncoding,
) -> crate::Result<types::WorkspaceEdit> {
let mut tracker = WorkspaceEditTracker::new(client_capabilities);
tracker.set_fixes_for_document(
fix_all_edit(query, linter_settings, encoding)?,
fix_all_edit(query, file_resolver_settings, linter_settings, encoding)?,
query.version(),
)?;
Ok(tracker.into_workspace_edit())
@ -97,21 +103,23 @@ pub(super) fn resolve_edit_for_fix_all(
pub(super) fn fix_all_edit(
query: &DocumentQuery,
file_resolver_settings: &FileResolverSettings,
linter_settings: &LinterSettings,
encoding: PositionEncoding,
) -> crate::Result<Fixes> {
crate::fix::fix_all(query, linter_settings, encoding)
crate::fix::fix_all(query, file_resolver_settings, linter_settings, encoding)
}
pub(super) fn resolve_edit_for_organize_imports(
query: &DocumentQuery,
client_capabilities: &ResolvedClientCapabilities,
linter_settings: &ruff_linter::settings::LinterSettings,
file_resolver_settings: &FileResolverSettings,
linter_settings: &LinterSettings,
encoding: PositionEncoding,
) -> crate::Result<types::WorkspaceEdit> {
let mut tracker = WorkspaceEditTracker::new(client_capabilities);
tracker.set_fixes_for_document(
organize_imports_edit(query, linter_settings, encoding)?,
organize_imports_edit(query, file_resolver_settings, linter_settings, encoding)?,
query.version(),
)?;
Ok(tracker.into_workspace_edit())
@ -119,6 +127,7 @@ pub(super) fn resolve_edit_for_organize_imports(
pub(super) fn organize_imports_edit(
query: &DocumentQuery,
file_resolver_settings: &FileResolverSettings,
linter_settings: &LinterSettings,
encoding: PositionEncoding,
) -> crate::Result<Fixes> {
@ -130,5 +139,5 @@ pub(super) fn organize_imports_edit(
.into_iter()
.collect();
crate::fix::fix_all(query, &linter_settings, encoding)
crate::fix::fix_all(query, file_resolver_settings, &linter_settings, encoding)
}

View file

@ -64,6 +64,7 @@ impl super::SyncRequestHandler for ExecuteCommand {
Command::FixAll => {
let fixes = super::code_action_resolve::fix_all_edit(
snapshot.query(),
snapshot.query().settings().file_resolver(),
snapshot.query().settings().linter(),
snapshot.encoding(),
)
@ -81,6 +82,7 @@ impl super::SyncRequestHandler for ExecuteCommand {
Command::OrganizeImports => {
let fixes = super::code_action_resolve::organize_imports_edit(
snapshot.query(),
snapshot.query().settings().file_resolver(),
snapshot.query().settings().linter(),
snapshot.encoding(),
)

View file

@ -1,14 +1,19 @@
use std::path::Path;
use lsp_types::{self as types, request as req};
use types::TextEdit;
use ruff_python_ast::PySourceType;
use ruff_source_file::LineIndex;
use ruff_workspace::resolver::match_any_exclusion;
use ruff_workspace::{FileResolverSettings, FormatterSettings};
use crate::edit::{Replacement, ToRangeExt};
use crate::fix::Fixes;
use crate::server::api::LSPResult;
use crate::server::{client::Notifier, Result};
use crate::session::DocumentSnapshot;
use crate::{PositionEncoding, TextDocument};
use lsp_types::{self as types, request as req};
use ruff_python_ast::PySourceType;
use ruff_source_file::LineIndex;
use ruff_workspace::FormatterSettings;
use types::TextEdit;
pub(crate) struct Format;
@ -39,6 +44,8 @@ pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result<Fixes>
if let Some(changes) = format_text_document(
text_document,
snapshot.query().source_type(),
snapshot.query().file_path(),
snapshot.query().settings().file_resolver(),
snapshot.query().settings().formatter(),
snapshot.encoding(),
true,
@ -50,6 +57,8 @@ pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result<Fixes>
if let Some(changes) = format_text_document(
snapshot.query().as_single_document().unwrap(),
snapshot.query().source_type(),
snapshot.query().file_path(),
snapshot.query().settings().file_resolver(),
snapshot.query().settings().formatter(),
snapshot.encoding(),
false,
@ -71,6 +80,8 @@ pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result<super::Form
format_text_document(
text_document,
snapshot.query().source_type(),
snapshot.query().file_path(),
snapshot.query().settings().file_resolver(),
snapshot.query().settings().formatter(),
snapshot.encoding(),
snapshot.query().as_notebook().is_some(),
@ -80,10 +91,24 @@ pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result<super::Form
fn format_text_document(
text_document: &TextDocument,
source_type: PySourceType,
file_path: &Path,
file_resolver_settings: &FileResolverSettings,
formatter_settings: &FormatterSettings,
encoding: PositionEncoding,
is_notebook: bool,
) -> Result<super::FormatResponse> {
// If the document is excluded, return early.
if let Some(exclusion) = match_any_exclusion(
file_path,
&file_resolver_settings.exclude,
&file_resolver_settings.extend_exclude,
None,
Some(&formatter_settings.exclude),
) {
tracing::debug!("Ignored path via `{}`: {}", exclusion, file_path.display());
return Ok(None);
}
let source = text_document.contents();
let mut formatted = crate::format::format(text_document, source_type, formatter_settings)
.with_failure_code(lsp_server::ErrorCode::InternalError)?;

View file

@ -1,8 +1,16 @@
use std::path::Path;
use lsp_types::{self as types, request as req, Range};
use ruff_python_ast::PySourceType;
use ruff_workspace::resolver::match_any_exclusion;
use ruff_workspace::{FileResolverSettings, FormatterSettings};
use crate::edit::{RangeExt, ToRangeExt};
use crate::server::api::LSPResult;
use crate::server::{client::Notifier, Result};
use crate::session::DocumentSnapshot;
use lsp_types::{self as types, request as req};
use crate::{PositionEncoding, TextDocument};
pub(crate) struct FormatRange;
@ -17,25 +25,63 @@ impl super::BackgroundDocumentRequestHandler for FormatRange {
_notifier: Notifier,
params: types::DocumentRangeFormattingParams,
) -> Result<super::FormatResponse> {
let document = snapshot
.query()
.as_single_document()
.expect("hover should only be called on text documents or notebook cells");
let text = document.contents();
let index = document.index();
let range = params.range.to_text_range(text, index, snapshot.encoding());
let formatted_range = crate::format::format_range(
document,
snapshot.query().source_type(),
snapshot.query().settings().formatter(),
range,
)
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
Ok(Some(vec![types::TextEdit {
range: formatted_range
.source_range()
.to_range(text, index, snapshot.encoding()),
new_text: formatted_range.into_code(),
}]))
format_document_range(&snapshot, params.range)
}
}
/// Formats the specified [`Range`] in the [`DocumentSnapshot`].
fn format_document_range(
snapshot: &DocumentSnapshot,
range: Range,
) -> Result<super::FormatResponse> {
let text_document = snapshot
.query()
.as_single_document()
.expect("format should only be called on text documents or notebook cells");
format_text_document_range(
text_document,
range,
snapshot.query().source_type(),
snapshot.query().file_path(),
snapshot.query().settings().file_resolver(),
snapshot.query().settings().formatter(),
snapshot.encoding(),
)
}
/// Formats the specified [`Range`] in the [`TextDocument`].
fn format_text_document_range(
text_document: &TextDocument,
range: Range,
source_type: PySourceType,
file_path: &Path,
file_resolver_settings: &FileResolverSettings,
formatter_settings: &FormatterSettings,
encoding: PositionEncoding,
) -> Result<super::FormatResponse> {
// If the document is excluded, return early.
if let Some(exclusion) = match_any_exclusion(
file_path,
&file_resolver_settings.exclude,
&file_resolver_settings.extend_exclude,
None,
Some(&formatter_settings.exclude),
) {
tracing::debug!("Ignored path via `{}`: {}", exclusion, file_path.display());
return Ok(None);
}
let text = text_document.contents();
let index = text_document.index();
let range = range.to_text_range(text, index, encoding);
let formatted_range =
crate::format::format_range(text_document, source_type, formatter_settings, range)
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
Ok(Some(vec![types::TextEdit {
range: formatted_range
.source_range()
.to_range(text, index, encoding),
new_text: formatted_range.into_code(),
}]))
}

View file

@ -479,7 +479,7 @@ impl DocumentQuery {
}
/// Get the underlying file path for the document selected by this query.
pub(crate) fn file_path(&self) -> &PathBuf {
pub(crate) fn file_path(&self) -> &Path {
match self {
Self::Text { file_path, .. } | Self::Notebook { file_path, .. } => file_path,
}

View file

@ -73,10 +73,17 @@ impl RuffSettings {
}
}
/// Return the [`ruff_workspace::FileResolverSettings`] for this [`RuffSettings`].
pub(crate) fn file_resolver(&self) -> &ruff_workspace::FileResolverSettings {
&self.file_resolver
}
/// Return the [`ruff_linter::settings::LinterSettings`] for this [`RuffSettings`].
pub(crate) fn linter(&self) -> &ruff_linter::settings::LinterSettings {
&self.linter
}
/// Return the [`ruff_workspace::FormatterSettings`] for this [`RuffSettings`].
pub(crate) fn formatter(&self) -> &ruff_workspace::FormatterSettings {
&self.formatter
}

View file

@ -626,6 +626,63 @@ pub fn match_candidate_exclusion(
exclusion.is_match_candidate(file_path) || exclusion.is_match_candidate(file_basename)
}
#[derive(Debug, Copy, Clone)]
pub enum ExclusionKind {
/// The exclusion came from the `exclude` setting.
Exclude,
/// The exclusion came from the `extend-exclude` setting.
ExtendExclude,
/// The exclusion came from the `lint.exclude` setting.
LintExclude,
/// The exclusion came from the `lint.extend-exclude` setting.
FormatExclude,
}
impl std::fmt::Display for ExclusionKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExclusionKind::Exclude => write!(f, "exclude"),
ExclusionKind::ExtendExclude => write!(f, "extend-exclude"),
ExclusionKind::LintExclude => write!(f, "lint.exclude"),
ExclusionKind::FormatExclude => write!(f, "lint.extend-exclude"),
}
}
}
/// Return the [`ExclusionKind`] for a given [`Path`], if the path or any of its ancestors match
/// any of the exclusion criteria.
pub fn match_any_exclusion(
path: &Path,
exclude: &GlobSet,
extend_exclude: &GlobSet,
lint_exclude: Option<&GlobSet>,
format_exclude: Option<&GlobSet>,
) -> Option<ExclusionKind> {
for path in path.ancestors() {
if let Some(basename) = path.file_name() {
let path = Candidate::new(path);
let basename = Candidate::new(basename);
if match_candidate_exclusion(&path, &basename, exclude) {
return Some(ExclusionKind::Exclude);
}
if match_candidate_exclusion(&path, &basename, extend_exclude) {
return Some(ExclusionKind::ExtendExclude);
}
if let Some(lint_exclude) = lint_exclude {
if match_candidate_exclusion(&path, &basename, lint_exclude) {
return Some(ExclusionKind::LintExclude);
}
}
if let Some(format_exclude) = format_exclude {
if match_candidate_exclusion(&path, &basename, format_exclude) {
return Some(ExclusionKind::FormatExclude);
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use std::fs::{create_dir, File};