increparse_lsp/diagnostics.rs
1//! Turning failed parse regions into publishable LSP diagnostics.
2
3use increparse::{NodeId, Span, Status};
4use lsp_types::Diagnostic;
5
6use crate::Document;
7
8/// One parse-tree node offered to the user's diagnostic hook.
9#[derive(Debug, Clone, Copy)]
10pub struct FailedNode<'a, C> {
11 /// The node's id (usable for further tree queries).
12 pub id: NodeId,
13 /// The node's parse context.
14 pub ctx: &'a C,
15 /// Why the node is a candidate: [`Status::Failed`], or
16 /// [`Status::Unparsed`] when the schedule ran out with work left.
17 pub status: Status,
18 /// The node's region.
19 pub span: Span,
20}
21
22/// Options for [`diagnostics`].
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub struct DiagnosticsOptions {
25 /// When `false` (the default), a node whose *ancestor* is already a
26 /// failure is skipped — one broken function yields one diagnostic, not
27 /// one per unparsed statement inside it. When `true`, every failing
28 /// node is reported.
29 pub cascades: bool,
30}
31
32/// Builds diagnostics from a settled (or cancelled, or round-capped) parse.
33///
34/// The tree is walked from the root; every failing node is offered to `hook`
35/// (unless its ancestor already failed and `options.cascades` is `false`).
36/// The hook turns a [`FailedNode`] into a `Diagnostic` — or `None` to stay
37/// silent, e.g. for regions whose context type makes the failure expected.
38///
39/// Diagnostics returned with the default (zero) range get their range filled
40/// in from the node's span, converted with the document's position encoding;
41/// hooks that compute their own ranges keep them.
42///
43/// # Examples
44///
45/// ```
46/// use increparse::{Engine, Outcome, Pass, Schedule, SerialExecutor, Span, Status};
47/// use increparse_lsp::{diagnostics, DiagnosticsOptions, Document, PositionEncoding};
48/// use lsp_types::{Diagnostic, DiagnosticSeverity, Position, Uri};
49///
50/// #[derive(Clone, Debug, PartialEq, Eq)]
51/// enum Ctx { File, Bad }
52///
53/// struct Split;
54/// impl Pass for Split {
55/// type Ctx = Ctx;
56/// fn parse(&self, source: &str, span: Span, ctx: &Ctx) -> Outcome<Ctx> {
57/// match ctx {
58/// Ctx::File if span.len() >= 2 => Outcome::Expand(vec![(
59/// Span::new(span.start + 1, span.end, span.rev),
60/// Ctx::Bad,
61/// )]),
62/// Ctx::File => Outcome::Done,
63/// Ctx::Bad if source[span.to_range()].contains('!') => Outcome::Failed,
64/// Ctx::Bad => Outcome::Done,
65/// }
66/// }
67/// }
68///
69/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
70/// let mut schedule = Schedule::new();
71/// schedule.push(Split);
72/// schedule.push(Split);
73/// let engine = Engine::new(schedule);
74///
75/// let uri: Uri = "file:///x.txt".parse()?;
76/// let mut doc = Document::open(uri, 1, "".into(), "ok!".into(), PositionEncoding::Utf16, Ctx::File);
77/// doc.apply_changes(&engine, 1, &[], &SerialExecutor, &increparse::CancelToken::new());
78///
79/// let diags = diagnostics(&doc, DiagnosticsOptions::default(), |node| {
80/// let _ = node.status;
81/// Some(Diagnostic {
82/// severity: Some(DiagnosticSeverity::ERROR),
83/// message: "this region failed to parse".into(),
84/// ..Diagnostic::default()
85/// })
86/// });
87///
88/// assert_eq!(diags.len(), 1);
89/// assert_eq!(diags[0].range.start, Position { line: 0, character: 1 });
90/// # Ok(())
91/// # }
92/// ```
93pub fn diagnostics<C, F>(
94 doc: &Document<C>,
95 options: DiagnosticsOptions,
96 mut hook: F,
97) -> Vec<Diagnostic>
98where
99 C: Clone + PartialEq + Send + 'static,
100 F: FnMut(FailedNode<'_, C>) -> Option<Diagnostic>,
101{
102 let tree = doc.session().tree();
103 let mut out = Vec::new();
104 walk(doc, tree.root(), false, options, &mut hook, &mut out);
105 out
106}
107
108fn walk<C, F>(
109 doc: &Document<C>,
110 id: increparse::NodeId,
111 under_failure: bool,
112 options: DiagnosticsOptions,
113 hook: &mut F,
114 out: &mut Vec<Diagnostic>,
115) where
116 C: Clone + PartialEq + Send + 'static,
117 F: FnMut(FailedNode<'_, C>) -> Option<Diagnostic>,
118{
119 let tree = doc.session().tree();
120 let status = tree.status(id);
121 let failing = matches!(status, Status::Failed | Status::Unparsed);
122
123 if failing && (!under_failure || options.cascades) {
124 let span = tree.span(id);
125 let node = FailedNode {
126 id,
127 ctx: tree.ctx(id),
128 status,
129 span,
130 };
131 if let Some(mut diagnostic) = hook(node) {
132 if diagnostic.range == Diagnostic::default().range {
133 diagnostic.range = doc.range(span);
134 }
135 out.push(diagnostic);
136 }
137 }
138
139 let child_under_failure = under_failure || (failing && !options.cascades);
140 for child in tree.children(id) {
141 walk(doc, *child, child_under_failure, options, hook, out);
142 }
143}