Skip to main content

i_slint_compiler/generator/
accessor_names.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! Single source of truth for the names of the public accessors emitted by the
5//! Rust and C++ backends for a property, callback, or function.
6//!
7//! Both backends apply only one transformation to the declaration name (`-` →
8//! `_`) and then prefix it with `get_`/`set_`/`invoke_`/`on_` depending on the
9//! declaration kind. Keeping that mapping here means codegen sites and any
10//! consumer that needs to refer to accessors by name (notably the LSP, when
11//! computing cross-language rename edits) cannot drift.
12
13use smol_str::{SmolStr, format_smolstr};
14
15/// Kind of a public Slint declaration whose accessors we emit.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
17pub enum DeclarationKind {
18    Property,
19    Callback,
20    Function,
21}
22
23/// Individual accessor a backend emits for a public declaration.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
25pub enum AccessorKind {
26    /// `get_<name>` — property getter.
27    Getter,
28    /// `set_<name>` — property setter.
29    Setter,
30    /// `invoke_<name>` — callback or function caller.
31    Invoker,
32    /// `on_<name>` — callback handler installer.
33    Handler,
34}
35
36impl AccessorKind {
37    pub const fn prefix(self) -> &'static str {
38        match self {
39            Self::Getter => "get_",
40            Self::Setter => "set_",
41            Self::Invoker => "invoke_",
42            Self::Handler => "on_",
43        }
44    }
45}
46
47impl DeclarationKind {
48    /// Accessor kinds emitted for this declaration, in the order both backends
49    /// declare them.
50    pub const fn accessor_kinds(self) -> &'static [AccessorKind] {
51        match self {
52            Self::Property => &[AccessorKind::Getter, AccessorKind::Setter],
53            Self::Callback => &[AccessorKind::Invoker, AccessorKind::Handler],
54            Self::Function => &[AccessorKind::Invoker],
55        }
56    }
57}
58
59/// The accessor name emitted by the Rust backend for a declaration named
60/// `name` (e.g. `"get_foo_bar"` for `("foo-bar", Getter)`).
61///
62/// Mirrors the suffix transformation in [`super::rust::ident`]. The prefix
63/// guarantees the result is never a Rust keyword, so no raw-identifier
64/// escaping is applied here.
65pub fn rust_accessor_name(name: &str, accessor: AccessorKind) -> SmolStr {
66    format_accessor_name(name, accessor)
67}
68
69/// The accessor name emitted by the C++ backend for a declaration named
70/// `name`.
71///
72/// Mirrors [`super::cpp::concatenate_ident`]. Today this is identical to
73/// [`rust_accessor_name`]; the helpers are kept separate so the two backends
74/// can diverge cleanly if either ever needs language-specific escaping.
75pub fn cpp_accessor_name(name: &str, accessor: AccessorKind) -> SmolStr {
76    format_accessor_name(name, accessor)
77}
78
79fn format_accessor_name(name: &str, accessor: AccessorKind) -> SmolStr {
80    let prefix = accessor.prefix();
81    if name.contains('-') {
82        let snake = name.replace('-', "_");
83        format_smolstr!("{prefix}{snake}")
84    } else {
85        format_smolstr!("{prefix}{name}")
86    }
87}
88
89/// Same as [`rust_accessor_name`] but wrapped in a [`proc_macro2::Ident`] for
90/// direct use in `quote!` templates.
91#[cfg(any(feature = "rust", feature = "slint-sc"))]
92pub fn rust_accessor_ident(name: &str, accessor: AccessorKind) -> proc_macro2::Ident {
93    quote::format_ident!("{}", rust_accessor_name(name, accessor).as_str())
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn accessor_name_mapping() {
102        // (input_name, accessor_kind, expected_accessor)
103        // Rust and C++ produce identical output today, so each row is asserted
104        // against both helpers in one loop. Adding a case anywhere -- new
105        // kebab-case form, new keyword collision, new whitespace edge case --
106        // is a single row here, not two parallel asserts.
107        const CASES: &[(&str, AccessorKind, &str)] = &[
108            // Bare snake-case / single-word inputs.
109            ("foo", AccessorKind::Getter, "get_foo"),
110            ("foo", AccessorKind::Setter, "set_foo"),
111            ("clicked", AccessorKind::Invoker, "invoke_clicked"),
112            ("clicked", AccessorKind::Handler, "on_clicked"),
113            ("foo_bar", AccessorKind::Getter, "get_foo_bar"),
114            // Kebab-case becomes snake-case.
115            ("foo-bar", AccessorKind::Getter, "get_foo_bar"),
116            ("multi-word-name", AccessorKind::Setter, "set_multi_word_name"),
117            ("do-it", AccessorKind::Invoker, "invoke_do_it"),
118            // The accessor prefix neutralizes language keywords on both sides,
119            // so neither backend needs an escape pass.
120            ("type", AccessorKind::Getter, "get_type"),
121            ("if", AccessorKind::Handler, "on_if"),
122            ("class", AccessorKind::Getter, "get_class"),
123            ("delete", AccessorKind::Invoker, "invoke_delete"),
124        ];
125        for &(name, kind, expected) in CASES {
126            assert_eq!(rust_accessor_name(name, kind), expected, "rust: ({name:?}, {kind:?})");
127            assert_eq!(cpp_accessor_name(name, kind), expected, "cpp: ({name:?}, {kind:?})");
128        }
129    }
130
131    #[test]
132    fn kebab_and_snake_collapse_to_same_accessor() {
133        // Both spellings produce the same accessor; the collision is intentional
134        // and is the LSP scanner's concern, not this helper's.
135        assert_eq!(
136            rust_accessor_name("foo-bar", AccessorKind::Getter),
137            rust_accessor_name("foo_bar", AccessorKind::Getter),
138        );
139    }
140
141    #[test]
142    fn declaration_kind_accessor_sets() {
143        const CASES: &[(DeclarationKind, &[AccessorKind])] = &[
144            (DeclarationKind::Property, &[AccessorKind::Getter, AccessorKind::Setter]),
145            (DeclarationKind::Callback, &[AccessorKind::Invoker, AccessorKind::Handler]),
146            (DeclarationKind::Function, &[AccessorKind::Invoker]),
147        ];
148        for &(kind, expected) in CASES {
149            assert_eq!(kind.accessor_kinds(), expected, "{kind:?}");
150        }
151    }
152}