Skip to main content

jlens_schema/
service.rs

1//! The language service: completion, diagnostics, hover and signature help for JSONata
2//! expressions, driven by a [`Schema`].
3//!
4//! Offsets and spans are byte offsets into the expression text. Editors that count in UTF-16
5//! (browsers) convert at the boundary; the Wasm package does that.
6
7use jlens_core::ast::{Node, Span};
8use jlens_core::signature::Signature;
9use jlens_core::{Diagnostic, Severity, parse_recover};
10use serde::{Deserialize, Serialize};
11
12use crate::shape::{Field, Schema, Shape};
13use crate::typing::{Event, Scope, Symbols, Ty, Walker};
14
15/// The identifier substituted for the word under the cursor before parsing.
16const PLACEHOLDER: &str = "__jlens_cursor__";
17
18const OPERATOR_CHARS: &str = ".[]{}(),@#;:?+-*/%|=<>^&!~\"'`";
19
20/// What a completion item inserts.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
22#[serde(rename_all = "lowercase")]
23pub enum CompletionKind {
24    /// A field of the context value.
25    Field,
26    /// A variable in scope.
27    Variable,
28    /// A built-in function, or a host function declared with
29    /// [`LanguageService::with_function`].
30    Function,
31    /// `true`, `false`, `null`.
32    Keyword,
33    /// An operator (`and`, `or`, `in`, ...).
34    Operator,
35}
36
37/// One completion suggestion.
38#[derive(Debug, Clone, Serialize)]
39#[serde(rename_all = "camelCase")]
40pub struct CompletionItem {
41    /// The text shown in the list.
42    pub label: String,
43    /// What is suggested.
44    pub kind: CompletionKind,
45    /// A short description: the type of a field or variable, the signature of a function.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub detail: Option<String>,
48    /// Longer documentation (Markdown).
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub documentation: Option<String>,
51    /// The text to insert in place of the replaced range.
52    pub insert_text: String,
53}
54
55/// Completion results.
56#[derive(Debug, Clone, Serialize)]
57pub struct Completions {
58    /// The byte range the chosen item replaces (the word under the cursor).
59    pub range: Span,
60    /// The suggestions, best first.
61    pub items: Vec<CompletionItem>,
62}
63
64/// Hover information.
65#[derive(Debug, Clone, Serialize)]
66pub struct Hover {
67    /// The byte range the information is about.
68    pub range: Span,
69    /// Markdown text.
70    pub contents: String,
71}
72
73/// Help for the function call around the cursor.
74#[derive(Debug, Clone, Serialize)]
75#[serde(rename_all = "camelCase")]
76pub struct SignatureHelp {
77    /// The function name, with `$`.
78    pub name: String,
79    /// The signature, such as `<s-nn?:s>`.
80    pub signature: String,
81    /// Parameter names, with `$`.
82    pub params: Vec<String>,
83    /// The parameter being edited (0-based).
84    pub active_param: usize,
85    /// What the function does.
86    pub summary: String,
87}
88
89/// A function the host application provides (a `jlens_core::HostFunction`), declared to the
90/// language service so that it is completed and documented like a built-in function.
91///
92/// It deserializes from `{"name", "signature", "params", "summary"}`; all but `name` are
93/// optional.
94#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
95#[serde(default, deny_unknown_fields)]
96pub struct FunctionDecl {
97    /// The name, without the leading `$`.
98    pub name: String,
99    /// The signature, such as `<n-n?:n>`.
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub signature: Option<String>,
102    /// Parameter names, without `$`, shown by signature help.
103    pub params: Vec<String>,
104    /// One-line description.
105    pub summary: String,
106}
107
108impl FunctionDecl {
109    /// A declaration with a name and a summary.
110    pub fn new(name: impl Into<String>, summary: impl Into<String>) -> Self {
111        FunctionDecl {
112            name: name.into(),
113            summary: summary.into(),
114            ..Self::default()
115        }
116    }
117
118    /// Sets the signature.
119    pub fn signature(mut self, signature: impl Into<String>) -> Self {
120        self.signature = Some(signature.into());
121        self
122    }
123
124    /// Sets the parameter names (without `$`).
125    pub fn params<S: Into<String>>(mut self, params: impl IntoIterator<Item = S>) -> Self {
126        self.params = params.into_iter().map(Into::into).collect();
127        self
128    }
129}
130
131/// A function as the service presents it: declared or built-in.
132struct FunctionView {
133    name: String,
134    signature: String,
135    params: Vec<String>,
136    summary: String,
137}
138
139/// A call found by the walk, checked once every binding is known.
140struct CallSite {
141    /// The span of the `$name`.
142    span: Span,
143    name: String,
144    /// The number of arguments, the value applied with `~>` included.
145    count: usize,
146    /// The span and the types of each argument; `None` for a partial application.
147    args: Option<Vec<(Span, Option<Symbols>)>>,
148}
149
150/// Completion, diagnostics, hover and signature help for expressions over a schema.
151///
152/// ```
153/// use jlens_schema::{LanguageService, Schema};
154/// use serde_json::json;
155///
156/// let schema = Schema::infer(&[json!({"items": [{"type": "x", "price": 1}]})]);
157/// let service = LanguageService::new(Some(schema));
158/// let text = "items[type = 'x'].";
159/// let completions = service.complete(text, text.len());
160/// let labels: Vec<_> = completions.items.iter().map(|i| i.label.as_str()).collect();
161/// assert_eq!(labels, ["type", "price"]);
162/// ```
163#[derive(Debug, Clone, Default)]
164pub struct LanguageService {
165    schema: Option<Schema>,
166    variables: Vec<(String, Option<Shape>)>,
167    functions: Vec<FunctionDecl>,
168}
169
170/// Lexical situation at an offset.
171#[derive(Debug, PartialEq)]
172enum Lexical {
173    Code,
174    /// Inside a string literal or a comment.
175    Literal,
176    /// Inside a backquoted name that starts at this offset.
177    Backquote(usize),
178}
179
180fn lexical_state(text: &str, offset: usize) -> Lexical {
181    let bytes = text.as_bytes();
182    let mut i = 0;
183    while i < offset.min(bytes.len()) {
184        match bytes[i] {
185            q @ (b'"' | b'\'') => {
186                i += 1;
187                while i < bytes.len() && bytes[i] != q {
188                    if bytes[i] == b'\\' {
189                        i += 1;
190                    }
191                    i += 1;
192                }
193                if i >= offset {
194                    return Lexical::Literal;
195                }
196            }
197            b'`' => {
198                let start = i;
199                i += 1;
200                while i < bytes.len() && bytes[i] != b'`' {
201                    i += 1;
202                }
203                if i >= offset {
204                    return Lexical::Backquote(start);
205                }
206            }
207            b'/' if bytes.get(i + 1) == Some(&b'*') => match text[i + 2..].find("*/") {
208                Some(end) if i + 2 + end + 2 <= offset => i += 2 + end + 1,
209                _ => return Lexical::Literal,
210            },
211            _ => {}
212        }
213        i += 1;
214    }
215    Lexical::Code
216}
217
218fn is_word_char(c: char) -> bool {
219    !c.is_whitespace() && !OPERATOR_CHARS.contains(c)
220}
221
222/// Inserts a field name, quoting it with backquotes when it is not a plain name.
223fn field_insert(name: &str) -> String {
224    let plain = !name.is_empty()
225        && name.chars().all(is_word_char)
226        && !name.starts_with(|c: char| c.is_ascii_digit() || c == '$')
227        && !matches!(name, "and" | "or" | "in" | "true" | "false" | "null");
228    if plain {
229        name.to_owned()
230    } else {
231        format!("`{name}`")
232    }
233}
234
235fn shape_docs(shape: &Shape, field: Option<&Field>) -> Option<String> {
236    let mut parts = Vec::new();
237    if let Some(label) = &shape.label {
238        parts.push(format!("**{label}**"));
239    }
240    if let Some(d) = &shape.description {
241        parts.push(d.clone());
242    }
243    if let Some(f) = field
244        && !f.required
245        && f.presence > 0.0
246    {
247        parts.push(format!(
248            "Optional: present in {:.0}% of the samples.",
249            f.presence * 100.0
250        ));
251    }
252    if let Some(format) = &shape.format {
253        parts.push(format!("Format: `{format}`"));
254    }
255    if let Some(values) = &shape.enumeration {
256        let v: Vec<String> = values.iter().map(|v| format!("`{v}`")).collect();
257        parts.push(format!("Values: {}", v.join(", ")));
258    } else if !shape.examples.is_empty() {
259        let v: Vec<String> = shape.examples.iter().map(|v| format!("`{v}`")).collect();
260        parts.push(format!("Examples: {}", v.join(", ")));
261    }
262    (!parts.is_empty()).then(|| parts.join("\n\n"))
263}
264
265/// The result of walking an expression looking for one node.
266#[derive(Default)]
267struct Probe<'s> {
268    /// The context of the field reference, or the scope of the variable, found.
269    ctx: Option<Ty<'s>>,
270    scope: Scope<'s>,
271    found_variable: bool,
272}
273
274impl LanguageService {
275    /// A service for expressions over documents described by `schema` (or without a schema:
276    /// functions and variables are still completed).
277    pub fn new(schema: Option<Schema>) -> Self {
278        LanguageService {
279            schema,
280            variables: Vec::new(),
281            functions: Vec::new(),
282        }
283    }
284
285    /// Declares a variable the expression is evaluated with (a binding), optionally with the
286    /// shape of its value.
287    pub fn with_variable(mut self, name: impl Into<String>, shape: Option<Shape>) -> Self {
288        self.variables.push((name.into(), shape));
289        self
290    }
291
292    /// Declares a host function the expression is evaluated with. It is completed, and shown by
293    /// hover and signature help, like a built-in function, and replaces a built-in function of
294    /// the same name.
295    pub fn with_function(mut self, function: FunctionDecl) -> Self {
296        self.functions.retain(|f| f.name != function.name);
297        self.functions.push(function);
298        self
299    }
300
301    /// The declared functions, then the built-in functions they do not replace.
302    fn functions(&self) -> Vec<FunctionView> {
303        let declared = self.functions.iter().map(|f| FunctionView {
304            name: f.name.clone(),
305            signature: f.signature.clone().unwrap_or_default(),
306            params: f.params.clone(),
307            summary: f.summary.clone(),
308        });
309        let built_in = jlens_core::function_catalog()
310            .into_iter()
311            .filter(|b| self.functions.iter().all(|f| f.name != b.name))
312            .map(|b| FunctionView {
313                name: b.name.to_owned(),
314                signature: b.signature,
315                params: b.params.iter().map(|p| (*p).to_owned()).collect(),
316                summary: b.summary.to_owned(),
317            });
318        declared.chain(built_in).collect()
319    }
320
321    fn function(&self, name: &str) -> Option<FunctionView> {
322        self.functions().into_iter().find(|f| f.name == name)
323    }
324
325    fn root_ty(&self) -> Ty<'_> {
326        match &self.schema {
327            Some(s) => Ty::of(&s.root),
328            None => Ty::unknown(),
329        }
330    }
331
332    fn externals(&self) -> Vec<(String, Ty<'_>)> {
333        self.variables
334            .iter()
335            .map(|(n, s)| (n.clone(), s.as_ref().map_or_else(Ty::unknown, Ty::of)))
336            .collect()
337    }
338
339    /// Walks `root`, calling `visit` for each field reference and variable.
340    fn walk<'s>(&'s self, root: &Node, visit: &mut dyn FnMut(Event<'_, 's>)) {
341        // Declared functions are external names too: a call to one is not typed as a built-in.
342        let mut externals = self.externals();
343        externals.extend(
344            self.functions
345                .iter()
346                .map(|f| (f.name.clone(), Ty::unknown())),
347        );
348        let mut walker = Walker {
349            root: self.root_ty(),
350            externals: &externals,
351            visit,
352        };
353        let ctx = walker.root.clone();
354        walker.walk(root, &ctx, &mut Vec::new());
355    }
356
357    /// Suggestions for the word at `offset`.
358    pub fn complete(&self, text: &str, offset: usize) -> Completions {
359        let mut offset = offset.min(text.len());
360        while !text.is_char_boundary(offset) {
361            offset -= 1;
362        }
363        let empty = |range| Completions {
364            range,
365            items: Vec::new(),
366        };
367        match lexical_state(text, offset) {
368            Lexical::Literal => return empty(Span::new(offset, offset)),
369            Lexical::Backquote(start) => {
370                // Complete a quoted field name; the closing quote is replaced too.
371                let end = if text[offset..].starts_with('`') {
372                    offset + 1
373                } else {
374                    offset
375                };
376                let prefix = &text[start + 1..offset];
377                let modified = format!("{}{PLACEHOLDER}{}", &text[..start], &text[end..]);
378                let probe = self.probe(&modified, false);
379                let mut items = self.field_items(probe.ctx.as_ref(), prefix);
380                for item in &mut items {
381                    item.insert_text = format!("`{}`", item.label);
382                }
383                return Completions {
384                    range: Span::new(start, end),
385                    items,
386                };
387            }
388            Lexical::Code => {}
389        }
390
391        // The word around the cursor.
392        let start = text[..offset]
393            .char_indices()
394            .rev()
395            .take_while(|(_, c)| is_word_char(*c))
396            .last()
397            .map_or(offset, |(i, _)| i);
398        let end = offset
399            + text[offset..]
400                .chars()
401                .take_while(|c| is_word_char(*c))
402                .map(char::len_utf8)
403                .sum::<usize>();
404        let prefix = &text[start..offset];
405        let range = Span::new(start, end);
406        let before = text[..start].trim_end();
407        let after_dot = before.ends_with('.') && !before.ends_with("..");
408        let previous = before.chars().last();
409
410        // After a complete operand and whitespace, an operator is expected.
411        let previous_word: String = before
412            .chars()
413            .rev()
414            .take_while(|c| is_word_char(*c))
415            .collect::<String>()
416            .chars()
417            .rev()
418            .collect();
419        let operator_position = !prefix.starts_with('$')
420            && before.len() < start
421            && previous.is_some_and(|c| is_word_char(c) || ")]}\"'`".contains(c))
422            && !before.ends_with('$')
423            && !matches!(previous_word.as_str(), "and" | "or" | "in");
424        if operator_position && !after_dot {
425            let ops = [
426                ("and", "Both conditions hold"),
427                ("or", "Either condition holds"),
428                ("in", "Value is one of the items of an array"),
429            ];
430            let items = ops
431                .iter()
432                .filter(|(op, _)| op.starts_with(prefix))
433                .map(|(op, doc)| CompletionItem {
434                    label: (*op).into(),
435                    kind: CompletionKind::Operator,
436                    detail: None,
437                    documentation: Some((*doc).into()),
438                    insert_text: (*op).into(),
439                })
440                .collect();
441            return Completions { range, items };
442        }
443
444        let variable_mode = prefix.starts_with('$');
445        let placeholder = if variable_mode {
446            format!("${PLACEHOLDER}")
447        } else {
448            PLACEHOLDER.to_owned()
449        };
450        let modified = format!("{}{placeholder}{}", &text[..start], &text[end..]);
451        let probe = self.probe(&modified, variable_mode);
452
453        let mut items = Vec::new();
454        if variable_mode {
455            items.extend(self.variable_items(&probe.scope, &prefix[1..]));
456            items.extend(self.function_items(&prefix[1..]));
457        } else {
458            items.extend(self.field_items(probe.ctx.as_ref(), prefix));
459            if !after_dot {
460                items.extend(self.variable_items(&probe.scope, prefix));
461                items.extend(self.function_items(prefix));
462                for kw in ["true", "false", "null"] {
463                    if kw.starts_with(prefix) && !prefix.is_empty() {
464                        items.push(CompletionItem {
465                            label: kw.into(),
466                            kind: CompletionKind::Keyword,
467                            detail: None,
468                            documentation: None,
469                            insert_text: kw.into(),
470                        });
471                    }
472                }
473            }
474        }
475        Completions { range, items }
476    }
477
478    /// Parses `modified` (which contains the placeholder) and finds the placeholder's context.
479    fn probe<'s>(&'s self, modified: &str, variable: bool) -> Probe<'s> {
480        let ast = parse_recover(modified);
481        let mut probe = Probe::default();
482        let mut visit = |event: Event<'_, 's>| match event {
483            Event::Field {
484                name, ctx, scope, ..
485            } if !variable && name == PLACEHOLDER => {
486                probe.ctx = Some(ctx.clone());
487                probe.scope = scope.clone();
488            }
489            Event::Variable { name, scope, .. } if variable && name == PLACEHOLDER => {
490                probe.scope = scope.clone();
491                probe.found_variable = true;
492            }
493            _ => {}
494        };
495        self.walk(&ast.root, &mut visit);
496        if probe.ctx.is_none() && !probe.found_variable && !variable {
497            // Could not locate the cursor (very broken input): offer the root fields.
498            probe.ctx = Some(self.root_ty());
499        }
500        probe
501    }
502
503    fn field_items(&self, ctx: Option<&Ty<'_>>, prefix: &str) -> Vec<CompletionItem> {
504        let Some(ctx) = ctx else {
505            return Vec::new();
506        };
507        let lower = prefix.to_lowercase();
508        let mut fields: Vec<(&str, &Field)> = ctx
509            .fields()
510            .into_iter()
511            .filter(|(name, f)| !f.shape.hidden && name.to_lowercase().starts_with(&lower))
512            .collect();
513        // Required fields first, then by how often they occur; ties keep schema order.
514        fields.sort_by(|a, b| {
515            b.1.required
516                .cmp(&a.1.required)
517                .then(b.1.presence.total_cmp(&a.1.presence))
518        });
519        fields
520            .into_iter()
521            .map(|(name, f)| CompletionItem {
522                label: name.to_owned(),
523                kind: CompletionKind::Field,
524                detail: Some(f.shape.type_summary()),
525                documentation: shape_docs(&f.shape, Some(f)),
526                insert_text: field_insert(name),
527            })
528            .collect()
529    }
530
531    fn variable_items(&self, scope: &Scope<'_>, prefix: &str) -> Vec<CompletionItem> {
532        let mut seen: Vec<String> = Vec::new();
533        let mut items = Vec::new();
534        let externals = self.externals();
535        let candidates = scope
536            .iter()
537            .rev()
538            .chain(externals.iter())
539            .map(|(n, t)| (n.clone(), t.clone()));
540        for (name, ty) in candidates {
541            if name.starts_with(prefix) && !seen.contains(&name) && !name.is_empty() {
542                let detail = match ty.shapes.as_slice() {
543                    [shape] => Some(shape.type_summary()),
544                    _ => None,
545                };
546                items.push(CompletionItem {
547                    label: format!("${name}"),
548                    kind: CompletionKind::Variable,
549                    detail,
550                    documentation: None,
551                    insert_text: format!("${name}"),
552                });
553                seen.push(name);
554            }
555        }
556        for (name, doc) in [("$", "The context value"), ("$$", "The input document")] {
557            if prefix.is_empty() || name[1..].starts_with(prefix) {
558                items.push(CompletionItem {
559                    label: name.into(),
560                    kind: CompletionKind::Variable,
561                    detail: None,
562                    documentation: Some(doc.into()),
563                    insert_text: name.into(),
564                });
565            }
566        }
567        items
568    }
569
570    fn function_items(&self, prefix: &str) -> Vec<CompletionItem> {
571        self.functions()
572            .into_iter()
573            .filter(|f| f.name.starts_with(prefix))
574            .map(|f| CompletionItem {
575                label: format!("${}", f.name),
576                kind: CompletionKind::Function,
577                detail: (!f.signature.is_empty()).then(|| f.signature.clone()),
578                documentation: (!f.summary.is_empty()).then_some(f.summary),
579                insert_text: format!("${}", f.name),
580            })
581            .collect()
582    }
583
584    /// Syntax errors, annotation warnings, references to fields the schema does not have,
585    /// calls of unknown functions, with the wrong number of arguments or with arguments of
586    /// types the signature does not accept, and, when variables are declared, references to
587    /// variables that are not.
588    pub fn diagnostics(&self, text: &str) -> Vec<Diagnostic> {
589        let ast = parse_recover(text);
590        let mut out = ast.diagnostics.clone();
591        // Calls and variables are checked after the walk: a name bound later can be used by a
592        // function defined earlier, or by itself.
593        let mut calls: Vec<CallSite> = Vec::new();
594        let mut variables: Vec<(Span, String)> = Vec::new();
595        let mut bound: Vec<String> = Vec::new();
596        let mut visit = |event: Event<'_, '_>| match event {
597            Event::Field {
598                node, name, ctx, ..
599            } if self.schema.is_some()
600                && ctx.is_known_objects()
601                && ctx.fields().iter().all(|(n, _)| *n != name) =>
602            {
603                let known: Vec<&str> = ctx.fields().iter().map(|(n, _)| *n).collect();
604                let quoted = text[node.span.start..].starts_with('`');
605                let len = name.len() + if quoted { 2 } else { 0 };
606                let span = Span::new(node.span.start, (node.span.start + len).min(node.span.end));
607                out.push(Diagnostic::new(
608                    Severity::Warning,
609                    "W0201",
610                    span,
611                    &[("token", name), ("hint", &did_you_mean(name, &known, ""))],
612                ));
613            }
614            Event::Call {
615                node,
616                name,
617                args,
618                types,
619            } => calls.push(CallSite {
620                span: node.span,
621                name: name.to_owned(),
622                count: args,
623                args: types.map(|t| t.iter().map(|(s, ty)| (*s, ty.symbols())).collect()),
624            }),
625            Event::Variable { node, name, .. } => variables.push((node.span, name.to_owned())),
626            Event::Bind { name } => bound.push(name.to_owned()),
627            Event::Field { .. } => {}
628        };
629        self.walk(&ast.root, &mut visit);
630        out.extend(self.call_diagnostics(&calls, &bound));
631        out.extend(self.variable_diagnostics(&variables, &calls, &bound));
632        out.sort_by_key(|d| d.span.start);
633        out
634    }
635
636    /// `W0202` for calls of functions that are neither built in, declared, nor bound in the
637    /// expression, `W0203` for calls with a number of arguments their signature rejects, and
638    /// `W0205` for arguments whose known types their signature rejects.
639    fn call_diagnostics(&self, calls: &[CallSite], bound: &[String]) -> Vec<Diagnostic> {
640        let functions = self.functions();
641        let mut out = Vec::new();
642        for CallSite {
643            span,
644            name,
645            count,
646            args,
647        } in calls
648        {
649            if name.is_empty()
650                || name == "$"
651                || bound.contains(name)
652                || self.variables.iter().any(|(v, _)| v == name)
653            {
654                continue;
655            }
656            let Some(function) = functions.iter().find(|f| f.name == *name) else {
657                let known: Vec<&str> = functions.iter().map(|f| f.name.as_str()).collect();
658                out.push(Diagnostic::new(
659                    Severity::Warning,
660                    "W0202",
661                    *span,
662                    &[("token", name), ("hint", &did_you_mean(name, &known, "$"))],
663                ));
664                continue;
665            };
666            let Some((min, max)) = arity(&function.signature) else {
667                continue;
668            };
669            if *count < min || max.is_some_and(|max| *count > max) {
670                let arguments = |n: usize| match n {
671                    1 => "1 argument".to_owned(),
672                    n => format!("{n} arguments"),
673                };
674                let expected = match max {
675                    Some(max) if max == min => arguments(max),
676                    Some(max) if min == 0 => format!("at most {}", arguments(max)),
677                    Some(max) => format!("{min} to {max} arguments"),
678                    None => format!("at least {}", arguments(min)),
679                };
680                out.push(Diagnostic::new(
681                    Severity::Warning,
682                    "W0203",
683                    *span,
684                    &[
685                        ("token", name),
686                        ("expected", &expected),
687                        ("count", &count.to_string()),
688                    ],
689                ));
690                continue;
691            }
692            if let (Some(args), Ok(signature)) = (args, Signature::parse(&function.signature))
693                && let Some(index) = mismatch(&signature, args)
694                && let (arg_span, Some(symbols)) = &args[index]
695            {
696                out.push(Diagnostic::new(
697                    Severity::Warning,
698                    "W0205",
699                    *arg_span,
700                    &[
701                        ("index", &(index + 1).to_string()),
702                        ("token", name),
703                        ("type", &describe(symbols)),
704                        ("signature", &function.signature),
705                    ],
706                ));
707            }
708        }
709        out
710    }
711
712    /// `W0204` for references to variables that are neither declared, bound in the expression,
713    /// nor functions. Only when variables are declared: the declarations are then taken to be
714    /// complete.
715    fn variable_diagnostics(
716        &self,
717        references: &[(Span, String)],
718        calls: &[CallSite],
719        bound: &[String],
720    ) -> Vec<Diagnostic> {
721        if self.variables.is_empty() {
722            return Vec::new();
723        }
724        let functions = self.functions();
725        let mut known: Vec<&str> = self.variables.iter().map(|(n, _)| n.as_str()).collect();
726        known.extend(bound.iter().map(String::as_str));
727        references
728            .iter()
729            .filter(|(span, name)| {
730                !name.is_empty()
731                    && name != "$"
732                    && !known.contains(&name.as_str())
733                    && functions.iter().all(|f| f.name != *name)
734                    // A call of an unknown name already has `W0202`.
735                    && calls.iter().all(|c| c.span != *span)
736            })
737            .map(|(span, name)| {
738                Diagnostic::new(
739                    Severity::Warning,
740                    "W0204",
741                    *span,
742                    &[("token", name), ("hint", &did_you_mean(name, &known, "$"))],
743                )
744            })
745            .collect()
746    }
747
748    /// Information about the field, variable or function at `offset`.
749    pub fn hover(&self, text: &str, offset: usize) -> Option<Hover> {
750        let ast = parse_recover(text);
751        let mut best: Option<(Span, String)> = None;
752        let mut consider = |span: Span, contents: String| {
753            if span.contains(offset)
754                && best
755                    .as_ref()
756                    .is_none_or(|(b, _)| span.end - span.start <= b.end - b.start)
757            {
758                best = Some((span, contents));
759            }
760        };
761        let mut visit = |event: Event<'_, '_>| match event {
762            Event::Field {
763                node, name, ctx, ..
764            } => {
765                let span = Span::new(
766                    node.span.start,
767                    (node.span.start + name.len()).min(node.span.end),
768                );
769                let field = ctx
770                    .fields()
771                    .into_iter()
772                    .find(|(n, _)| *n == name)
773                    .map(|(_, f)| f);
774                let contents = match field {
775                    Some(f) => {
776                        let mut s = format!("`{name}`: {}", f.shape.type_summary());
777                        if let Some(docs) = shape_docs(&f.shape, Some(f)) {
778                            s.push_str("\n\n");
779                            s.push_str(&docs);
780                        }
781                        s
782                    }
783                    None if ctx.is_known_objects() => format!("`{name}`: not in the schema"),
784                    None => format!("`{name}`"),
785                };
786                consider(span, contents);
787            }
788            Event::Variable { node, name, ty, .. } => {
789                let contents = match self.function(name) {
790                    Some(f) => format!("`${}{}`\n\n{}", f.name, f.signature, f.summary),
791                    None => match ty.shapes.as_slice() {
792                        [shape] => format!("`${name}`: {}", shape.type_summary()),
793                        _ => format!("`${name}`"),
794                    },
795                };
796                consider(node.span, contents);
797            }
798            Event::Call { .. } | Event::Bind { .. } => {}
799        };
800        self.walk(&ast.root, &mut visit);
801        best.map(|(range, contents)| Hover { range, contents })
802    }
803
804    /// The function call around `offset` and the argument being edited.
805    pub fn signature_help(&self, text: &str, offset: usize) -> Option<SignatureHelp> {
806        let offset = offset.min(text.len());
807        let bytes = text.as_bytes();
808        let mut depth = 0i32;
809        let mut commas = 0;
810        let mut i = offset;
811        while i > 0 {
812            i -= 1;
813            if lexical_state(text, i) != Lexical::Code {
814                continue;
815            }
816            match bytes[i] {
817                b')' | b']' | b'}' => depth += 1,
818                b'[' | b'{' if depth > 0 => depth -= 1,
819                b'[' | b'{' => return None,
820                b'(' if depth > 0 => depth -= 1,
821                b'(' => {
822                    let name_end = text[..i].trim_end().len();
823                    let name_start = text[..name_end]
824                        .char_indices()
825                        .rev()
826                        .take_while(|(_, c)| is_word_char(*c))
827                        .last()
828                        .map_or(name_end, |(j, _)| j);
829                    let name = text[name_start..name_end].strip_prefix('$')?;
830                    let f = self.function(name)?;
831                    return Some(SignatureHelp {
832                        name: format!("${name}"),
833                        signature: f.signature,
834                        params: f.params.iter().map(|p| format!("${p}")).collect(),
835                        active_param: commas,
836                        summary: f.summary,
837                    });
838                }
839                b',' if depth == 0 => commas += 1,
840                _ => {}
841            }
842        }
843        None
844    }
845}
846
847/// The fewest and the most arguments a signature accepts (`None`: no limit), or `None` when
848/// there is no signature to check against. Parameters marked `?` are optional, and so are those
849/// marked `-`, which take the context value; `+` takes any number.
850fn arity(signature: &str) -> Option<(usize, Option<usize>)> {
851    if signature.is_empty() {
852        return None;
853    }
854    let params = Signature::parse(signature).ok()?.params;
855    let min = params.iter().filter(|p| !p.pattern.ends_with('?')).count();
856    let variadic = params.iter().any(|p| p.pattern.ends_with('+'));
857    Some((min, (!variadic).then_some(params.len())))
858}
859
860/// One parameter of a signature: the symbols it accepts, how many arguments it takes, and the
861/// type its array items must have.
862struct Piece<'p> {
863    set: &'p str,
864    min: usize,
865    max: usize,
866    items: Option<char>,
867}
868
869impl Piece<'_> {
870    /// Whether an argument of these types may be accepted (unknown types always may).
871    fn accepts(&self, arg: Option<&Symbols>) -> bool {
872        let Some(arg) = arg else {
873            return true;
874        };
875        // A path yields the items of an array, and a single item stands alone.
876        let mut values = arg.values.chars().chain(arg.items.chars());
877        let mut items = arg
878            .items
879            .chars()
880            .chain(arg.values.chars().filter(|c| *c != 'a'));
881        values.any(|c| self.set.contains(c))
882            && self.items.is_none_or(|want| items.any(|c| c == want))
883    }
884}
885
886/// The index of the first argument whose known types `signature` cannot accept, as jsonata-js
887/// finds it when it reports `T0410` or `T0412`, or `None`.
888fn mismatch(signature: &Signature, args: &[(Span, Option<Symbols>)]) -> Option<usize> {
889    let pieces: Vec<Piece> = signature
890        .params
891        .iter()
892        .map(|p| {
893            let (set, quantifier) = match p.pattern.strip_prefix('[') {
894                Some(rest) => rest.split_once(']').unwrap_or((rest, "")),
895                None => p.pattern.split_at(p.pattern.len().min(1)),
896            };
897            let (min, max) = match quantifier {
898                q if q.contains('+') => (1, usize::MAX),
899                q if q.contains('?') => (0, 1),
900                _ => (1, 1),
901            };
902            let items = match (p.array, p.subtype.as_deref().and_then(|s| s.chars().next())) {
903                (true, Some(c)) if "snblo".contains(c) => Some(c),
904                _ => None,
905            };
906            Piece {
907                set,
908                min,
909                max,
910                items,
911            }
912        })
913        .collect();
914    let args: Vec<Option<&Symbols>> = args.iter().map(|(_, s)| s.as_ref()).collect();
915    if match_pieces(&pieces, &args, 0, true).is_some() {
916        return None;
917    }
918    // The end of the longest run of parameters that match from the start.
919    let mut good_to = 0;
920    for index in 0..pieces.len() {
921        match match_pieces(&pieces[..=index], &args, 0, false) {
922            Some(end) => good_to = end,
923            None => break,
924        }
925    }
926    args.get(good_to).copied().flatten().map(|_| good_to)
927}
928
929/// Greedy backtracking match of `pieces` against `args` from `pos`, as jsonata-js matches its
930/// signature regular expression; returns where the match ends.
931fn match_pieces(
932    pieces: &[Piece],
933    args: &[Option<&Symbols>],
934    pos: usize,
935    anchored: bool,
936) -> Option<usize> {
937    let Some((p, rest)) = pieces.split_first() else {
938        return (!anchored || pos == args.len()).then_some(pos);
939    };
940    let mut n = 0;
941    while n < p.max && pos + n < args.len() && p.accepts(args[pos + n]) {
942        n += 1;
943    }
944    loop {
945        if n < p.min {
946            return None;
947        }
948        if let Some(end) = match_pieces(rest, args, pos + n, anchored) {
949            return Some(end);
950        }
951        if n == 0 {
952            return None;
953        }
954        n -= 1;
955    }
956}
957
958/// `a string`, `an array of numbers or strings`, `a number or null`...
959fn describe(symbols: &Symbols) -> String {
960    let plural = |c| match c {
961        's' => "strings",
962        'n' => "numbers",
963        'b' => "booleans",
964        'l' => "nulls",
965        'o' => "objects",
966        _ => "arrays",
967    };
968    let parts: Vec<String> = symbols
969        .values
970        .chars()
971        .map(|c| match c {
972            's' => "a string".to_owned(),
973            'n' => "a number".to_owned(),
974            'b' => "a boolean".to_owned(),
975            'l' => "null".to_owned(),
976            'o' => "an object".to_owned(),
977            _ => {
978                let items: Vec<&str> = symbols.items.chars().map(plural).collect();
979                format!("an array of {}", items.join(" or "))
980            }
981        })
982        .collect();
983    parts.join(" or ")
984}
985
986/// `; did you mean `{prefix}{candidate}`?` for the closest candidate, or nothing.
987fn did_you_mean(name: &str, candidates: &[&str], prefix: &str) -> String {
988    match closest(name, candidates) {
989        Some(s) => format!("; did you mean `{prefix}{s}`?"),
990        None => String::new(),
991    }
992}
993
994/// The candidate within edit distance 2 of `name`, if any.
995fn closest<'a>(name: &str, candidates: &[&'a str]) -> Option<&'a str> {
996    fn distance(a: &str, b: &str) -> usize {
997        let a: Vec<char> = a.to_lowercase().chars().collect();
998        let b: Vec<char> = b.to_lowercase().chars().collect();
999        let mut prev: Vec<usize> = (0..=b.len()).collect();
1000        for (i, ca) in a.iter().enumerate() {
1001            let mut cur = vec![i + 1; b.len() + 1];
1002            for (j, cb) in b.iter().enumerate() {
1003                cur[j + 1] = (prev[j] + usize::from(ca != cb))
1004                    .min(prev[j + 1] + 1)
1005                    .min(cur[j] + 1);
1006            }
1007            prev = cur;
1008        }
1009        prev[b.len()]
1010    }
1011    candidates
1012        .iter()
1013        .map(|c| (distance(name, c), *c))
1014        .filter(|(d, _)| *d <= 2)
1015        .min_by_key(|(d, _)| *d)
1016        .map(|(_, c)| c)
1017}