Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

Commit

Permalink
feat(rome_analyze): allow suppress rule via code actions
Browse files Browse the repository at this point in the history
  • Loading branch information
ematipico committed Nov 15, 2022
1 parent 875f4fb commit eef75b3
Show file tree
Hide file tree
Showing 21 changed files with 319 additions and 87 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/rome_analyze/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ rustc-hash = { workspace = true }
serde = { version = "1.0.136", features = ["derive"] }
serde_json = { version = "1.0.85", features = ["raw_value"]}
schemars = { version = "0.8.10", optional = true }
tracing = { workspace = true }

[dev-dependencies]
rome_js_syntax = { path = "../rome_js_syntax" }
Expand Down
2 changes: 1 addition & 1 deletion crates/rome_analyze/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub use crate::registry::{
};
pub use crate::rule::{
CategoryLanguage, GroupCategory, GroupLanguage, Rule, RuleAction, RuleDiagnostic, RuleGroup,
RuleMeta, RuleMetadata,
RuleMeta, RuleMetadata, SuppressAction,
};
pub use crate::services::{FromServices, MissingServicesDiagnostic, ServiceBag};
use crate::signals::DiagnosticSignal;
Expand Down
26 changes: 25 additions & 1 deletion crates/rome_analyze/src/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use rome_diagnostics::v2::{
Visit,
};
use rome_diagnostics::Applicability;
use rome_rowan::{BatchMutation, Language, TextRange};
use rome_rowan::{BatchMutation, Language, SyntaxNode, TextRange};
use serde::de::DeserializeOwned;

/// Static metadata containing information about a rule
Expand Down Expand Up @@ -328,6 +328,16 @@ pub trait Rule: RuleMeta {
let (..) = (ctx, state);
None
}

/// Create a code action that allows to suppress the rule. The function
/// has to return the node to which the suppression comment needs to be applied.
fn can_suppress(
ctx: &RuleContext<Self>,
state: &Self::State,
) -> Option<SuppressAction<RuleLanguage<Self>>> {
let _ = (ctx, state);
None
}
}

/// Diagnostic object returned by a single analysis rule
Expand Down Expand Up @@ -482,3 +492,17 @@ pub struct RuleAction<L: Language> {
pub message: MarkupBuf,
pub mutation: BatchMutation<L>,
}

pub struct SuppressAction<L: Language>(SyntaxNode<L>);

impl<L: Language> SuppressAction<L> {
pub fn node(&self) -> &SyntaxNode<L> {
&self.0
}
}

impl<L: Language> From<SyntaxNode<L>> for SuppressAction<L> {
fn from(node: SyntaxNode<L>) -> Self {
Self(node)
}
}
230 changes: 198 additions & 32 deletions crates/rome_analyze/src/signals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ use crate::{
rule::Rule,
AnalyzerDiagnostic, AnalyzerOptions, Queryable, RuleGroup, ServiceBag,
};
use rome_console::MarkupBuf;
use rome_console::{markup, MarkupBuf};
use rome_diagnostics::file::FileSpan;
use rome_diagnostics::v2::advice::CodeSuggestionAdvice;
use rome_diagnostics::{file::FileId, Applicability, CodeSuggestion};
use rome_rowan::{BatchMutation, Language};
use rome_rowan::{AstNode, BatchMutation, BatchMutationExt, Language, TriviaPieceKind};
use std::borrow::Cow;
use std::iter::FusedIterator;
use std::vec::IntoIter;

/// Event raised by the analyzer when a [Rule](crate::Rule)
/// emits a diagnostic, a code action, or both
pub trait AnalyzerSignal<L: Language> {
fn diagnostic(&self) -> Option<AnalyzerDiagnostic>;
fn action(&self) -> Option<AnalyzerAction<L>>;
fn actions(&self) -> Option<AnalyzerActionIter<L>>;
}

/// Simple implementation of [AnalyzerSignal] generating a [AnalyzerDiagnostic] from a
Expand All @@ -41,7 +44,7 @@ where
Some((self.factory)())
}

fn action(&self) -> Option<AnalyzerAction<L>> {
fn actions(&self) -> Option<AnalyzerActionIter<L>> {
None
}
}
Expand All @@ -51,7 +54,7 @@ where
///
/// This struct can be converted into a [CodeSuggestion] and injected into
/// a diagnostic emitted by the same signal
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct AnalyzerAction<L: Language> {
pub group_name: &'static str,
pub rule_name: &'static str,
Expand All @@ -62,13 +65,19 @@ pub struct AnalyzerAction<L: Language> {
pub mutation: BatchMutation<L>,
}

impl<L> From<AnalyzerAction<L>> for CodeSuggestionAdvice<MarkupBuf>
where
L: Language,
{
impl<L: Language> AnalyzerAction<L> {
pub fn is_suppression(&self) -> bool {
self.category.matches("quickfix.rome.suppressRule")
}
}

pub struct AnalyzerActionIter<L: Language> {
analyzer_actions: IntoIter<AnalyzerAction<L>>,
}

impl<L: Language> From<AnalyzerAction<L>> for CodeSuggestionAdvice<MarkupBuf> {
fn from(action: AnalyzerAction<L>) -> Self {
let (_, suggestion) = action.mutation.as_text_edits().unwrap_or_default();

CodeSuggestionAdvice {
applicability: action.applicability,
msg: action.message,
Expand All @@ -77,22 +86,119 @@ where
}
}

impl<L> From<AnalyzerAction<L>> for CodeSuggestion
where
L: Language,
{
impl<'a, L: Language> From<AnalyzerAction<L>> for CodeSuggestionItem<'a> {
fn from(action: AnalyzerAction<L>) -> Self {
let (range, suggestion) = action.mutation.as_text_edits().unwrap_or_default();

CodeSuggestion {
span: FileSpan {
file: action.file_id,
range,
CodeSuggestionItem {
rule_name: action.rule_name,
category: action.category,
suggestion: CodeSuggestion {
span: FileSpan {
file: action.file_id,
range,
},
applicability: action.applicability,
msg: action.message,
suggestion,
labels: vec![],
},
applicability: action.applicability,
msg: action.message,
suggestion,
labels: vec![],
}
}
}

impl<L: Language> AnalyzerActionIter<L> {
pub fn new(actions: Vec<AnalyzerAction<L>>) -> Self {
Self {
analyzer_actions: actions.into_iter(),
}
}
}

impl<L: Language> Iterator for AnalyzerActionIter<L> {
type Item = AnalyzerAction<L>;

fn next(&mut self) -> Option<Self::Item> {
self.analyzer_actions.next()
}
}

impl<L: Language> FusedIterator for AnalyzerActionIter<L> {}

impl<L: Language> ExactSizeIterator for AnalyzerActionIter<L> {
fn len(&self) -> usize {
self.analyzer_actions.len()
}
}

#[derive(Debug)]
pub struct AnalyzerMutation<L: Language> {
pub message: MarkupBuf,
pub mutation: BatchMutation<L>,
pub category: ActionCategory,
pub rule_name: String,
}

pub struct CodeSuggestionAdviceIter<L: Language> {
iter: IntoIter<AnalyzerAction<L>>,
}

impl<L: Language> Iterator for CodeSuggestionAdviceIter<L> {
type Item = CodeSuggestionAdvice<MarkupBuf>;

fn next(&mut self) -> Option<Self::Item> {
let action = self.iter.next()?;
Some(action.into())
}
}

impl<L: Language> FusedIterator for CodeSuggestionAdviceIter<L> {}

impl<L: Language> ExactSizeIterator for CodeSuggestionAdviceIter<L> {
fn len(&self) -> usize {
self.iter.len()
}
}

pub struct CodeActionIter<L: Language> {
iter: IntoIter<AnalyzerAction<L>>,
}

pub struct CodeSuggestionItem<'a> {
pub category: ActionCategory,
pub suggestion: CodeSuggestion,
pub rule_name: &'a str,
}

impl<L: Language> Iterator for CodeActionIter<L> {
type Item = CodeSuggestionItem<'static>;

fn next(&mut self) -> Option<Self::Item> {
let action = self.iter.next()?;
Some(action.into())
}
}

impl<L: Language> FusedIterator for CodeActionIter<L> {}

impl<L: Language> ExactSizeIterator for CodeActionIter<L> {
fn len(&self) -> usize {
self.iter.len()
}
}

impl<L: Language> AnalyzerActionIter<L> {
/// Returns an iterator that yields [CodeSuggestionAdvice]
pub fn into_code_suggestion_advices(self) -> CodeSuggestionAdviceIter<L> {
CodeSuggestionAdviceIter {
iter: self.analyzer_actions,
}
}

/// Returns an iterator that yields [CodeAction]
pub fn into_code_action_iter(self) -> CodeActionIter<L> {
CodeActionIter {
iter: self.analyzer_actions,
}
}
}
Expand Down Expand Up @@ -141,18 +247,78 @@ where
R::diagnostic(&ctx, &self.state).map(|diag| diag.into_analyzer_diagnostic(self.file_id))
}

fn action(&self) -> Option<AnalyzerAction<RuleLanguage<R>>> {
fn actions(&self) -> Option<AnalyzerActionIter<RuleLanguage<R>>> {
let ctx =
RuleContext::new(&self.query_result, self.root, self.services, &self.options).ok()?;
let mut actions = Vec::new();
if let Some(action) = R::action(&ctx, &self.state) {
actions.push(AnalyzerAction {
group_name: <R::Group as RuleGroup>::NAME,
rule_name: R::METADATA.name,
file_id: self.file_id,
category: action.category,
applicability: action.applicability,
mutation: action.mutation,
message: action.message,
});
};
let node_to_suppress = R::can_suppress(&ctx, &self.state);
let suppression_node = node_to_suppress.and_then(|suppression_node| {
let ancestor = suppression_node.node().ancestors().find_map(|node| {
if node
.first_token()
.map(|token| {
token
.leading_trivia()
.pieces()
.any(|trivia| trivia.is_newline())
})
.unwrap_or(false)
{
Some(node)
} else {
None
}
});
if ancestor.is_some() {
ancestor
} else {
Some(ctx.root().syntax().clone())
}
});
let suppression_action = suppression_node.and_then(|suppression_node| {
let first_token = suppression_node.first_token();
let rule = format!(
"lint({}/{})",
<R::Group as RuleGroup>::NAME,
R::METADATA.name
);
let mes = format!("// rome-ignore {}: suppressed", rule);

R::action(&ctx, &self.state).map(|action| AnalyzerAction {
group_name: <R::Group as RuleGroup>::NAME,
rule_name: R::METADATA.name,
file_id: self.file_id,
category: action.category,
applicability: action.applicability,
message: action.message,
mutation: action.mutation,
})
first_token.map(|first_token| {
let trivia = vec![
(TriviaPieceKind::Newline, "\n"),
(TriviaPieceKind::SingleLineComment, mes.as_str()),
(TriviaPieceKind::Newline, "\n"),
];
let mut mutation = ctx.root().begin();
let new_token = first_token.with_leading_trivia(trivia.clone());

mutation.replace_token_discard_trivia(first_token, new_token);
AnalyzerAction {
group_name: <R::Group as RuleGroup>::NAME,
rule_name: R::METADATA.name,
file_id: self.file_id,
category: ActionCategory::Other(Cow::Borrowed("quickfix.rome.suppressRule")),
applicability: Applicability::Always,
mutation,
message: markup! { "Suppress rule " {rule} }.to_owned(),
}
})
});
if let Some(suppression_action) = suppression_action {
actions.push(suppression_action);
}
Some(AnalyzerActionIter::new(actions))
}
}
2 changes: 1 addition & 1 deletion crates/rome_diagnostics/src/suggestion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use rome_rowan::TextRange;
use rome_text_edit::TextEdit;
use serde::{Deserialize, Serialize};

/// A Suggestion that is provided by rslint, and
/// A Suggestion that is provided by Rome's linter, and
/// can be reported to the user, and can be automatically
/// applied if it has the right [`Applicability`].
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
Expand Down
5 changes: 5 additions & 0 deletions crates/rome_js_analyze/src/analyzers/a11y/use_alt_text.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::JsSuppressAction;
use rome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic};
use rome_console::markup;
use rome_js_syntax::{
Expand Down Expand Up @@ -142,6 +143,10 @@ impl Rule for UseAltText {
}),
)
}

fn can_suppress(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<JsSuppressAction> {
Some(ctx.query().syntax().clone().into())
}
}

/// This function checks for the attribute `type` for input element where we checking for the input type which is image.
Expand Down
Loading

0 comments on commit eef75b3

Please sign in to comment.