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/// Membership is decided by the return type: a `WidgetBuilder` method
26/// returning `WidgetWithHandlers<Self>` belongs here, because that
27/// return is what breaks the chain. The `teksilo-teksu-guard` crate
28/// parses `crates/teksilo-core/src/widget_builder.rs` and fails the
29/// build when a wrapping method is absent from the list below, so this
30/// is not kept in step by discipline.
31pub fn is_widget_builder_method(name: &str) -> bool {
32 matches!(
33 name,
34 // Gestures
35 "on_tap"
36 | "on_double_tap"
37 | "on_triple_tap"
38 | "on_long_press"
39 | "on_drag"
40 | "on_swipe"
41 | "on_pinch"
42 | "gesture_dead_zone"
43 | "accept_tap_buttons"
44 | "accept_double_tap_buttons"
45 | "accept_triple_tap_buttons"
46 | "accept_long_press_buttons"
47 // Focus / keyboard / pointer
48 | "on_focus"
49 | "on_key"
50 | "on_key_preview"
51 | "on_pointer_event"
52 | "on_pointer_cancel"
53 | "on_hover"
54 | "on_scroll"
55 | "keyboard_capture"
56 // Touch and pointer arbitration
57 | "touch_action"
58 | "scroll_container"
59 | "pan_claim"
60 | "overscroll_behavior"
61 | "multi_contact"
62 | "long_press_role"
63 | "hit_slop"
64 | "no_hit_slop"
65 // Framework-level node properties
66 | "focusable"
67 | "tab_index"
68 | "cursor"
69 | "clips_children_on"
70 | "ime_input"
71 | "event_pass_through"
72 | "hit_transparent"
73 | "context_menu"
74 | "focus_within"
75 | "hover_within"
76 | "visible_when"
77 // Drag / drop
78 | "drag_activation"
79 | "on_drag_hover"
80 | "on_drag_leave"
81 | "on_drag_tick"
82 | "on_drag_ended"
83 | "on_drop"
84 // Accessibility
85 | "on_access_action"
86 | "on_access_action_request"
87 | "access_action"
88 | "access_remove_action"
89 | "access_custom_action"
90 | "access_custom_action_literal"
91 | "access_customize"
92 | "access_label"
93 | "access_label_literal"
94 | "access_description"
95 | "access_description_literal"
96 | "access_hint"
97 | "access_hint_literal"
98 | "access_value"
99 | "access_value_literal"
100 | "access_role"
101 | "access_hidden"
102 | "access_disabled"
103 | "access_identifier"
104 | "access_controls"
105 | "access_described_by"
106 | "access_labelled_by"
107 | "access_live"
108 | "access_current"
109 | "access_has_popup"
110 | "access_orientation"
111 | "access_numeric_value"
112 | "access_numeric_range"
113 | "access_numeric_step"
114 | "access_shortcut_literal"
115 | "access_shortcut_id"
116 | "access_exclude_subtree"
117 | "access_merge_subtree"
118 | "access_subtree"
119 )
120}
121
122/// A bare child element at body position inside a Category
123/// B widget whose content is addressed by named slots. The list below
124/// tracks the set of widgets that have no `.child()` method in the V2
125/// builder API; if a user writes a bare child under one of them, the
126/// compiler would otherwise produce a generic method-resolution error.
127/// We pre-empt with a targeted message pointing at the slot name they
128/// most likely meant.
129pub fn category_b_bare_child(parent_ty: &str, child_span: Span) -> Error {
130 let slot_hint = category_b_slot_hint(parent_ty);
131 Error::new(
132 child_span,
133 format!(
134 "`{parent_ty}` is a Category B widget with named slots — \
135 use `{slot_hint}: <widget>` instead of a bare child element"
136 ),
137 )
138}
139
140/// Returns `Some(canonical type name)` if `ident` names a widget whose
141/// content is addressed via named slots and which does not implement
142/// `.child()`. `None` for every other type — including Category A
143/// containers (VStack, Panel, …) where bare children are legal.
144///
145pub fn is_category_b_widget(ident: &str) -> bool {
146 matches!(
147 ident,
148 "Card"
149 | "Accordion"
150 | "TitleBar"
151 | "DialogContent"
152 | "Breadcrumb"
153 | "TabWidget"
154 // The popover family is four names, all of them aliases of
155 // `PopoverWidget<T>`, and none of them spelled `Popover` — which is
156 // what this list used to say, so a bare child in any real popover
157 // fell through to the generic error instead of the slot hint.
158 | "PopoverWidget"
159 | "PopoverButton"
160 | "PopoverIconButton"
161 | "PopoverCustom"
162 | "Snackbar"
163 | "Dialog"
164 | "Wizard"
165 )
166}
167
168/// Pick the most likely slot name for a Category B widget. Used only
169/// to render a better "use `<slot>:` instead" hint — if the user
170/// actually wanted a different slot, the hint still points them at a
171/// real method name and the rest of their fix is obvious.
172fn category_b_slot_hint(ident: &str) -> &'static str {
173 match ident {
174 "Card" => "content",
175 "Accordion" => "content",
176 "TitleBar" => "leading",
177 "DialogContent" => "body",
178 "Breadcrumb" => "item",
179 "TabWidget" => "tab",
180 "PopoverWidget" | "PopoverButton" | "PopoverIconButton" | "PopoverCustom" => "content",
181 "Snackbar" => "content",
182 "Dialog" => "content",
183 "Wizard" => "step",
184 _ => "content",
185 }
186}