Skip to main content

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}