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/// 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}