xidl_parser/lint/
check.rs1use std::ops::Range;
2
3use crate::typed_ast::{AnnotationAppl, AnnotationName};
4use serde::Serialize;
5
6use super::{AnnotationDefinition, BUILTIN_ANNOTATIONS, Site};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
10#[serde(rename_all = "kebab-case")]
11pub enum DiagnosticCode {
12 MisspelledAnnotation,
14 MisplacedAnnotation,
16 BadAnnotationArgs,
18 UnknownAnnotation,
20}
21
22impl DiagnosticCode {
23 pub fn as_str(self) -> &'static str {
25 match self {
26 Self::MisspelledAnnotation => "misspelled-annotation",
27 Self::MisplacedAnnotation => "misplaced-annotation",
28 Self::BadAnnotationArgs => "bad-annotation-args",
29 Self::UnknownAnnotation => "unknown-annotation",
30 }
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
36#[serde(rename_all = "lowercase")]
37pub enum Severity {
38 Error,
40 Warning,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
46pub struct Diagnostic {
47 pub code: DiagnosticCode,
49 pub severity: Severity,
51 pub span: Range<usize>,
53 pub message: String,
55 pub suggestion: Option<String>,
57}
58
59#[derive(Default)]
60pub(super) struct Checker {
61 diagnostics: Vec<Diagnostic>,
62}
63
64impl Checker {
65 pub(super) fn finish(mut self) -> Vec<Diagnostic> {
66 self.diagnostics
67 .sort_by_key(|diagnostic| diagnostic.span.start);
68 self.diagnostics
69 }
70
71 pub(super) fn annotations(&mut self, annotations: &[AnnotationAppl], site: Site) {
72 for annotation in annotations {
73 self.annotation(annotation, site);
74 self.annotations(&annotation.extra, site);
75 }
76 }
77
78 fn annotation(&mut self, annotation: &AnnotationAppl, site: Site) {
79 let Some(span) = annotation.span.clone() else {
80 return;
81 };
82 let name = match &annotation.name {
83 AnnotationName::Builtin(name) => name.clone(),
84 AnnotationName::ScopedName(name) => {
85 let name: crate::hir::ScopedName = name.clone().into();
86 format!(
87 "{}{}",
88 if name.is_root { "::" } else { "" },
89 name.name.join("::")
90 )
91 }
92 };
93 if AnnotationDefinition::is_passthrough(&name) {
94 return;
95 }
96 let Some(definition) = AnnotationDefinition::lookup(&name) else {
97 self.unknown(name, span);
98 return;
99 };
100 if !definition.allows(site) {
101 self.diagnostics.push(Diagnostic {
102 code: DiagnosticCode::MisplacedAnnotation,
103 severity: Severity::Error,
104 span: span.clone(),
105 message: format!("@{name} is not allowed on {site:?}"),
106 suggestion: None,
107 });
108 }
109 if let Err(reason) = definition.check_arguments(annotation, site) {
110 self.diagnostics.push(Diagnostic {
111 code: DiagnosticCode::BadAnnotationArgs,
112 severity: Severity::Error,
113 span,
114 message: format!("@{name}: {reason}"),
115 suggestion: None,
116 });
117 }
118 }
119
120 fn unknown(&mut self, name: String, span: Range<usize>) {
121 let lowered = name.to_ascii_lowercase();
122 let suggestion = BUILTIN_ANNOTATIONS
123 .iter()
124 .filter(|entry| entry.name.len().abs_diff(lowered.len()) <= 2)
125 .map(|entry| (strsim::levenshtein(&lowered, entry.name), entry.name))
126 .filter(|(distance, _)| *distance <= 2)
127 .min()
128 .map(|(_, name)| name.to_string());
129 let (code, severity) = if suggestion.is_some() {
130 (DiagnosticCode::MisspelledAnnotation, Severity::Error)
131 } else {
132 (DiagnosticCode::UnknownAnnotation, Severity::Warning)
133 };
134 self.diagnostics.push(Diagnostic {
135 code,
136 severity,
137 span,
138 message: format!("unknown annotation `@{name}`"),
139 suggestion,
140 });
141 }
142}