immigrant_schema/
diagnostics.rs

1#[cfg(feature = "hi-doc")]
2use hi_doc::{Formatting, SnippetBuilder, Text};
3#[cfg(feature = "tree-sitter-highlight")]
4use tree_sitter_highlight::HighlightConfiguration;
5
6use crate::span::SimpleSpan;
7
8#[derive(PartialEq, Eq, Clone, Copy)]
9pub enum Severity {
10	Warning,
11	Error,
12}
13
14#[derive(Clone)]
15pub struct ReportPart {
16	pub msg: String,
17	pub severity: Severity,
18	pub annotations: Vec<Annotation>,
19}
20
21#[derive(Clone)]
22pub struct Annotation {
23	pub span: SimpleSpan,
24	pub msg: String,
25}
26
27#[derive(Default, Clone)]
28pub struct Report {
29	pub parts: Vec<ReportPart>,
30}
31
32#[cfg(feature = "tree-sitter-highlight")]
33fn highlight(b: &mut SnippetBuilder) {
34	let language = tree_sitter_immigrant::LANGUAGE;
35	let mut config = HighlightConfiguration::new(
36		language.into(),
37		"immigrant",
38		tree_sitter_immigrant::HIGHLIGHTS_QUERY,
39		tree_sitter_immigrant::INJECTIONS_QUERY,
40		tree_sitter_immigrant::LOCALS_QUERY,
41	)
42	.expect("highlight configuration is valid");
43	config.configure(&[
44		"punctuation.bracket",
45		"keyword",
46		"property",
47		"type",
48	]);
49	b.highlight(config, |name, _str| {
50		match name {
51			1 => Formatting::rgb([255, 50, 50]),
52			2 => Formatting::rgb([50, 150, 50]),
53			3 => Formatting::rgb([120, 150, 50]),
54			_ => Formatting::listchar(),
55		}
56	});
57}
58
59impl Report {
60	pub fn new() -> Self {
61		Self::default()
62	}
63	#[cfg(feature = "hi-doc")]
64	pub fn to_hi_doc(self, src: &str) -> Vec<hi_doc::Source> {
65		let mut out = Vec::new();
66
67		let mut snippet_src = src.to_owned();
68		// For missing something at the end of input - create real character to point at
69		snippet_src.push(' ');
70
71		for part in self.parts {
72			let mut builder = SnippetBuilder::new(&snippet_src);
73			#[cfg(feature = "tree-sitter-highlight")]
74			highlight(&mut builder);
75			// TODO: other severity
76			for ele in part.annotations {
77				let mut ann = builder.error(Text::fragment(
78					format!("{}: {}", part.msg, ele.msg),
79					Formatting::rgb([127, 127, 255]),
80				));
81				if ele.span.start == ele.span.end {
82					// FIXME: They shouldn't be inclusive by default
83					ann = ann.range(ele.span.start as usize..=ele.span.end as usize);
84				} else {
85					ann = ann.range(ele.span.start as usize..=ele.span.end as usize - 1);
86				}
87				ann.build();
88			}
89			out.push(builder.build())
90		}
91		out
92	}
93	pub fn is_error(&self) -> bool {
94		self.parts.iter().any(|p| p.severity == Severity::Error)
95	}
96
97	pub fn error(&mut self, msg: impl AsRef<str>) -> PartBuilder<'_> {
98		let part = ReportPart {
99			msg: msg.as_ref().to_owned(),
100			severity: Severity::Error,
101			annotations: vec![],
102		};
103		self.parts.push(part);
104		PartBuilder {
105			part: self.parts.last_mut().expect("just inserted"),
106		}
107	}
108}
109
110pub struct PartBuilder<'r> {
111	part: &'r mut ReportPart,
112}
113impl PartBuilder<'_> {
114	pub fn annotate(&mut self, msg: impl AsRef<str>, span: SimpleSpan) -> &mut Self {
115		self.part.annotations.push(Annotation {
116			span,
117			msg: msg.as_ref().to_owned(),
118		});
119		self
120	}
121}
122
123#[test]
124#[cfg(feature = "hi-doc")]
125fn diagnostics() {
126	use crate::process::NamingConvention;
127	use crate::root::SchemaProcessOptions;
128	use crate::uid::RenameMap;
129
130	let mut rn = RenameMap::new();
131
132	let mut report = Report::new();
133
134	let src = r#"
135			scalar idd = "INTEGER";
136			table A {
137				idd;
138			};
139			table A {
140				idd;
141			};
142		"#;
143	crate::parser::parse(
144		src,
145		false,
146		&SchemaProcessOptions {
147			generator_supports_domain: true,
148			naming_convention: NamingConvention::Postgres,
149		},
150		&mut rn,
151		&mut report,
152	)
153	.expect("parsed");
154
155	assert!(report.is_error());
156	let hidoc = report.to_hi_doc(src);
157	for hidoc in hidoc {
158		let ansi = hi_doc::source_to_ansi(&hidoc);
159		println!("{ansi}")
160	}
161}