ink_analyzer/analysis/
inlay_hints.rs

1//! ink! attribute argument inlay hints.
2
3use ink_analyzer_ir::syntax::{AstToken, TextRange, TextSize};
4use ink_analyzer_ir::{InkArg, InkArgKind, InkArgValueKind, InkAttributeKind, InkEntity, InkFile};
5
6use crate::Version;
7
8/// An ink! attribute argument inlay hint.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct InlayHint {
11    /// Text of the inlay hint.
12    pub label: String,
13    /// Position of the inlay hint.
14    pub position: TextSize,
15    /// Range to which the inlay hint applies.
16    pub range: TextRange,
17    /// Extra details about the inlay hint.
18    pub detail: Option<String>,
19}
20
21/// Computes ink! attribute argument inlay hints for the given text range (if any).
22pub fn inlay_hints(file: &InkFile, range: Option<TextRange>, version: Version) -> Vec<InlayHint> {
23    let mut results = Vec::new();
24
25    let mut process_inlay_hint = |arg: &InkArg, is_constructor: bool| {
26        // Filters out ink! attribute arguments that aren't in the selection range.
27        // Note that range of `None` means entire file is in range.
28        if range.is_none_or(|it| it.contains_range(arg.text_range())) {
29            // Creates inlay hint if a non-empty label is defined for the ink! attribute argument.
30            let arg_value_kind = if version.is_legacy() {
31                InkArgValueKind::from(*arg.kind())
32            } else {
33                InkArgValueKind::from_v5(*arg.kind(), Some(is_constructor))
34            };
35
36            let label = arg_value_kind.to_string();
37            if !label.is_empty() {
38                let doc = arg_value_kind.detail();
39                results.push(InlayHint {
40                    label,
41                    position: arg
42                        .name()
43                        .map(|name| name.syntax().text_range().end())
44                        .unwrap_or_else(|| arg.text_range().end()),
45                    range: arg
46                        .name()
47                        .map(|name| name.syntax().text_range())
48                        .unwrap_or_else(|| arg.text_range()),
49                    detail: (!doc.is_empty()).then(|| doc.to_owned()),
50                })
51            }
52        }
53    };
54
55    // Iterates over all ink! attributes in the file.
56    for attr in file.tree().ink_attrs_in_scope() {
57        // Returns inlay hints for all ink! attribute arguments with values in the selection range.
58        let is_constructor = *attr.kind() == InkAttributeKind::Arg(InkArgKind::Constructor);
59        for arg in attr.args() {
60            // For ink! >= 6.x, chain extensions are deprecated.
61            // Ref: <https://github.com/use-ink/ink/pull/2621>
62            if version.is_gte_v6()
63                && matches!(
64                    arg.kind(),
65                    InkArgKind::Extension | InkArgKind::Function | InkArgKind::HandleStatus
66                )
67            {
68                continue;
69            }
70
71            process_inlay_hint(arg, is_constructor);
72
73            let mut nested_arg = None;
74            while let Some(arg) = nested_arg.as_ref().unwrap_or(arg).nested() {
75                process_inlay_hint(&arg, is_constructor);
76                nested_arg = Some(arg);
77            }
78        }
79    }
80    results
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::Version;
87    use ink_analyzer_ir::MinorVersion;
88    use test_utils::parse_offset_at;
89
90    #[test]
91    fn inlay_hints_works() {
92        let fixtures_gte_v5 = vec![
93            (
94                "#[ink(message, default, payable, selector=1)]",
95                None,
96                vec![(
97                    "u32 | _ | @",
98                    Some("selector"),
99                    (Some("<-selector"), Some("selector")),
100                )],
101            ),
102            (
103                "#[ink(constructor, default, payable, selector=1)]",
104                None,
105                vec![(
106                    "u32 | _",
107                    Some("selector"),
108                    (Some("<-selector"), Some("selector")),
109                )],
110            ),
111            (
112                r#"#[ink::event(signature_topic="1111111111111111111111111111111111111111111111111111111111111111")]"#,
113                None,
114                vec![(
115                    "&str",
116                    Some("signature_topic"),
117                    (Some("<-signature_topic"), Some("signature_topic")),
118                )],
119            ),
120            (
121                r#"#[ink(event, signature_topic="1111111111111111111111111111111111111111111111111111111111111111")]"#,
122                None,
123                vec![(
124                    "&str",
125                    Some("signature_topic"),
126                    (Some("<-signature_topic"), Some("signature_topic")),
127                )],
128            ),
129            (
130                r#"#[ink_e2e::test(
131                environment=ink::env::DefaultEnvironment,
132                backend(node(url="ws://127.0.0.1:9000"))
133                )]"#,
134                None,
135                vec![
136                    (
137                        "impl Environment",
138                        Some("environment"),
139                        (Some("<-environment"), Some("environment")),
140                    ),
141                    (
142                        "node | runtime_only",
143                        Some("backend"),
144                        (Some("<-backend"), Some("backend")),
145                    ),
146                    ("&str", Some("url"), (Some("<-url"), Some("url"))),
147                ],
148            ),
149            (
150                r#"#[ink_e2e::test(
151                environment=ink::env::DefaultEnvironment,
152                backend(runtime_only(sandbox=ink_e2e::MinimalSandbox))
153                )]"#,
154                None,
155                vec![
156                    (
157                        "impl Environment",
158                        Some("environment"),
159                        (Some("<-environment"), Some("environment")),
160                    ),
161                    (
162                        "node | runtime_only",
163                        Some("backend"),
164                        (Some("<-backend"), Some("backend")),
165                    ),
166                    (
167                        "impl drink::Sandbox",
168                        Some("sandbox"),
169                        (Some("<-sandbox"), Some("sandbox")),
170                    ),
171                ],
172            ),
173        ];
174        let fixtures_v5_only = vec![
175            (
176                "#[ink::chain_extension(extension=1)]",
177                None,
178                vec![(
179                    "u16",
180                    Some("extension->"),
181                    (Some("<-extension->"), Some("extension->")),
182                )],
183            ),
184            (
185                "#[ink(function=1, handle_status=true)]",
186                None,
187                vec![
188                    (
189                        "u16",
190                        Some("function"),
191                        (Some("<-function"), Some("function")),
192                    ),
193                    (
194                        "bool",
195                        Some("handle_status"),
196                        (Some("<-handle_status"), Some("handle_status")),
197                    ),
198                ],
199            ),
200        ];
201        for (version, fixtures) in [
202            (
203                Version::Legacy,
204                vec![
205                    (
206                        "#[ink(message, default, payable, selector=1)]",
207                        None,
208                        vec![(
209                            "u32 | _",
210                            Some("selector"),
211                            (Some("<-selector"), Some("selector")),
212                        )],
213                    ),
214                    (
215                        "#[ink(extension=1, handle_status=true)]",
216                        None,
217                        vec![
218                            (
219                                "u32",
220                                Some("extension"),
221                                (Some("<-extension"), Some("extension")),
222                            ),
223                            (
224                                "bool",
225                                Some("handle_status"),
226                                (Some("<-handle_status"), Some("handle_status")),
227                            ),
228                        ],
229                    ),
230                    (
231                        r#"#[ink_e2e::test(
232                        additional_contracts="adder/Cargo.toml flipper/Cargo.toml",
233                        environment=ink::env::DefaultEnvironment,
234                        keep_attr="foo,bar"
235                        )]"#,
236                        None,
237                        vec![
238                            (
239                                "&str",
240                                Some("additional_contracts"),
241                                (Some("<-additional_contracts"), Some("additional_contracts")),
242                            ),
243                            (
244                                "impl Environment",
245                                Some("environment"),
246                                (Some("<-environment"), Some("environment")),
247                            ),
248                            (
249                                "&str",
250                                Some("keep_attr"),
251                                (Some("<-keep_attr"), Some("keep_attr")),
252                            ),
253                        ],
254                    ),
255                ],
256            ),
257            (
258                Version::V5(MinorVersion::Base),
259                fixtures_gte_v5
260                    .clone()
261                    .into_iter()
262                    .chain(fixtures_v5_only.clone())
263                    .collect(),
264            ),
265            (
266                Version::V5(MinorVersion::Latest),
267                fixtures_gte_v5
268                    .clone()
269                    .into_iter()
270                    .chain(fixtures_v5_only.clone())
271                    .collect(),
272            ),
273            (
274                Version::V6,
275                [
276                    (
277                        r#"#[ink(message, name="name")]"#,
278                        None,
279                        vec![("&str", Some("name"), (Some("<-name"), Some("name")))],
280                    ),
281                    (
282                        r#"#[ink(constructor, name="name")]"#,
283                        None,
284                        vec![("&str", Some("name"), (Some("<-name"), Some("name")))],
285                    ),
286                    (
287                        r#"#[ink::event(name="name")]"#,
288                        None,
289                        vec![("&str", Some("name"), (Some("<-name"), Some("name")))],
290                    ),
291                    (
292                        r#"#[ink(event, name="name")]"#,
293                        None,
294                        vec![("&str", Some("name"), (Some("<-name"), Some("name")))],
295                    ),
296                    (
297                        r#"#[ink::contract_ref(name="name", env=my::env::Types)]"#,
298                        None,
299                        vec![
300                            ("&str", Some("name"), (Some("<-name"), Some("name"))),
301                            (
302                                "impl Environment",
303                                Some("env"),
304                                (Some("<-env"), Some("env")),
305                            ),
306                        ],
307                    ),
308                    ("#[ink::chain_extension(extension=1)]", None, vec![]),
309                    ("#[ink(function=1, handle_status=true)]", None, vec![]),
310                ]
311                .into_iter()
312                .chain(fixtures_gte_v5)
313                .collect(),
314            ),
315        ] {
316            for (code, selection_range_pat, expected_results) in [
317                // (code, Option<(selection_pat_start, selection_pat_end)>, [(label, detail, pos_pat, (range_pat_start, range_pat_end))]) where:
318                // code = source code,
319                // selection_pat_start = substring used to find the start of the selection range (see `test_utils::parse_offset_at` doc),
320                // selection_pat_end = substring used to find the end of the range the selection range (see `test_utils::parse_offset_at` doc).
321                // label = the label text for the inlay hint,
322                // detail = the optional detail text for the inlay hint,
323                // pos_pat = substring used to find the cursor offset for the inlay hint (see `test_utils::parse_offset_at` doc),
324                // range_pat_start = substring used to find the start of the range the inlay hint applies to (see `test_utils::parse_offset_at` doc),
325                // range_pat_end = substring used to find the end of the range the inlay hint applies to (see `test_utils::parse_offset_at` doc).
326
327                // Control tests.
328                ("// Nothing", None, vec![]),
329                (
330                    r#"
331                    mod my_mod {
332                        fn my_fn(a: bool, b: u8) {
333                        }
334                    }
335                    "#,
336                    None,
337                    vec![],
338                ),
339                // ink! attribute macros.
340                ("#[ink::contract]", None, vec![]),
341                ("#[ink::trait_definition]", None, vec![]),
342                ("#[ink::chain_extension]", None, vec![]),
343                ("#[ink::storage_item]", None, vec![]),
344                ("#[ink::test]", None, vec![]),
345                (
346                    r#"#[ink::contract(env=my::env::Types, keep_attr="foo,bar")]"#,
347                    None,
348                    vec![
349                        (
350                            "impl Environment",
351                            Some("env"),
352                            (Some("<-env"), Some("env")),
353                        ),
354                        (
355                            "&str",
356                            Some("keep_attr"),
357                            (Some("<-keep_attr"), Some("keep_attr")),
358                        ),
359                    ],
360                ),
361                (
362                    r#"#[ink::contract(env=my::env::Types, keep_attr="foo,bar")]"#,
363                    Some((Some("<-"), Some("->"))),
364                    vec![
365                        (
366                            "impl Environment",
367                            Some("env"),
368                            (Some("<-env"), Some("env")),
369                        ),
370                        (
371                            "&str",
372                            Some("keep_attr"),
373                            (Some("<-keep_attr"), Some("keep_attr")),
374                        ),
375                    ],
376                ),
377                (
378                    r#"#[ink::contract(env=my::env::Types, keep_attr="foo,bar")]"#,
379                    Some((Some("<-"), Some("my::env::Types"))),
380                    vec![(
381                        "impl Environment",
382                        Some("env"),
383                        (Some("<-env"), Some("env")),
384                    )],
385                ),
386                (
387                    r#"#[ink::contract(env=my::env::Types, keep_attr="foo,bar")]"#,
388                    Some((Some("<-keep_attr"), Some("->"))),
389                    vec![(
390                        "&str",
391                        Some("keep_attr"),
392                        (Some("<-keep_attr"), Some("keep_attr")),
393                    )],
394                ),
395                (
396                    r#"#[ink::trait_definition(namespace="my_namespace", keep_attr="foo,bar")]"#,
397                    None,
398                    vec![
399                        (
400                            "&str",
401                            Some("namespace"),
402                            (Some("<-namespace"), Some("namespace")),
403                        ),
404                        (
405                            "&str",
406                            Some("keep_attr"),
407                            (Some("<-keep_attr"), Some("keep_attr")),
408                        ),
409                    ],
410                ),
411                (
412                    "#[ink::storage_item(derive=true)]",
413                    None,
414                    vec![("bool", Some("derive"), (Some("<-derive"), Some("derive")))],
415                ),
416                // ink! attribute arguments.
417                ("#[ink(storage)]", None, vec![]),
418                ("#[ink(event, anonymous)]", None, vec![]),
419                (
420                    "#[ink(constructor, default, selector=1)]",
421                    None,
422                    vec![(
423                        "u32 | _",
424                        Some("selector"),
425                        (Some("<-selector"), Some("selector")),
426                    )],
427                ),
428                (
429                    r#"#[ink(impl, namespace="my_namespace")]"#,
430                    None,
431                    vec![(
432                        "&str",
433                        Some("namespace"),
434                        (Some("<-namespace"), Some("namespace")),
435                    )],
436                ),
437            ]
438            .into_iter()
439            .chain(fixtures)
440            {
441                let range = selection_range_pat.map(|(pat_start, pat_end)| {
442                    TextRange::new(
443                        TextSize::from(parse_offset_at(code, pat_start).unwrap() as u32),
444                        TextSize::from(parse_offset_at(code, pat_end).unwrap() as u32),
445                    )
446                });
447                let results = inlay_hints(&InkFile::parse(code), range, version);
448
449                assert_eq!(
450                    results
451                        .into_iter()
452                        .map(|item| (item.label, item.position, item.range))
453                        .collect::<Vec<(String, TextSize, TextRange)>>(),
454                    expected_results
455                        .into_iter()
456                        .map(|(label, pos_pat_start, (range_pat_start, range_pat_end))| (
457                            label.to_owned(),
458                            TextSize::from(parse_offset_at(code, pos_pat_start).unwrap() as u32),
459                            TextRange::new(
460                                TextSize::from(
461                                    parse_offset_at(code, range_pat_start).unwrap() as u32
462                                ),
463                                TextSize::from(parse_offset_at(code, range_pat_end).unwrap() as u32)
464                            )
465                        ))
466                        .collect::<Vec<(String, TextSize, TextRange)>>(),
467                    "code: {code}, version: {:?}",
468                    version
469                );
470            }
471        }
472    }
473}