teksilo_parse/diag.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Diagnostic helpers for the `teksu!` macro.
5//!
6//! Every `compile_error!` emitted by expansion runs through these
7//! helpers so error spans land on a user token per spec §9.1, and the
8//! messages match the patterns listed in §9.2.
9
10use proc_macro2::Span;
11use syn::Error;
12
13pub fn error<T: std::fmt::Display>(span: Span, msg: T) -> Error {
14 Error::new(span, msg)
15}
16
17/// Returns true if `name` is a method on `WidgetBuilder` (or the
18/// inherent impl on `WidgetWithHandlers`). These methods wrap the
19/// widget in `WidgetWithHandlers<T>`, which doesn't expose per-widget
20/// builder methods. The lowering reorders handler-attachment items
21/// to come AFTER every widget-specific item so users can write them
22/// in any order without hitting "no method named `child` found for
23/// `WidgetWithHandlers<T>`".
24///
25/// Kept in sync with `crates/teksilo-core/src/widget_builder.rs`.
26pub fn is_widget_builder_method(name: &str) -> bool {
27 matches!(
28 name,
29 // Gestures
30 "on_tap"
31 | "on_double_tap"
32 | "on_triple_tap"
33 | "on_long_press"
34 | "on_drag"
35 | "on_swipe"
36 | "on_pinch"
37 | "accept_tap_buttons"
38 | "accept_double_tap_buttons"
39 | "accept_triple_tap_buttons"
40 | "accept_long_press_buttons"
41 // Focus / keyboard / pointer
42 | "on_focus"
43 | "on_key"
44 | "on_key_preview"
45 | "on_pointer_event"
46 | "on_hover"
47 | "on_scroll"
48 // Framework-level node properties
49 | "focusable"
50 | "tab_index"
51 | "cursor"
52 | "clips_children_on"
53 | "ime_input"
54 | "event_pass_through"
55 | "hit_transparent"
56 | "context_menu"
57 | "focus_within"
58 | "hover_within"
59 | "visible_when"
60 // Drag / drop
61 | "on_drag_hover"
62 | "on_drag_leave"
63 | "on_drag_tick"
64 | "on_drag_ended"
65 | "on_drop"
66 // Accessibility
67 | "on_access_action"
68 | "on_access_action_request"
69 | "access_action"
70 | "access_remove_action"
71 | "access_custom_action"
72 | "access_custom_action_literal"
73 | "access_customize"
74 | "access_label"
75 | "access_label_literal"
76 | "access_description"
77 | "access_description_literal"
78 | "access_hint"
79 | "access_hint_literal"
80 | "access_value"
81 | "access_value_literal"
82 | "access_role"
83 | "access_hidden"
84 | "access_disabled"
85 | "access_identifier"
86 | "access_controls"
87 | "access_described_by"
88 | "access_labelled_by"
89 | "access_live"
90 | "access_current"
91 | "access_has_popup"
92 | "access_orientation"
93 | "access_numeric_value"
94 | "access_numeric_range"
95 | "access_numeric_step"
96 | "access_shortcut_literal"
97 | "access_shortcut_id"
98 | "access_exclude_subtree"
99 | "access_merge_subtree"
100 | "access_subtree"
101 )
102}
103
104/// A bare child element at body position inside a Category
105/// B widget whose content is addressed by named slots. The list below
106/// tracks the set of widgets that have no `.child()` method in the V2
107/// builder API; if a user writes a bare child under one of them, the
108/// compiler would otherwise produce a generic method-resolution error.
109/// We pre-empt with a targeted message pointing at the slot name they
110/// most likely meant.
111pub fn category_b_bare_child(parent_ty: &str, child_span: Span) -> Error {
112 let slot_hint = category_b_slot_hint(parent_ty);
113 Error::new(
114 child_span,
115 format!(
116 "`{parent_ty}` is a Category B widget with named slots — \
117 use `{slot_hint}: <widget>` instead of a bare child element"
118 ),
119 )
120}
121
122/// Returns `Some(canonical type name)` if `ident` names a widget whose
123/// content is addressed via named slots and which does not implement
124/// `.child()`. `None` for every other type — including Category A
125/// containers (VStack, Panel, …) where bare children are legal.
126///
127pub fn is_category_b_widget(ident: &str) -> bool {
128 matches!(
129 ident,
130 "Card"
131 | "Accordion"
132 | "TitleBar"
133 | "DialogContent"
134 | "Breadcrumb"
135 | "TabWidget"
136 | "Popover"
137 | "Snackbar"
138 | "Dialog"
139 | "Wizard"
140 )
141}
142
143/// Pick the most likely slot name for a Category B widget. Used only
144/// to render a better "use `<slot>:` instead" hint — if the user
145/// actually wanted a different slot, the hint still points them at a
146/// real method name and the rest of their fix is obvious.
147fn category_b_slot_hint(ident: &str) -> &'static str {
148 match ident {
149 "Card" => "content",
150 "Accordion" => "content",
151 "TitleBar" => "leading",
152 "DialogContent" => "body",
153 "Breadcrumb" => "item",
154 "TabWidget" => "tab",
155 "Popover" => "content",
156 "Snackbar" => "content",
157 "Dialog" => "content",
158 "Wizard" => "step",
159 _ => "content",
160 }
161}