Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support diagnostics without ranges #1299

Merged
merged 4 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 92 additions & 48 deletions src/context.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

use crate::control_flow::ControlFlow;
use crate::diagnostic::{LintDiagnostic, LintFix};
use crate::diagnostic::{
LintDiagnostic, LintDiagnosticDetails, LintDiagnosticRange, LintFix,
};
use crate::ignore_directives::{
parse_line_ignore_directives, CodeStatus, FileIgnoreDirective,
LineIgnoreDirective,
Expand Down Expand Up @@ -234,18 +236,20 @@ impl<'a> Context<'a> {

for diagnostic in self.diagnostics.iter().cloned() {
if let Some(f) = self.file_ignore_directive.as_mut() {
if f.check_used(&diagnostic.code) {
if f.check_used(&diagnostic.details.code) {
continue;
}
}
let Some(range) = diagnostic.range.as_ref() else {
continue;
};

let diagnostic_line =
diagnostic.text_info.line_index(diagnostic.range.start);
let diagnostic_line = range.text_info.line_index(range.range.start);
if diagnostic_line > 0 {
if let Some(l) =
self.line_ignore_directives.get_mut(&(diagnostic_line - 1))
{
if l.check_used(&diagnostic.code) {
if l.check_used(&diagnostic.details.code) {
continue;
}
}
Expand Down Expand Up @@ -290,11 +294,13 @@ impl<'a> Context<'a> {
file_ignore.codes().iter().filter(is_unused_code)
{
let d = self.create_diagnostic(
file_ignore.range(),
CODE,
format!("Ignore for code \"{}\" was not used.", unused_code),
None,
Vec::new(),
Some(self.create_diagnostic_range(file_ignore.range())),
self.create_diagnostic_details(
CODE,
format!("Ignore for code \"{}\" was not used.", unused_code),
None,
Vec::new(),
),
);
diagnostics.push(d);
}
Expand All @@ -309,11 +315,13 @@ impl<'a> Context<'a> {
line_ignore.codes().iter().filter(is_unused_code)
{
let d = self.create_diagnostic(
line_ignore.range(),
CODE,
format!("Ignore for code \"{}\" was not used.", unused_code),
None,
Vec::new(),
Some(self.create_diagnostic_range(line_ignore.range())),
self.create_diagnostic_details(
CODE,
format!("Ignore for code \"{}\" was not used.", unused_code),
None,
Vec::new(),
),
);
diagnostics.push(d);
}
Expand All @@ -337,11 +345,13 @@ impl<'a> Context<'a> {
file_ignore.codes().keys().filter(is_unknown_rule)
{
let d = self.create_diagnostic(
file_ignore.range(),
rules::ban_unknown_rule_code::CODE,
format!("Unknown rule for code \"{}\"", unknown_rule_code),
None,
Vec::new(),
Some(self.create_diagnostic_range(file_ignore.range())),
self.create_diagnostic_details(
rules::ban_unknown_rule_code::CODE,
format!("Unknown rule for code \"{}\"", unknown_rule_code),
None,
Vec::new(),
),
);
diagnostics.push(d);
}
Expand All @@ -352,11 +362,13 @@ impl<'a> Context<'a> {
line_ignore.codes().keys().filter(is_unknown_rule)
{
let d = self.create_diagnostic(
line_ignore.range(),
rules::ban_unknown_rule_code::CODE,
format!("Unknown rule for code \"{}\"", unknown_rule_code),
None,
Vec::new(),
Some(self.create_diagnostic_range(line_ignore.range())),
self.create_diagnostic_details(
rules::ban_unknown_rule_code::CODE,
format!("Unknown rule for code \"{}\"", unknown_rule_code),
None,
Vec::new(),
),
);
diagnostics.push(d);
}
Expand Down Expand Up @@ -386,14 +398,15 @@ impl<'a> Context<'a> {
code: impl ToString,
message: impl ToString,
) {
let diagnostic = self.create_diagnostic(
range,
code,
message.to_string(),
None,
Vec::new(),
self.add_diagnostic_details(
Some(self.create_diagnostic_range(range)),
self.create_diagnostic_details(
code,
message.to_string(),
None,
Vec::new(),
),
);
self.diagnostics.push(diagnostic);
}

pub fn add_diagnostic_with_hint(
Expand All @@ -403,14 +416,15 @@ impl<'a> Context<'a> {
message: impl ToString,
hint: impl ToString,
) {
let diagnostic = self.create_diagnostic(
range,
code,
message,
Some(hint.to_string()),
Vec::new(),
self.add_diagnostic_details(
Some(self.create_diagnostic_range(range)),
self.create_diagnostic_details(
code,
message,
Some(hint.to_string()),
Vec::new(),
),
);
self.diagnostics.push(diagnostic);
}

pub fn add_diagnostic_with_fixes(
Expand All @@ -421,31 +435,61 @@ impl<'a> Context<'a> {
hint: Option<String>,
fixes: Vec<LintFix>,
) {
let diagnostic = self.create_diagnostic(range, code, message, hint, fixes);
self.diagnostics.push(diagnostic);
self.add_diagnostic_details(
Some(self.create_diagnostic_range(range)),
self.create_diagnostic_details(code, message, hint, fixes),
);
}

pub fn add_diagnostic_details(
&mut self,
maybe_range: Option<LintDiagnosticRange>,
details: LintDiagnosticDetails,
) {
self
.diagnostics
.push(self.create_diagnostic(maybe_range, details));
}

pub(crate) fn create_diagnostic(
&self,
range: SourceRange,
maybe_range: Option<LintDiagnosticRange>,
details: LintDiagnosticDetails,
) -> LintDiagnostic {
LintDiagnostic {
specifier: self.specifier().clone(),
range: maybe_range,
details,
}
}

pub(crate) fn create_diagnostic_details(
&self,
code: impl ToString,
message: impl ToString,
maybe_hint: Option<String>,
fixes: Vec<LintFix>,
) -> LintDiagnostic {
LintDiagnostic {
specifier: self.specifier().clone(),
range,
text_info: self.text_info().clone(),
) -> LintDiagnosticDetails {
LintDiagnosticDetails {
message: message.to_string(),
code: code.to_string(),
hint: maybe_hint,
fixes,
custom_docs_url: None,
range_description: None,
info: vec![],
}
}

pub(crate) fn create_diagnostic_range(
&self,
range: SourceRange,
) -> LintDiagnosticRange {
LintDiagnosticRange {
range,
text_info: self.text_info().clone(),
description: None,
}
}
}

/// A struct containing a boolean value to control whether a node's children
Expand Down
67 changes: 45 additions & 22 deletions src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,15 @@ pub struct LintFix {
}

#[derive(Clone)]
pub struct LintDiagnostic {
pub specifier: ModuleSpecifier,
pub range: SourceRange,
pub struct LintDiagnosticRange {
pub text_info: SourceTextInfo,
pub range: SourceRange,
/// Additional information displayed beside the highlighted range.
pub description: Option<String>,
}

#[derive(Clone)]
pub struct LintDiagnosticDetails {
pub message: String,
pub code: String,
pub hint: Option<String>,
Expand All @@ -44,67 +49,85 @@ pub struct LintDiagnostic {
/// URL to the lint rule documentation. By default, the url uses the
/// code to link to lint.deno.land
pub custom_docs_url: Option<String>,
/// Additional information displayed beside the highlighted range.
pub range_description: Option<String>,
/// Displays additional information at the end of a diagnostic.
pub info: Vec<Cow<'static, str>>,
}

#[derive(Clone)]
pub struct LintDiagnostic {
pub specifier: ModuleSpecifier,
/// Optional range within the file.
///
/// Diagnostics that don't have a range mean there's something wrong with
/// the whole file.
pub range: Option<LintDiagnosticRange>,
pub details: LintDiagnosticDetails,
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split this up so a context.add_diagnostic_details(...) could be added that doesn't require providing a specifier.


impl Diagnostic for LintDiagnostic {
fn level(&self) -> DiagnosticLevel {
DiagnosticLevel::Error
}

fn code(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.code)
Cow::Borrowed(&self.details.code)
}

fn message(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.message)
Cow::Borrowed(&self.details.message)
}

fn location(&self) -> DiagnosticLocation {
DiagnosticLocation::ModulePosition {
specifier: Cow::Borrowed(&self.specifier),
text_info: Cow::Borrowed(&self.text_info),
source_pos: DiagnosticSourcePos::SourcePos(self.range.start),
match &self.range {
Some(range) => DiagnosticLocation::ModulePosition {
specifier: Cow::Borrowed(&self.specifier),
text_info: Cow::Borrowed(&range.text_info),
source_pos: DiagnosticSourcePos::SourcePos(range.range.start),
},
None => DiagnosticLocation::Module {
specifier: Cow::Borrowed(&self.specifier),
},
}
}

fn snippet(&self) -> Option<DiagnosticSnippet<'_>> {
let range = DiagnosticSourceRange {
start: DiagnosticSourcePos::SourcePos(self.range.start),
end: DiagnosticSourcePos::SourcePos(self.range.end),
};
let range = self.range.as_ref()?;
Some(DiagnosticSnippet {
source: Cow::Borrowed(&self.text_info),
source: Cow::Borrowed(&range.text_info),
highlights: vec![DiagnosticSnippetHighlight {
range,
range: DiagnosticSourceRange {
start: DiagnosticSourcePos::SourcePos(range.range.start),
end: DiagnosticSourcePos::SourcePos(range.range.end),
},
style: DiagnosticSnippetHighlightStyle::Error,
description: self.range_description.as_deref().map(Cow::Borrowed),
description: range.description.as_deref().map(Cow::Borrowed),
}],
})
}

fn hint(&self) -> Option<Cow<'_, str>> {
self.hint.as_ref().map(|s| Cow::Borrowed(s.as_str()))
self
.details
.hint
.as_ref()
.map(|s| Cow::Borrowed(s.as_str()))
}

fn snippet_fixed(&self) -> Option<DiagnosticSnippet<'_>> {
None // todo
}

fn info(&self) -> Cow<'_, [std::borrow::Cow<'_, str>]> {
Cow::Borrowed(&self.info)
Cow::Borrowed(&self.details.info)
}

fn docs_url(&self) -> Option<Cow<'_, str>> {
if let Some(custom_docs_url) = &self.custom_docs_url {
if let Some(custom_docs_url) = &self.details.custom_docs_url {
Some(Cow::Borrowed(custom_docs_url))
} else {
Some(Cow::Owned(format!(
"https://lint.deno.land/rules/{}",
&self.code
&self.details.code
)))
}
}
Expand Down
12 changes: 10 additions & 2 deletions src/linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::diagnostic::LintDiagnostic;
use crate::ignore_directives::parse_file_ignore_directives;
use crate::performance_mark::PerformanceMark;
use crate::rules::{ban_unknown_rule_code::BanUnknownRuleCode, LintRule};
use deno_ast::diagnostics::Diagnostic;
use deno_ast::MediaType;
use deno_ast::ParsedSource;
use deno_ast::{ModuleSpecifier, ParseDiagnostic};
Expand Down Expand Up @@ -140,8 +141,15 @@ impl Linter {
// Run `ban-unused-ignore`
diagnostics.extend(context.ban_unused_ignore(&self.ctx.rules));

// Finally sort by position the diagnostics originates on
diagnostics.sort_by_key(|d| d.range.start);
// Finally sort by position the diagnostics originates on then by code
diagnostics.sort_by(|a, b| {
let a_range = a.range.as_ref().map(|r| r.range.start);
let b_range = b.range.as_ref().map(|r| r.range.start);
match a_range.cmp(&b_range) {
std::cmp::Ordering::Equal => a.code().cmp(&b.code()),
cmp => cmp,
}
});

diagnostics
}
Expand Down
Loading
Loading