plotnik_lib/analyze/
link.rs

1//! Link pass: resolve node types and fields against tree-sitter grammar.
2//!
3//! Two-phase approach:
4//! 1. Resolve all symbols (node types and fields) against grammar
5//! 2. Validate structural constraints (field on node type, child type for field)
6
7use std::collections::HashMap;
8
9use indexmap::{IndexMap, IndexSet};
10use plotnik_core::{Interner, NodeFieldId, NodeTypeId, Symbol};
11use plotnik_langs::Lang;
12use rowan::TextRange;
13
14/// Output from the link phase for binary emission.
15#[derive(Default)]
16pub struct LinkOutput {
17    /// Interned name → NodeTypeId (for binary: StringId → NodeTypeId)
18    pub node_type_ids: IndexMap<Symbol, NodeTypeId>,
19    /// Interned name → NodeFieldId (for binary: StringId → NodeFieldId)
20    pub node_field_ids: IndexMap<Symbol, NodeFieldId>,
21}
22
23use super::symbol_table::SymbolTable;
24use super::utils::find_similar;
25use super::visitor::{Visitor, walk};
26use crate::diagnostics::{DiagnosticKind, Diagnostics};
27use crate::parser::ast::{self, Expr, NamedNode};
28use crate::parser::{SyntaxKind, SyntaxToken, token_src};
29use crate::query::{AstMap, SourceId, SourceMap};
30
31/// Link query against a language grammar.
32///
33/// This function is decoupled from `Query` to allow easier testing and
34/// modularity. It orchestrates the resolution and validation phases.
35pub fn link<'q>(
36    interner: &mut Interner,
37    lang: &Lang,
38    source_map: &'q SourceMap,
39    ast_map: &AstMap,
40    symbol_table: &SymbolTable,
41    output: &mut LinkOutput,
42    diagnostics: &mut Diagnostics,
43) {
44    // Local deduplication maps (not exposed in output)
45    let mut node_type_ids: HashMap<&'q str, Option<NodeTypeId>> = HashMap::new();
46    let mut node_field_ids: HashMap<&'q str, Option<NodeFieldId>> = HashMap::new();
47
48    for (&source_id, root) in ast_map {
49        let mut linker = Linker {
50            interner,
51            lang,
52            source_map,
53            symbol_table,
54            source_id,
55            node_type_ids: &mut node_type_ids,
56            node_field_ids: &mut node_field_ids,
57            output,
58            diagnostics,
59        };
60        linker.link(root);
61    }
62}
63
64struct Linker<'a, 'q> {
65    // Refs
66    interner: &'a mut Interner,
67    lang: &'a Lang,
68    source_map: &'q SourceMap,
69    symbol_table: &'a SymbolTable,
70    source_id: SourceId,
71    node_type_ids: &'a mut HashMap<&'q str, Option<NodeTypeId>>,
72    node_field_ids: &'a mut HashMap<&'q str, Option<NodeFieldId>>,
73    output: &'a mut LinkOutput,
74    diagnostics: &'a mut Diagnostics,
75}
76
77impl<'a, 'q> Linker<'a, 'q> {
78    fn source(&self) -> &'q str {
79        self.source_map.content(self.source_id)
80    }
81
82    fn link(&mut self, root: &ast::Root) {
83        self.resolve_symbols(root);
84        self.validate_structure(root);
85    }
86
87    fn resolve_symbols(&mut self, root: &ast::Root) {
88        let mut resolver = SymbolResolver { linker: self };
89        resolver.visit(root);
90    }
91
92    fn resolve_named_node(&mut self, node: &NamedNode) {
93        if node.is_any() {
94            return;
95        }
96        let Some(type_token) = node.node_type() else {
97            return;
98        };
99        if matches!(
100            type_token.kind(),
101            SyntaxKind::KwError | SyntaxKind::KwMissing
102        ) {
103            return;
104        }
105        let type_name = type_token.text();
106        if self.node_type_ids.contains_key(type_name) {
107            return;
108        }
109        let resolved = self.lang.resolve_named_node(type_name);
110        self.node_type_ids
111            .insert(token_src(&type_token, self.source()), resolved);
112        if let Some(id) = resolved {
113            let sym = self.interner.intern(type_name);
114            self.output.node_type_ids.entry(sym).or_insert(id);
115        }
116        if resolved.is_none() {
117            let all_types = self.lang.all_named_node_kinds();
118            let max_dist = (type_name.len() / 3).clamp(2, 4);
119            let suggestion = find_similar(type_name, &all_types, max_dist);
120
121            let mut builder = self
122                .diagnostics
123                .report(
124                    self.source_id,
125                    DiagnosticKind::UnknownNodeType,
126                    type_token.text_range(),
127                )
128                .message(type_name);
129
130            if let Some(similar) = suggestion {
131                builder = builder.hint(format!("did you mean `{}`?", similar));
132            }
133            builder.emit();
134        }
135    }
136
137    fn resolve_field_by_token(&mut self, name_token: Option<SyntaxToken>) {
138        let Some(name_token) = name_token else {
139            return;
140        };
141        let field_name = name_token.text();
142        if self.node_field_ids.contains_key(field_name) {
143            return;
144        }
145        let resolved = self.lang.resolve_field(field_name);
146        self.node_field_ids
147            .insert(token_src(&name_token, self.source()), resolved);
148        if let Some(id) = resolved {
149            let sym = self.interner.intern(field_name);
150            self.output.node_field_ids.entry(sym).or_insert(id);
151            return;
152        }
153        let all_fields = self.lang.all_field_names();
154        let max_dist = (field_name.len() / 3).clamp(2, 4);
155        let suggestion = find_similar(field_name, &all_fields, max_dist);
156
157        let mut builder = self
158            .diagnostics
159            .report(
160                self.source_id,
161                DiagnosticKind::UnknownField,
162                name_token.text_range(),
163            )
164            .message(field_name);
165
166        if let Some(similar) = suggestion {
167            builder = builder.hint(format!("did you mean `{}`?", similar));
168        }
169        builder.emit();
170    }
171
172    fn validate_structure(&mut self, root: &ast::Root) {
173        let defs: Vec<_> = root.defs().collect();
174        for def in defs {
175            let Some(body) = def.body() else { continue };
176            let mut visited = IndexSet::new();
177            self.validate_expr_structure(&body, None, &mut visited);
178        }
179    }
180
181    fn validate_expr_structure(
182        &mut self,
183        expr: &Expr,
184        ctx: Option<ValidationContext>,
185        visited: &mut IndexSet<String>,
186    ) {
187        match expr {
188            Expr::NamedNode(node) => {
189                let child_ctx = self.make_node_context(node);
190
191                for child in node.children() {
192                    if let Expr::FieldExpr(f) = &child {
193                        self.validate_field_expr(f, child_ctx.as_ref(), visited);
194                    } else {
195                        self.validate_expr_structure(&child, child_ctx, visited);
196                    }
197                }
198
199                if let Some(ctx) = child_ctx {
200                    for child in node.as_cst().children() {
201                        if let Some(neg) = ast::NegatedField::cast(child) {
202                            self.validate_negated_field(&neg, &ctx);
203                        }
204                    }
205                }
206            }
207            Expr::AnonymousNode(_) => {}
208            Expr::FieldExpr(f) => {
209                // Should be handled by parent NamedNode, but handle gracefully
210                self.validate_field_expr(f, ctx.as_ref(), visited);
211            }
212            Expr::AltExpr(alt) => {
213                for branch in alt.branches() {
214                    let Some(body) = branch.body() else { continue };
215                    self.validate_expr_structure(&body, ctx, visited);
216                }
217            }
218            Expr::SeqExpr(seq) => {
219                for child in seq.children() {
220                    self.validate_expr_structure(&child, ctx, visited);
221                }
222            }
223            Expr::CapturedExpr(cap) => {
224                let Some(inner) = cap.inner() else { return };
225                self.validate_expr_structure(&inner, ctx, visited);
226            }
227            Expr::QuantifiedExpr(q) => {
228                let Some(inner) = q.inner() else { return };
229                self.validate_expr_structure(&inner, ctx, visited);
230            }
231            Expr::Ref(r) => {
232                let Some(name_token) = r.name() else { return };
233                let name = name_token.text();
234                if !visited.insert(name.to_string()) {
235                    return;
236                }
237                let Some(body) = self.symbol_table.get(name).cloned() else {
238                    visited.swap_remove(name);
239                    return;
240                };
241                self.validate_expr_structure(&body, ctx, visited);
242                visited.swap_remove(name);
243            }
244        }
245    }
246
247    /// Create validation context for a named node's children.
248    fn make_node_context(&self, node: &NamedNode) -> Option<ValidationContext> {
249        if node.is_any() {
250            return None;
251        }
252        let type_token = node.node_type()?;
253        if matches!(
254            type_token.kind(),
255            SyntaxKind::KwError | SyntaxKind::KwMissing
256        ) {
257            return None;
258        }
259        let type_name = type_token.text();
260        let parent_id = self.node_type_ids.get(type_name).copied().flatten()?;
261        // Verify the node type exists in the grammar
262        self.lang.node_type_name(parent_id)?;
263        Some(ValidationContext {
264            parent_id,
265            parent_range: type_token.text_range(),
266        })
267    }
268
269    fn validate_field_expr(
270        &mut self,
271        field: &ast::FieldExpr,
272        ctx: Option<&ValidationContext>,
273        visited: &mut IndexSet<String>,
274    ) {
275        let Some(name_token) = field.name() else {
276            return;
277        };
278        let Some(field_id) = self
279            .node_field_ids
280            .get(name_token.text())
281            .copied()
282            .flatten()
283        else {
284            return;
285        };
286        let Some(ctx) = ctx else { return };
287
288        if !self.lang.has_field(ctx.parent_id, field_id) {
289            self.emit_field_not_on_node(
290                name_token.text_range(),
291                name_token.text(),
292                ctx.parent_id,
293                ctx.parent_range,
294            );
295            return;
296        }
297
298        let Some(value) = field.value() else { return };
299        self.validate_expr_structure(&value, Some(*ctx), visited);
300    }
301
302    fn validate_negated_field(&mut self, neg: &ast::NegatedField, ctx: &ValidationContext) {
303        let Some(name_token) = neg.name() else {
304            return;
305        };
306        let field_name = name_token.text();
307
308        let Some(field_id) = self.node_field_ids.get(field_name).copied().flatten() else {
309            return;
310        };
311
312        if self.lang.has_field(ctx.parent_id, field_id) {
313            return;
314        }
315        self.emit_field_not_on_node(
316            name_token.text_range(),
317            field_name,
318            ctx.parent_id,
319            ctx.parent_range,
320        );
321    }
322
323    fn emit_field_not_on_node(
324        &mut self,
325        range: TextRange,
326        field_name: &str,
327        parent_id: NodeTypeId,
328        parent_range: TextRange,
329    ) {
330        let valid_fields = self.lang.fields_for_node_type(parent_id);
331        let parent_name = self
332            .lang
333            .node_type_name(parent_id)
334            .expect("validated parent_id must have a name");
335
336        let mut builder = self
337            .diagnostics
338            .report(self.source_id, DiagnosticKind::FieldNotOnNodeType, range)
339            .message(field_name)
340            .related_to(
341                self.source_id,
342                parent_range,
343                format!("on `{}`", parent_name),
344            );
345
346        if valid_fields.is_empty() {
347            builder = builder.hint(format!("`{}` has no fields", parent_name));
348        } else {
349            let max_dist = (field_name.len() / 3).clamp(2, 4);
350            if let Some(similar) = find_similar(field_name, &valid_fields, max_dist) {
351                builder = builder.hint(format!("did you mean `{}`?", similar));
352            }
353            builder = builder.hint(format!(
354                "valid fields for `{}`: {}",
355                parent_name,
356                format_list(&valid_fields, 5)
357            ));
358        }
359        builder.emit();
360    }
361}
362
363/// Format a list of items for display, truncating if too long.
364fn format_list(items: &[&str], max_items: usize) -> String {
365    if items.is_empty() {
366        return String::new();
367    }
368    if items.len() <= max_items {
369        items
370            .iter()
371            .map(|s| format!("`{}`", s))
372            .collect::<Vec<_>>()
373            .join(", ")
374    } else {
375        let shown: Vec<_> = items[..max_items]
376            .iter()
377            .map(|s| format!("`{}`", s))
378            .collect();
379        format!(
380            "{}, ... ({} more)",
381            shown.join(", "),
382            items.len() - max_items
383        )
384    }
385}
386
387/// Context for validating child types.
388#[derive(Clone, Copy)]
389struct ValidationContext {
390    /// The parent node type being validated against.
391    parent_id: NodeTypeId,
392    /// The parent node type token range for related_to.
393    parent_range: TextRange,
394}
395
396/// Combined symbol resolver for node types and fields.
397struct SymbolResolver<'l, 'a, 'q> {
398    linker: &'l mut Linker<'a, 'q>,
399}
400
401impl Visitor for SymbolResolver<'_, '_, '_> {
402    fn visit(&mut self, root: &ast::Root) {
403        walk(self, root);
404    }
405
406    fn visit_named_node(&mut self, node: &ast::NamedNode) {
407        self.linker.resolve_named_node(node);
408
409        for neg in node.as_cst().children().filter_map(ast::NegatedField::cast) {
410            self.linker.resolve_field_by_token(neg.name());
411        }
412
413        super::visitor::walk_named_node(self, node);
414    }
415
416    fn visit_anonymous_node(&mut self, node: &ast::AnonymousNode) {
417        if node.is_any() {
418            return;
419        }
420        let Some(value_token) = node.value() else {
421            return;
422        };
423        let value = value_token.text();
424        if self.linker.node_type_ids.contains_key(value) {
425            return;
426        }
427
428        let resolved = self.linker.lang.resolve_anonymous_node(value);
429        self.linker
430            .node_type_ids
431            .insert(token_src(&value_token, self.linker.source()), resolved);
432
433        if let Some(id) = resolved {
434            let sym = self.linker.interner.intern(value);
435            self.linker.output.node_type_ids.entry(sym).or_insert(id);
436            return;
437        }
438
439        self.linker
440            .diagnostics
441            .report(
442                self.linker.source_id,
443                DiagnosticKind::UnknownNodeType,
444                value_token.text_range(),
445            )
446            .message(value)
447            .emit();
448    }
449
450    fn visit_field_expr(&mut self, field: &ast::FieldExpr) {
451        self.linker.resolve_field_by_token(field.name());
452        super::visitor::walk_field_expr(self, field);
453    }
454}