1use std::collections::HashMap;
8
9use indexmap::{IndexMap, IndexSet};
10use plotnik_core::{Interner, NodeFieldId, NodeTypeId, Symbol};
11use plotnik_langs::Lang;
12use rowan::TextRange;
13
14#[derive(Default)]
16pub struct LinkOutput {
17 pub node_type_ids: IndexMap<Symbol, NodeTypeId>,
19 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
31pub 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 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 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 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 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 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
363fn 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#[derive(Clone, Copy)]
389struct ValidationContext {
390 parent_id: NodeTypeId,
392 parent_range: TextRange,
394}
395
396struct 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}