Skip to main content

draxl_rust/
patch_schema.rs

1use draxl_ast::{Expr, File, Item, Pattern, Stmt};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum NodeKind {
5    File,
6    Mod,
7    Use,
8    Struct,
9    Enum,
10    Fn,
11    Field,
12    Variant,
13    Param,
14    LetStmt,
15    ExprStmt,
16    MatchArm,
17    PatternIdent,
18    PatternWild,
19    Type,
20    ExprPath,
21    ExprLit,
22    ExprGroup,
23    ExprBinary,
24    ExprUnary,
25    ExprCall,
26    ExprMatch,
27    ExprBlock,
28    Doc,
29    Comment,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum FragmentKind {
34    Item,
35    Field,
36    Variant,
37    Param,
38    Stmt,
39    MatchArm,
40    Expr,
41    Type,
42    Pattern,
43    Doc,
44    Comment,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ValueKind {
49    Ident,
50    Str,
51    Bool,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum SlotArity {
56    Ranked,
57    Single,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum AttachmentContainerKind {
62    Items,
63    Stmts,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct SlotSpec {
68    pub public_name: &'static str,
69    pub meta_slot_name: &'static str,
70    pub fragment_kind: FragmentKind,
71    pub arity: SlotArity,
72    pub occupant_removable: bool,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct PathSpec {
77    pub public_name: &'static str,
78    pub value_kind: ValueKind,
79    pub clearable: bool,
80}
81
82pub fn slot_spec(owner: NodeKind, slot: &str) -> Option<SlotSpec> {
83    match (owner, slot) {
84        (NodeKind::File, "items") => Some(SlotSpec {
85            public_name: "items",
86            meta_slot_name: "file_items",
87            fragment_kind: FragmentKind::Item,
88            arity: SlotArity::Ranked,
89            occupant_removable: true,
90        }),
91        (NodeKind::Mod, "items") => Some(SlotSpec {
92            public_name: "items",
93            meta_slot_name: "items",
94            fragment_kind: FragmentKind::Item,
95            arity: SlotArity::Ranked,
96            occupant_removable: true,
97        }),
98        (NodeKind::Struct, "fields") => Some(SlotSpec {
99            public_name: "fields",
100            meta_slot_name: "fields",
101            fragment_kind: FragmentKind::Field,
102            arity: SlotArity::Ranked,
103            occupant_removable: true,
104        }),
105        (NodeKind::Enum, "variants") => Some(SlotSpec {
106            public_name: "variants",
107            meta_slot_name: "variants",
108            fragment_kind: FragmentKind::Variant,
109            arity: SlotArity::Ranked,
110            occupant_removable: true,
111        }),
112        (NodeKind::Fn, "params") => Some(SlotSpec {
113            public_name: "params",
114            meta_slot_name: "params",
115            fragment_kind: FragmentKind::Param,
116            arity: SlotArity::Ranked,
117            occupant_removable: true,
118        }),
119        (NodeKind::Fn, "body") | (NodeKind::ExprBlock, "body") => Some(SlotSpec {
120            public_name: "body",
121            meta_slot_name: "body",
122            fragment_kind: FragmentKind::Stmt,
123            arity: SlotArity::Ranked,
124            occupant_removable: true,
125        }),
126        (NodeKind::ExprMatch, "arms") => Some(SlotSpec {
127            public_name: "arms",
128            meta_slot_name: "arms",
129            fragment_kind: FragmentKind::MatchArm,
130            arity: SlotArity::Ranked,
131            occupant_removable: true,
132        }),
133        (NodeKind::Fn, "ret") => Some(SlotSpec {
134            public_name: "ret",
135            meta_slot_name: "ret",
136            fragment_kind: FragmentKind::Type,
137            arity: SlotArity::Single,
138            occupant_removable: true,
139        }),
140        (NodeKind::Field, "ty") | (NodeKind::Param, "ty") => Some(SlotSpec {
141            public_name: "ty",
142            meta_slot_name: "ty",
143            fragment_kind: FragmentKind::Type,
144            arity: SlotArity::Single,
145            occupant_removable: false,
146        }),
147        (NodeKind::LetStmt, "pat") => Some(SlotSpec {
148            public_name: "pat",
149            meta_slot_name: "pat",
150            fragment_kind: FragmentKind::Pattern,
151            arity: SlotArity::Single,
152            occupant_removable: false,
153        }),
154        (NodeKind::LetStmt, "init") => Some(SlotSpec {
155            public_name: "init",
156            meta_slot_name: "init",
157            fragment_kind: FragmentKind::Expr,
158            arity: SlotArity::Single,
159            occupant_removable: false,
160        }),
161        (NodeKind::ExprStmt, "expr") => Some(SlotSpec {
162            public_name: "expr",
163            meta_slot_name: "expr",
164            fragment_kind: FragmentKind::Expr,
165            arity: SlotArity::Single,
166            occupant_removable: false,
167        }),
168        (NodeKind::ExprGroup, "expr") | (NodeKind::ExprUnary, "expr") => Some(SlotSpec {
169            public_name: "expr",
170            meta_slot_name: "expr",
171            fragment_kind: FragmentKind::Expr,
172            arity: SlotArity::Single,
173            occupant_removable: false,
174        }),
175        (NodeKind::ExprBinary, "lhs") => Some(SlotSpec {
176            public_name: "lhs",
177            meta_slot_name: "lhs",
178            fragment_kind: FragmentKind::Expr,
179            arity: SlotArity::Single,
180            occupant_removable: false,
181        }),
182        (NodeKind::ExprBinary, "rhs") => Some(SlotSpec {
183            public_name: "rhs",
184            meta_slot_name: "rhs",
185            fragment_kind: FragmentKind::Expr,
186            arity: SlotArity::Single,
187            occupant_removable: false,
188        }),
189        (NodeKind::ExprCall, "callee") => Some(SlotSpec {
190            public_name: "callee",
191            meta_slot_name: "callee",
192            fragment_kind: FragmentKind::Expr,
193            arity: SlotArity::Single,
194            occupant_removable: false,
195        }),
196        (NodeKind::ExprMatch, "scrutinee") => Some(SlotSpec {
197            public_name: "scrutinee",
198            meta_slot_name: "scrutinee",
199            fragment_kind: FragmentKind::Expr,
200            arity: SlotArity::Single,
201            occupant_removable: false,
202        }),
203        (NodeKind::MatchArm, "pat") => Some(SlotSpec {
204            public_name: "pat",
205            meta_slot_name: "pat",
206            fragment_kind: FragmentKind::Pattern,
207            arity: SlotArity::Single,
208            occupant_removable: false,
209        }),
210        (NodeKind::MatchArm, "guard") => Some(SlotSpec {
211            public_name: "guard",
212            meta_slot_name: "guard",
213            fragment_kind: FragmentKind::Expr,
214            arity: SlotArity::Single,
215            occupant_removable: true,
216        }),
217        (NodeKind::MatchArm, "body") => Some(SlotSpec {
218            public_name: "body",
219            meta_slot_name: "body",
220            fragment_kind: FragmentKind::Expr,
221            arity: SlotArity::Single,
222            occupant_removable: false,
223        }),
224        _ => None,
225    }
226}
227
228pub fn ranked_slot_spec(owner: NodeKind, slot: &str) -> Option<SlotSpec> {
229    slot_spec(owner, slot).filter(|spec| spec.arity == SlotArity::Ranked)
230}
231
232pub fn single_slot_spec(owner: NodeKind, slot: &str) -> Option<SlotSpec> {
233    slot_spec(owner, slot).filter(|spec| spec.arity == SlotArity::Single)
234}
235
236pub fn removable_slot_spec(owner: NodeKind, slot: &str) -> Option<SlotSpec> {
237    slot_spec(owner, slot).filter(|spec| spec.occupant_removable)
238}
239
240pub fn path_spec(kind: NodeKind, path: &str) -> Option<PathSpec> {
241    match (kind, path) {
242        (NodeKind::Mod, "name")
243        | (NodeKind::Struct, "name")
244        | (NodeKind::Enum, "name")
245        | (NodeKind::Fn, "name")
246        | (NodeKind::Field, "name")
247        | (NodeKind::Variant, "name")
248        | (NodeKind::Param, "name")
249        | (NodeKind::PatternIdent, "name") => Some(PathSpec {
250            public_name: "name",
251            value_kind: ValueKind::Ident,
252            clearable: false,
253        }),
254        (NodeKind::Doc, "text") | (NodeKind::Comment, "text") => Some(PathSpec {
255            public_name: "text",
256            value_kind: ValueKind::Str,
257            clearable: true,
258        }),
259        (NodeKind::ExprBinary, "op") => Some(PathSpec {
260            public_name: "op",
261            value_kind: ValueKind::Ident,
262            clearable: false,
263        }),
264        (NodeKind::ExprUnary, "op") => Some(PathSpec {
265            public_name: "op",
266            value_kind: ValueKind::Ident,
267            clearable: true,
268        }),
269        (NodeKind::ExprStmt, "semi") => Some(PathSpec {
270            public_name: "semi",
271            value_kind: ValueKind::Bool,
272            clearable: true,
273        }),
274        _ => None,
275    }
276}
277
278pub fn clearable_path_spec(kind: NodeKind, path: &str) -> Option<PathSpec> {
279    path_spec(kind, path).filter(|spec| spec.clearable)
280}
281
282pub fn replace_fragment_kind(kind: NodeKind) -> FragmentKind {
283    match kind {
284        NodeKind::Mod | NodeKind::Use | NodeKind::Struct | NodeKind::Enum | NodeKind::Fn => {
285            FragmentKind::Item
286        }
287        NodeKind::Field => FragmentKind::Field,
288        NodeKind::Variant => FragmentKind::Variant,
289        NodeKind::Param => FragmentKind::Param,
290        NodeKind::LetStmt | NodeKind::ExprStmt => FragmentKind::Stmt,
291        NodeKind::MatchArm => FragmentKind::MatchArm,
292        NodeKind::PatternIdent | NodeKind::PatternWild => FragmentKind::Pattern,
293        NodeKind::Type => FragmentKind::Type,
294        NodeKind::ExprPath
295        | NodeKind::ExprLit
296        | NodeKind::ExprGroup
297        | NodeKind::ExprBinary
298        | NodeKind::ExprUnary
299        | NodeKind::ExprCall
300        | NodeKind::ExprMatch
301        | NodeKind::ExprBlock => FragmentKind::Expr,
302        NodeKind::Doc => FragmentKind::Doc,
303        NodeKind::Comment => FragmentKind::Comment,
304        NodeKind::File => FragmentKind::Item,
305    }
306}
307
308pub fn item_kind(item: &Item) -> NodeKind {
309    match item {
310        Item::Mod(_) => NodeKind::Mod,
311        Item::Use(_) => NodeKind::Use,
312        Item::Struct(_) => NodeKind::Struct,
313        Item::Enum(_) => NodeKind::Enum,
314        Item::Fn(_) => NodeKind::Fn,
315        Item::Doc(_) => NodeKind::Doc,
316        Item::Comment(_) => NodeKind::Comment,
317    }
318}
319
320pub fn stmt_kind(stmt: &Stmt) -> NodeKind {
321    match stmt {
322        Stmt::Let(_) => NodeKind::LetStmt,
323        Stmt::Expr(_) => NodeKind::ExprStmt,
324        Stmt::Item(item) => item_kind(item),
325        Stmt::Doc(_) => NodeKind::Doc,
326        Stmt::Comment(_) => NodeKind::Comment,
327    }
328}
329
330pub fn expr_kind(expr: &Expr) -> NodeKind {
331    match expr {
332        Expr::Path(_) => NodeKind::ExprPath,
333        Expr::Lit(_) => NodeKind::ExprLit,
334        Expr::Group(_) => NodeKind::ExprGroup,
335        Expr::Binary(_) => NodeKind::ExprBinary,
336        Expr::Unary(_) => NodeKind::ExprUnary,
337        Expr::Call(_) => NodeKind::ExprCall,
338        Expr::Match(_) => NodeKind::ExprMatch,
339        Expr::Block(_) => NodeKind::ExprBlock,
340    }
341}
342
343pub fn pattern_kind(pattern: &Pattern) -> NodeKind {
344    match pattern {
345        Pattern::Ident(_) => NodeKind::PatternIdent,
346        Pattern::Wild(_) => NodeKind::PatternWild,
347    }
348}
349
350pub fn node_kind_label(kind: NodeKind) -> &'static str {
351    match kind {
352        NodeKind::File => "file",
353        NodeKind::Mod => "module",
354        NodeKind::Use => "use item",
355        NodeKind::Struct => "struct",
356        NodeKind::Enum => "enum",
357        NodeKind::Fn => "function",
358        NodeKind::Field => "field",
359        NodeKind::Variant => "variant",
360        NodeKind::Param => "parameter",
361        NodeKind::LetStmt => "let statement",
362        NodeKind::ExprStmt => "expression statement",
363        NodeKind::MatchArm => "match arm",
364        NodeKind::PatternIdent => "identifier pattern",
365        NodeKind::PatternWild => "wildcard pattern",
366        NodeKind::Type => "type",
367        NodeKind::ExprPath => "path expression",
368        NodeKind::ExprLit => "literal expression",
369        NodeKind::ExprGroup => "grouped expression",
370        NodeKind::ExprBinary => "binary expression",
371        NodeKind::ExprUnary => "unary expression",
372        NodeKind::ExprCall => "call expression",
373        NodeKind::ExprMatch => "match expression",
374        NodeKind::ExprBlock => "block expression",
375        NodeKind::Doc => "doc comment",
376        NodeKind::Comment => "line comment",
377    }
378}
379
380pub fn value_kind_label(value_kind: ValueKind) -> &'static str {
381    match value_kind {
382        ValueKind::Ident => "an identifier value",
383        ValueKind::Str => "a string value",
384        ValueKind::Bool => "a boolean value",
385    }
386}
387
388pub fn attachment_container_kind_for_owner(kind: NodeKind) -> Option<AttachmentContainerKind> {
389    match kind {
390        NodeKind::File | NodeKind::Mod => Some(AttachmentContainerKind::Items),
391        NodeKind::Fn | NodeKind::ExprBlock => Some(AttachmentContainerKind::Stmts),
392        _ => None,
393    }
394}
395
396pub fn attachment_closure_allowed(
397    owner_kind: NodeKind,
398    slot: &str,
399    closure_kind: AttachmentContainerKind,
400) -> bool {
401    let Some(spec) = ranked_slot_spec(owner_kind, slot) else {
402        return false;
403    };
404    matches!(
405        (
406            closure_kind,
407            attachment_container_kind_for_owner(owner_kind),
408            spec.fragment_kind
409        ),
410        (
411            AttachmentContainerKind::Items,
412            Some(AttachmentContainerKind::Items),
413            FragmentKind::Item
414        ) | (
415            AttachmentContainerKind::Stmts,
416            Some(AttachmentContainerKind::Stmts),
417            FragmentKind::Stmt
418        )
419    )
420}
421
422pub fn is_attachable_kind(kind: NodeKind) -> bool {
423    matches!(kind, NodeKind::Doc | NodeKind::Comment)
424}
425
426pub fn invalid_ranked_slot_message(owner_label: &str, slot: &str) -> String {
427    format!("slot `{owner_label}.{slot}` is not available for ranked insertion")
428}
429
430pub fn invalid_single_slot_message(owner_label: &str, slot: &str) -> String {
431    format!("slot `{owner_label}.{slot}` is not available for `put`")
432}
433
434pub fn invalid_set_path_message(node_id: &str, path: &str, kind: NodeKind) -> String {
435    format!(
436        "path `@{node_id}.{path}` is not settable on {}",
437        node_kind_label(kind)
438    )
439}
440
441pub fn invalid_clear_path_message(node_id: &str, path: &str, kind: NodeKind) -> String {
442    format!(
443        "path `@{node_id}.{path}` is not clearable on {}",
444        node_kind_label(kind)
445    )
446}
447
448pub fn required_slot_error_message(action: &str, target_id: &str, slot: &str) -> String {
449    format!(
450        "{} target `{}` cannot be removed from required slot `{}`",
451        action, target_id, slot
452    )
453}
454
455pub fn unsupported_slot_error_message(action: &str, target_id: &str, slot: &str) -> String {
456    format!(
457        "{} target `{}` is in unsupported slot `{}`",
458        action, target_id, slot
459    )
460}
461
462pub fn trivia_move_target_message() -> &'static str {
463    "move does not support doc or comment targets; use attach, detach, replace, or delete"
464}
465
466pub fn single_slot_attachment_closure_message() -> &'static str {
467    "cannot move a node with attached docs/comments into a single-child slot"
468}
469
470pub fn invalid_attachment_closure_destination_message(
471    closure_kind: AttachmentContainerKind,
472) -> &'static str {
473    match closure_kind {
474        AttachmentContainerKind::Items => {
475            "cannot move item attachments into a non-item ranked slot"
476        }
477        AttachmentContainerKind::Stmts => {
478            "cannot move statement attachments into a non-body ranked slot"
479        }
480    }
481}
482
483pub fn invalid_attachment_container_owner_message(
484    owner_label: &str,
485    closure_kind: AttachmentContainerKind,
486) -> String {
487    match closure_kind {
488        AttachmentContainerKind::Items => {
489            format!("owner `{owner_label}` does not expose an item attachment container")
490        }
491        AttachmentContainerKind::Stmts => {
492            format!("owner `{owner_label}` does not expose a statement body slot")
493        }
494    }
495}
496
497pub fn attach_target_not_sibling_message(target_id: &str, node_id: &str) -> String {
498    format!("attach target `{target_id}` is not a sibling semantic node for `{node_id}`")
499}
500
501pub fn detach_requires_following_sibling_message(node_id: &str) -> String {
502    format!("detach source `{node_id}` needs a following sibling semantic node")
503}
504
505pub fn find_node_kind(file: &File, node_id: &str) -> Option<NodeKind> {
506    for item in &file.items {
507        if let Some(kind) = find_in_item(item, node_id) {
508            return Some(kind);
509        }
510    }
511    None
512}
513
514fn find_in_item(item: &Item, node_id: &str) -> Option<NodeKind> {
515    if item.meta().id == node_id {
516        return Some(item_kind(item));
517    }
518
519    match item {
520        Item::Mod(module) => {
521            for child in &module.items {
522                if let Some(kind) = find_in_item(child, node_id) {
523                    return Some(kind);
524                }
525            }
526            None
527        }
528        Item::Struct(strukt) => {
529            for field in &strukt.fields {
530                if field.meta.id == node_id {
531                    return Some(NodeKind::Field);
532                }
533                if field.ty.meta().id == node_id {
534                    return Some(NodeKind::Type);
535                }
536            }
537            None
538        }
539        Item::Enum(enm) => {
540            for variant in &enm.variants {
541                if variant.meta.id == node_id {
542                    return Some(NodeKind::Variant);
543                }
544            }
545            None
546        }
547        Item::Fn(function) => {
548            for param in &function.params {
549                if param.meta.id == node_id {
550                    return Some(NodeKind::Param);
551                }
552                if param.ty.meta().id == node_id {
553                    return Some(NodeKind::Type);
554                }
555            }
556            if function
557                .ret_ty
558                .as_ref()
559                .is_some_and(|ret_ty| ret_ty.meta().id == node_id)
560            {
561                return Some(NodeKind::Type);
562            }
563            find_in_block(&function.body, node_id)
564        }
565        Item::Use(_) | Item::Doc(_) | Item::Comment(_) => None,
566    }
567}
568
569fn find_in_block(block: &draxl_ast::Block, node_id: &str) -> Option<NodeKind> {
570    if block.meta.as_ref().is_some_and(|meta| meta.id == node_id) {
571        return Some(NodeKind::ExprBlock);
572    }
573
574    for stmt in &block.stmts {
575        if let Some(kind) = find_in_stmt(stmt, node_id) {
576            return Some(kind);
577        }
578    }
579    None
580}
581
582fn find_in_stmt(stmt: &Stmt, node_id: &str) -> Option<NodeKind> {
583    match stmt {
584        Stmt::Let(node) => {
585            if node.meta.id == node_id {
586                return Some(NodeKind::LetStmt);
587            }
588            find_in_pattern(&node.pat, node_id).or_else(|| find_in_expr(&node.value, node_id))
589        }
590        Stmt::Expr(node) => {
591            if node.meta.id == node_id {
592                return Some(NodeKind::ExprStmt);
593            }
594            find_in_expr(&node.expr, node_id)
595        }
596        Stmt::Item(item) => find_in_item(item, node_id),
597        Stmt::Doc(node) => (node.meta.id == node_id).then_some(NodeKind::Doc),
598        Stmt::Comment(node) => (node.meta.id == node_id).then_some(NodeKind::Comment),
599    }
600}
601
602fn find_in_expr(expr: &Expr, node_id: &str) -> Option<NodeKind> {
603    if expr.meta().is_some_and(|meta| meta.id == node_id) {
604        return Some(expr_kind(expr));
605    }
606
607    match expr {
608        Expr::Group(group) => find_in_expr(&group.expr, node_id),
609        Expr::Binary(binary) => {
610            find_in_expr(&binary.lhs, node_id).or_else(|| find_in_expr(&binary.rhs, node_id))
611        }
612        Expr::Unary(unary) => find_in_expr(&unary.expr, node_id),
613        Expr::Call(call) => {
614            if let Some(kind) = find_in_expr(&call.callee, node_id) {
615                return Some(kind);
616            }
617            for arg in &call.args {
618                if let Some(kind) = find_in_expr(arg, node_id) {
619                    return Some(kind);
620                }
621            }
622            None
623        }
624        Expr::Match(match_expr) => {
625            if let Some(kind) = find_in_expr(&match_expr.scrutinee, node_id) {
626                return Some(kind);
627            }
628            for arm in &match_expr.arms {
629                if arm.meta.id == node_id {
630                    return Some(NodeKind::MatchArm);
631                }
632                if let Some(kind) = find_in_pattern(&arm.pat, node_id) {
633                    return Some(kind);
634                }
635                if let Some(guard) = &arm.guard {
636                    if let Some(kind) = find_in_expr(guard, node_id) {
637                        return Some(kind);
638                    }
639                }
640                if let Some(kind) = find_in_expr(&arm.body, node_id) {
641                    return Some(kind);
642                }
643            }
644            None
645        }
646        Expr::Block(block) => find_in_block(block, node_id),
647        Expr::Path(_) | Expr::Lit(_) => None,
648    }
649}
650
651fn find_in_pattern(pattern: &Pattern, node_id: &str) -> Option<NodeKind> {
652    if pattern.meta().is_some_and(|meta| meta.id == node_id) {
653        return Some(pattern_kind(pattern));
654    }
655    None
656}