use std::{borrow::Cow, error::Error, fmt, iter::once, path::Path, string::ToString};
use lsp_types::DiagnosticTag;
use miette::{Diagnostic, LabeledSpan, Severity};
use nu_protocol::Span;
use crate::span::{FileSpan, LintSpan};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SourceFile {
Stdin,
File(String),
}
impl SourceFile {
#[must_use]
pub const fn as_str(&self) -> &str {
match self {
Self::Stdin => "<stdin>",
Self::File(path) => path.as_str(),
}
}
#[must_use]
pub fn as_path(&self) -> Option<&Path> {
match self {
Self::Stdin => None,
Self::File(path) => Some(Path::new(path)),
}
}
#[must_use]
pub const fn is_stdin(&self) -> bool {
matches!(self, Self::Stdin)
}
}
impl fmt::Display for SourceFile {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl From<&str> for SourceFile {
fn from(s: &str) -> Self {
Self::File(s.to_string())
}
}
impl From<String> for SourceFile {
fn from(s: String) -> Self {
Self::File(s)
}
}
impl From<&Path> for SourceFile {
fn from(p: &Path) -> Self {
Self::File(p.to_string_lossy().to_string())
}
}
#[derive(Debug, Clone)]
pub struct ExternalDetection {
pub file: String,
pub source: String,
pub span: FileSpan,
pub message: String,
pub label: Option<String>,
}
impl ExternalDetection {
#[must_use]
pub fn new(
file: impl Into<String>,
source: impl Into<String>,
span: FileSpan,
message: impl Into<String>,
) -> Self {
Self {
file: file.into(),
source: source.into(),
span,
message: message.into(),
label: None,
}
}
#[must_use]
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
}
impl fmt::Display for ExternalDetection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl Error for ExternalDetection {}
impl Diagnostic for ExternalDetection {
fn severity(&self) -> Option<Severity> {
Some(Severity::Advice)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
let label = LabeledSpan::at(
self.span.start..self.span.end,
self.label.as_deref().unwrap_or("here"),
);
Some(Box::new(once(label)))
}
}
#[derive(Debug, Clone)]
pub struct Detection {
pub message: Cow<'static, str>,
pub span: LintSpan,
pub primary_label: Option<Cow<'static, str>>,
pub extra_labels: Vec<(LintSpan, Option<String>)>,
pub external_detections: Vec<ExternalDetection>,
}
impl Detection {
#[must_use]
pub fn from_global_span(message: impl Into<Cow<'static, str>>, global_span: Span) -> Self {
Self {
message: message.into(),
span: LintSpan::from(global_span),
primary_label: None,
extra_labels: Vec::new(),
external_detections: Vec::new(),
}
}
#[must_use]
pub fn from_file_span(message: impl Into<Cow<'static, str>>, span: FileSpan) -> Self {
Self {
message: message.into(),
span: LintSpan::File(span),
primary_label: None,
extra_labels: Vec::new(),
external_detections: Vec::new(),
}
}
#[must_use]
pub fn with_external_detection(mut self, detection: ExternalDetection) -> Self {
self.external_detections.push(detection);
self
}
#[must_use]
pub fn with_primary_label(mut self, label: impl Into<Cow<'static, str>>) -> Self {
self.primary_label = Some(label.into());
self
}
#[must_use]
pub fn with_extra_label(mut self, label: impl Into<Cow<'static, str>>, span: Span) -> Self {
self.extra_labels
.push((LintSpan::from(span), Some(label.into().to_string())));
self
}
#[must_use]
pub fn with_extra_span(mut self, span: Span) -> Self {
self.extra_labels.push((LintSpan::from(span), None));
self
}
}
#[derive(Debug, Clone)]
pub struct Violation {
pub rule_id: Option<Cow<'static, str>>,
pub lint_level: Severity,
pub message: Cow<'static, str>,
pub span: LintSpan,
pub primary_label: Option<Cow<'static, str>>,
pub extra_labels: Vec<(LintSpan, Option<String>)>,
pub long_description: Option<String>,
pub fix: Option<Fix>,
pub(crate) file: Option<SourceFile>,
pub(crate) source: Option<Cow<'static, str>>,
pub doc_url: Option<&'static str>,
pub short_description: Option<&'static str>,
pub diagnostic_tags: Vec<DiagnosticTag>,
pub external_detections: Vec<ExternalDetection>,
}
impl Violation {
pub(crate) fn from_detected(
detected: Detection,
fix: Option<Fix>,
long_description: impl Into<Option<&'static str>>,
) -> Self {
Self {
rule_id: None,
lint_level: Severity::default(),
message: detected.message,
span: detected.span,
primary_label: detected.primary_label,
extra_labels: detected.extra_labels,
long_description: long_description.into().map(ToString::to_string),
fix,
file: None,
source: None,
doc_url: None,
short_description: None,
diagnostic_tags: Vec::new(),
external_detections: detected.external_detections,
}
}
pub(crate) fn set_rule_id(&mut self, rule_id: &'static str) {
self.rule_id = Some(Cow::Borrowed(rule_id));
}
pub(crate) const fn set_lint_level(&mut self, level: Severity) {
self.lint_level = level;
}
pub(crate) const fn set_doc_url(&mut self, url: Option<&'static str>) {
self.doc_url = url;
}
pub(crate) const fn set_short_description(&mut self, desc: &'static str) {
self.short_description = Some(desc);
}
pub(crate) fn set_diagnostic_tags(&mut self, tags: &[DiagnosticTag]) {
self.diagnostic_tags = tags.to_vec();
}
#[must_use]
pub fn file_span(&self) -> FileSpan {
self.span.file_span()
}
pub fn normalize_spans(&mut self, file_offset: usize) {
let file_span = self.span.to_file_span(file_offset);
self.span = LintSpan::File(file_span);
if let Some(fix) = &mut self.fix {
for replacement in &mut fix.replacements {
let file_span = replacement.span.to_file_span(file_offset);
replacement.span = LintSpan::File(file_span);
}
}
self.extra_labels = self
.extra_labels
.iter()
.map(|(span, label)| {
let file_span = span.to_file_span(file_offset);
(LintSpan::File(file_span), label.clone())
})
.collect();
}
}
impl fmt::Display for Violation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl Error for Violation {}
impl Diagnostic for Violation {
fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
Some(Box::new(format!(
"{:?}({})",
self.lint_level,
self.rule_id.as_deref().unwrap_or("unknown")
)))
}
fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
self.long_description
.as_ref()
.map(|h| Box::new(h.clone()) as Box<dyn fmt::Display>)
}
fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
self.doc_url
.map(|url| Box::new(url) as Box<dyn fmt::Display>)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
let file_span = self.file_span();
let span_range = file_span.start..file_span.end;
let primary = self.primary_label.as_ref().map_or_else(
|| LabeledSpan::underline(span_range.clone()),
|label| LabeledSpan::new_primary_with_span(Some(label.to_string()), span_range.clone()),
);
let extras = self.extra_labels.iter().map(|(span, label)| {
let file_span = span.file_span();
LabeledSpan::new_with_span(label.clone(), file_span.start..file_span.end)
});
Some(Box::new(once(primary).chain(extras)))
}
}
#[derive(Debug, Clone)]
pub struct Fix {
pub explanation: Cow<'static, str>,
pub replacements: Vec<Replacement>,
}
#[derive(Debug, Clone)]
pub struct Replacement {
pub span: LintSpan,
pub replacement_text: Cow<'static, str>,
}
impl Replacement {
#[must_use]
pub fn new(span: Span, replacement_text: impl Into<Cow<'static, str>>) -> Self {
Self {
span: LintSpan::from(span),
replacement_text: replacement_text.into(),
}
}
#[must_use]
pub fn with_file_span(span: FileSpan, replacement_text: impl Into<Cow<'static, str>>) -> Self {
Self {
span: LintSpan::File(span),
replacement_text: replacement_text.into(),
}
}
#[must_use]
pub fn file_span(&self) -> FileSpan {
match self.span {
LintSpan::File(f) => f,
LintSpan::Global(_) => panic!("Span not normalized - call normalize_spans first"),
}
}
}