Skip to main content

php_lsp/analysis/
code_lens.rs

1/// `textDocument/codeLens` — inline actionable annotations above declarations.
2///
3/// Four lens types are emitted:
4///   1. **Reference count** — above every function, class, and method declaration.
5///   2. **Run test** — above PHPUnit test methods (methods whose name starts with
6///      `test` or that carry a `/** @test */` docblock).
7///   3. **N implementations** — above abstract classes, interfaces, and traits,
8///      counting classes that extend/implement/use them.
9///   4. **overrides ClassName::method** — above methods that override a parent
10///      class method of the same name.
11///
12/// Counts and locations come from mir's inverted indexes (reference posting
13/// lists and subtype edges) plus the salsa workspace index — never from a
14/// per-request AST walk over the workspace.
15///
16/// Reference counts are deferred to `codeLens/resolve`, so only lenses the
17/// editor actually shows pay for a reference lookup.
18use std::collections::{HashMap, VecDeque};
19use std::sync::{Arc, Mutex, OnceLock};
20
21use php_ast::{ClassMemberKind, EnumMemberKind, NamespaceBody, Stmt, StmtKind};
22use serde_json::{Value, json};
23use tower_lsp_server::ls_types::{CodeLens, Command, Location, Uri};
24
25use crate::document::ast::{ParsedDoc, SourceView};
26use crate::document::document_store::{ContentModified, DocumentStore};
27use crate::navigation::moniker::resolve_fqn;
28use crate::navigation::references::{dedup_ref_locations, session_tuple_to_location};
29use crate::text::fqn_short_name;
30
31/// Build all code lenses for `uri`/`doc`. Reference-count lenses come back
32/// without a command; [`resolve_code_lens`] fills them in. Their symbols are
33/// recorded in `scopes` so the first resolve can narrow them all at once.
34///
35/// `should_cancel` is polled between top-level declarations; when it returns
36/// `true` the function returns an empty `Vec` immediately. Pass `|| false`
37/// for requests that do not need cooperative cancellation.
38pub fn code_lenses(
39    uri: &Uri,
40    doc: &ParsedDoc,
41    store: &DocumentStore,
42    imports: &HashMap<String, String>,
43    scopes: &CodeLensScopeCache,
44    should_cancel: impl Fn() -> bool,
45) -> Vec<CodeLens> {
46    let env = LensEnv {
47        uri,
48        doc,
49        store,
50        imports,
51    };
52    let sv = doc.view();
53    let mut slots = Vec::new();
54    collect_lenses(
55        &doc.program().stmts,
56        sv,
57        "",
58        &env,
59        &mut slots,
60        &should_cancel,
61    );
62    if should_cancel() {
63        return Vec::new();
64    }
65
66    let mut symbols = Vec::new();
67    let mut lenses = Vec::with_capacity(slots.len());
68    for slot in slots {
69        lenses.push(match slot {
70            LensSlot::Ready(lens) => lens,
71            LensSlot::RefCount { range, symbol } => {
72                let lens = CodeLens {
73                    range,
74                    command: None,
75                    data: Some(ref_count_data(uri, &symbol)),
76                };
77                symbols.push(symbol);
78                lens
79            }
80        });
81    }
82    scopes.record(uri, symbols);
83    lenses
84}
85
86/// Fill in a reference-count lens produced by [`code_lenses`]; any other
87/// lens is returned unchanged. `rev` is the workspace write revision the
88/// lookup runs at: a write past it aborts with [`ContentModified`].
89pub fn resolve_code_lens(
90    unresolved: CodeLens,
91    store: &DocumentStore,
92    scopes: &CodeLensScopeCache,
93    rev: u64,
94) -> Result<CodeLens, ContentModified> {
95    if unresolved.command.is_some() {
96        return Ok(unresolved);
97    }
98    let Some((uri, symbol)) = unresolved.data.as_ref().and_then(decode_ref_count_data) else {
99        return Ok(unresolved);
100    };
101    let files = scopes.candidate_files(store, &uri, rev, &symbol);
102    let mut locations: Vec<Location> = store
103        .indexed_references(&symbol, &files, false, Some(rev))?
104        .into_iter()
105        .filter_map(session_tuple_to_location)
106        .collect();
107    dedup_ref_locations(&mut locations);
108    let label = match locations.len() {
109        0 => "0 references".to_string(),
110        1 => "1 reference".to_string(),
111        n => format!("{n} references"),
112    };
113    Ok(lens(unresolved.range, &uri, label, locations))
114}
115
116/// Files whose lens symbols are remembered for resolve; older ones fall
117/// back to narrowing each resolved symbol on its own.
118const SCOPE_CACHE_FILES: usize = 32;
119
120type ScopeMap = HashMap<mir_analyzer::Name, Arc<Vec<Arc<str>>>>;
121type FileScopeCell = (Arc<[mir_analyzer::Name]>, Arc<OnceLock<ScopeMap>>);
122
123/// Reference-scope narrowing shared by every `codeLens/resolve` of a file.
124/// The first resolve at a write revision narrows all of the file's lens
125/// symbols in one workspace pass (see
126/// `DocumentStore::batch_reference_candidate_files`); concurrent and later
127/// resolves at that revision wait for and reuse it.
128#[derive(Default)]
129pub struct CodeLensScopeCache {
130    files: Mutex<VecDeque<(Uri, FileScopes)>>,
131}
132
133struct FileScopes {
134    symbols: Arc<[mir_analyzer::Name]>,
135    at_rev: Option<(u64, Arc<OnceLock<ScopeMap>>)>,
136}
137
138impl CodeLensScopeCache {
139    fn record(&self, uri: &Uri, symbols: Vec<mir_analyzer::Name>) {
140        let mut files = self.files.lock().unwrap();
141        files.retain(|(u, _)| u != uri);
142        if files.len() >= SCOPE_CACHE_FILES {
143            files.pop_front();
144        }
145        files.push_back((
146            uri.clone(),
147            FileScopes {
148                symbols: symbols.into(),
149                at_rev: None,
150            },
151        ));
152    }
153
154    fn scopes_at(&self, uri: &Uri, rev: u64) -> Option<FileScopeCell> {
155        let mut files = self.files.lock().unwrap();
156        let (_, file) = files.iter_mut().find(|(u, _)| u == uri)?;
157        let cell = match &file.at_rev {
158            Some((at, cell)) if *at == rev => Arc::clone(cell),
159            _ => {
160                let cell = Arc::new(OnceLock::new());
161                file.at_rev = Some((rev, Arc::clone(&cell)));
162                cell
163            }
164        };
165        Some((Arc::clone(&file.symbols), cell))
166    }
167
168    fn candidate_files(
169        &self,
170        store: &DocumentStore,
171        uri: &Uri,
172        rev: u64,
173        symbol: &mir_analyzer::Name,
174    ) -> Arc<Vec<Arc<str>>> {
175        if let Some((symbols, cell)) = self.scopes_at(uri, rev) {
176            // Narrowed outside the cache lock; `OnceLock` blocks concurrent
177            // resolves of this file until the one batched pass finishes.
178            let map = cell.get_or_init(|| {
179                symbols
180                    .iter()
181                    .cloned()
182                    .zip(store.batch_reference_candidate_files(&symbols))
183                    .collect()
184            });
185            if let Some(files) = map.get(symbol) {
186                return Arc::clone(files);
187            }
188        }
189        store
190            .batch_reference_candidate_files(std::slice::from_ref(symbol))
191            .pop()
192            .unwrap_or_default()
193    }
194}
195
196fn ref_count_data(uri: &Uri, symbol: &mir_analyzer::Name) -> Value {
197    use mir_analyzer::Name;
198    let refs = match symbol {
199        Name::Class(n) => json!(["class", &**n]),
200        Name::Function(n) => json!(["function", &**n]),
201        Name::GlobalConstant(n) => json!(["constant", &**n]),
202        Name::Method { class, name } => json!(["method", &**class, &**name]),
203        Name::Property { class, name } => json!(["property", &**class, &**name]),
204        Name::ClassConstant { class, name } => json!(["class_constant", &**class, &**name]),
205    };
206    json!({ "uri": uri, "refs": refs })
207}
208
209fn decode_ref_count_data(data: &Value) -> Option<(Uri, mir_analyzer::Name)> {
210    use mir_analyzer::Name;
211    let uri = data.get("uri")?.as_str()?.parse::<Uri>().ok()?;
212    let parts = data.get("refs")?.as_array()?;
213    let part = |i: usize| parts.get(i)?.as_str().map(Arc::<str>::from);
214    let symbol = match parts.first()?.as_str()? {
215        "class" => Name::Class(part(1)?),
216        "function" => Name::Function(part(1)?),
217        "constant" => Name::GlobalConstant(part(1)?),
218        "method" => Name::Method {
219            class: part(1)?,
220            name: part(2)?,
221        },
222        "property" => Name::Property {
223            class: part(1)?,
224            name: part(2)?,
225        },
226        "class_constant" => Name::ClassConstant {
227            class: part(1)?,
228            name: part(2)?,
229        },
230        _ => return None,
231    };
232    Some((uri, symbol))
233}
234
235/// Shared lookup context for one code-lens request.
236struct LensEnv<'a> {
237    uri: &'a Uri,
238    doc: &'a ParsedDoc,
239    store: &'a DocumentStore,
240    imports: &'a HashMap<String, String>,
241}
242
243/// A fully built lens, or a reference-count lens left for resolve.
244enum LensSlot {
245    Ready(CodeLens),
246    RefCount {
247        range: tower_lsp_server::ls_types::Range,
248        symbol: mir_analyzer::Name,
249    },
250}
251
252impl LensEnv<'_> {
253    fn impl_count_lens(
254        &self,
255        range: tower_lsp_server::ls_types::Range,
256        fqn: &str,
257        include_trait_users: bool,
258    ) -> CodeLens {
259        let locations: Vec<Location> = self
260            .store
261            .indexed_subtype_classes(fqn, include_trait_users)
262            .unwrap_or_default()
263            .into_iter()
264            .filter_map(|site| subtype_site_to_location(&site.file, &site.range))
265            .collect();
266        let label = match locations.len() {
267            0 => "0 implementations".to_string(),
268            1 => "1 implementation".to_string(),
269            n => format!("{n} implementations"),
270        };
271        lens(range, self.uri, label, locations)
272    }
273
274    /// Declaration location of `method` on the class/trait `parent_fqn`,
275    /// from the salsa workspace index. Prefers the exact-FQN class entry;
276    /// falls back to any same-short-name class declaring the method.
277    /// Walks the full `extends` ancestor chain starting at `parent_fqn` (not
278    /// just the direct parent), so a method inherited unchanged from a
279    /// grandparent (or further) still gets an "overrides" lens — a class two
280    /// or more levels below the declaring ancestor previously got none at all.
281    /// Returns the FQN of the ancestor that actually declares the method
282    /// (which may differ from `parent_fqn` itself) alongside its location.
283    fn parent_method_location(&self, parent_fqn: &str, method: &str) -> Option<(String, Location)> {
284        let ws = self.store.get_workspace_index_salsa();
285        let mut current = parent_fqn.trim_start_matches('\\').to_string();
286        let mut seen = std::collections::HashSet::new();
287        loop {
288            if !seen.insert(current.clone()) {
289                return None;
290            }
291            let chosen = self.store.resolve_class_ref_by_fqn(&ws, &current)?;
292            let (uri, cls) = ws.at(chosen)?;
293            let declaring_fqn = cls.fqn.trim_start_matches('\\').to_string();
294            if let Some(m) = cls.methods.iter().find(|m| m.name.as_ref() == method) {
295                let start = tower_lsp_server::ls_types::Position {
296                    line: m.start_line,
297                    character: m.name_char,
298                };
299                let end = tower_lsp_server::ls_types::Position {
300                    line: m.start_line,
301                    character: m.name_char + m.name.encode_utf16().count() as u32,
302                };
303                return Some((
304                    declaring_fqn,
305                    Location {
306                        uri: uri.clone(),
307                        range: tower_lsp_server::ls_types::Range { start, end },
308                    },
309                ));
310            }
311            let parent = cls.parent.as_deref()?;
312            current = parent.trim_start_matches('\\').to_string();
313        }
314    }
315}
316
317fn collect_lenses(
318    stmts: &[Stmt<'_, '_>],
319    sv: SourceView<'_>,
320    enclosing_ns: &str,
321    env: &LensEnv<'_>,
322    out: &mut Vec<LensSlot>,
323    should_cancel: &impl Fn() -> bool,
324) {
325    // `namespace App;` (unbraced) applies to every following statement.
326    let mut ns: String = enclosing_ns.to_string();
327    for stmt in stmts {
328        if should_cancel() {
329            out.clear();
330            return;
331        }
332        match &stmt.kind {
333            StmtKind::Function(f) => {
334                let name = f.name.as_str().unwrap_or_default();
335                let range = sv.name_range(name);
336                out.push(LensSlot::RefCount {
337                    range,
338                    symbol: mir_analyzer::Name::function(fqn_in(&ns, name)),
339                });
340            }
341            StmtKind::Class(c) => {
342                if let Some(class_name) = c.name {
343                    let class_name_str = class_name.as_str().unwrap_or_default();
344                    let class_fqn = fqn_in(&ns, class_name_str);
345                    let class_range = sv.name_range(class_name_str);
346                    out.push(LensSlot::RefCount {
347                        range: class_range,
348                        symbol: mir_analyzer::Name::class(class_fqn.clone()),
349                    });
350
351                    // Implementations count for abstract classes (classes extending this).
352                    if c.modifiers.is_abstract {
353                        out.push(LensSlot::Ready(env.impl_count_lens(
354                            class_range,
355                            &class_fqn,
356                            false,
357                        )));
358                    }
359
360                    // Direct supertypes — extends parent + used traits — checked once
361                    // per class for overrides lookups on each method.
362                    let parents = collect_direct_supertypes(c, env.doc, env.imports);
363
364                    for member in c.body.members.iter() {
365                        match &member.kind {
366                            ClassMemberKind::Method(m) => {
367                                let method_name = m.name.as_str().unwrap_or_default();
368                                let method_range = sv.name_range(method_name);
369                                out.push(LensSlot::RefCount {
370                                    range: method_range,
371                                    symbol: mir_analyzer::Name::method(
372                                        class_fqn.as_str(),
373                                        method_name,
374                                    ),
375                                });
376
377                                if is_test_method(sv.source(), m) {
378                                    out.push(LensSlot::Ready(run_test_lens(
379                                        method_range,
380                                        env.uri,
381                                        class_name_str,
382                                        method_name,
383                                    )));
384                                }
385
386                                // Overrides lens: emit for each direct supertype (parent class
387                                // or used trait) that declares a method with the same name.
388                                for parent_fqn in &parents {
389                                    if let Some((declaring_fqn, parent_loc)) =
390                                        env.parent_method_location(parent_fqn, method_name)
391                                    {
392                                        out.push(LensSlot::Ready(overrides_lens(
393                                            method_range,
394                                            env.uri,
395                                            fqn_short_name(&declaring_fqn),
396                                            method_name,
397                                            parent_loc,
398                                        )));
399                                    }
400                                }
401
402                                // Constructor-promoted params: `public function __construct(public string $name)`.
403                                if m.name == "__construct" {
404                                    for p in m.params.iter() {
405                                        if p.visibility.is_some() {
406                                            let param_name = p.name.as_str().unwrap_or_default();
407                                            let prop_range = sv.name_range(param_name);
408                                            out.push(LensSlot::RefCount {
409                                                range: prop_range,
410                                                symbol: property_name(&class_fqn, param_name),
411                                            });
412                                        }
413                                    }
414                                }
415                            }
416                            ClassMemberKind::Property(p) => {
417                                let prop_name = p.name.as_str().unwrap_or_default();
418                                let prop_range = sv.name_range(prop_name);
419                                out.push(LensSlot::RefCount {
420                                    range: prop_range,
421                                    symbol: property_name(&class_fqn, prop_name),
422                                });
423                            }
424                            _ => {}
425                        }
426                    }
427                }
428            }
429            StmtKind::Interface(i) => {
430                let name = i.name.as_str().unwrap_or_default();
431                let fqn = fqn_in(&ns, name);
432                let range = sv.name_range(name);
433                out.push(LensSlot::RefCount {
434                    range,
435                    symbol: mir_analyzer::Name::class(fqn.clone()),
436                });
437                // Implementations count lens.
438                out.push(LensSlot::Ready(env.impl_count_lens(range, &fqn, false)));
439            }
440            StmtKind::Trait(t) => {
441                let trait_name = t.name.as_str().unwrap_or_default();
442                let trait_fqn = fqn_in(&ns, trait_name);
443                let range = sv.name_range(trait_name);
444                out.push(LensSlot::RefCount {
445                    range,
446                    symbol: mir_analyzer::Name::class(trait_fqn.clone()),
447                });
448                // Usages: classes that `use` this trait (trait edges included).
449                out.push(LensSlot::Ready(
450                    env.impl_count_lens(range, &trait_fqn, true),
451                ));
452                for member in t.body.members.iter() {
453                    match &member.kind {
454                        ClassMemberKind::Method(m) => {
455                            let method_name = m.name.as_str().unwrap_or_default();
456                            let method_range = sv.name_range(method_name);
457                            out.push(LensSlot::RefCount {
458                                range: method_range,
459                                symbol: mir_analyzer::Name::method(trait_fqn.as_str(), method_name),
460                            });
461                        }
462                        ClassMemberKind::Property(p) => {
463                            let prop_name = p.name.as_str().unwrap_or_default();
464                            let prop_range = sv.name_range(prop_name);
465                            out.push(LensSlot::RefCount {
466                                range: prop_range,
467                                symbol: property_name(&trait_fqn, prop_name),
468                            });
469                        }
470                        _ => {}
471                    }
472                }
473            }
474            StmtKind::Enum(e) => {
475                let enum_name = e.name.as_str().unwrap_or_default();
476                let enum_fqn = fqn_in(&ns, enum_name);
477                let range = sv.name_range(enum_name);
478                out.push(LensSlot::RefCount {
479                    range,
480                    symbol: mir_analyzer::Name::class(enum_fqn.clone()),
481                });
482                for member in e.body.members.iter() {
483                    match &member.kind {
484                        EnumMemberKind::Method(m) => {
485                            let method_name = m.name.as_str().unwrap_or_default();
486                            let method_range = sv.name_range(method_name);
487                            out.push(LensSlot::RefCount {
488                                range: method_range,
489                                symbol: mir_analyzer::Name::method(enum_fqn.as_str(), method_name),
490                            });
491                        }
492                        EnumMemberKind::Case(c) => {
493                            let case_name = c.name.as_str().unwrap_or_default();
494                            let case_range = sv.name_range(case_name);
495                            out.push(LensSlot::RefCount {
496                                range: case_range,
497                                symbol: mir_analyzer::Name::class_constant(
498                                    enum_fqn.as_str(),
499                                    case_name,
500                                ),
501                            });
502                        }
503                        _ => {}
504                    }
505                }
506            }
507            StmtKind::Namespace(nsd) => {
508                let ns_name = nsd
509                    .name
510                    .as_ref()
511                    .map(|n| n.to_string_repr().into_owned())
512                    .unwrap_or_default();
513                match &nsd.body {
514                    NamespaceBody::Braced(inner) => {
515                        collect_lenses(&inner.stmts, sv, &ns_name, env, out, should_cancel);
516                    }
517                    NamespaceBody::Simple => {
518                        ns = ns_name;
519                    }
520                }
521            }
522            _ => {}
523        }
524    }
525}
526
527fn fqn_in(ns: &str, name: &str) -> String {
528    if ns.is_empty() {
529        name.to_string()
530    } else {
531        format!("{ns}\\{name}")
532    }
533}
534
535fn property_name(class_fqn: &str, prop: &str) -> mir_analyzer::Name {
536    mir_analyzer::Name::property(class_fqn, prop)
537}
538
539pub(crate) fn subtype_site_to_location(
540    file: &str,
541    range: &mir_analyzer::Range,
542) -> Option<Location> {
543    let uri = (file).parse::<Uri>().ok()?;
544    // mir uses 1-based lines; 0-based columns.
545    let line = range.start.line.saturating_sub(1);
546    Some(Location {
547        uri,
548        range: tower_lsp_server::ls_types::Range {
549            start: tower_lsp_server::ls_types::Position {
550                line,
551                character: range.start.column,
552            },
553            end: tower_lsp_server::ls_types::Position {
554                line,
555                character: range.end.column,
556            },
557        },
558    })
559}
560
561fn lens(
562    range: tower_lsp_server::ls_types::Range,
563    uri: &Uri,
564    title: String,
565    locations: Vec<Location>,
566) -> CodeLens {
567    CodeLens {
568        range,
569        command: Some(Command {
570            title,
571            command: "editor.action.showReferences".to_string(),
572            arguments: Some(vec![json!(uri), json!(range.start), json!(locations)]),
573        }),
574        data: None,
575    }
576}
577
578fn overrides_lens(
579    range: tower_lsp_server::ls_types::Range,
580    uri: &Uri,
581    parent_class: &str,
582    method_name: &str,
583    parent_location: Location,
584) -> CodeLens {
585    CodeLens {
586        range,
587        command: Some(Command {
588            title: format!("overrides {}::{}", parent_class, method_name),
589            command: "editor.action.showReferences".to_string(),
590            arguments: Some(vec![
591                json!(uri),
592                json!(range.start),
593                json!(vec![parent_location]),
594            ]),
595        }),
596        data: None,
597    }
598}
599
600fn run_test_lens(
601    range: tower_lsp_server::ls_types::Range,
602    uri: &Uri,
603    class: &str,
604    method: &str,
605) -> CodeLens {
606    CodeLens {
607        range,
608        command: Some(Command {
609            title: "▶ Run test".to_string(),
610            command: "php-lsp.runTest".to_string(),
611            arguments: Some(vec![
612                serde_json::json!(uri.to_string()),
613                serde_json::json!(format!("{class}::{method}")),
614            ]),
615        }),
616        data: None,
617    }
618}
619
620/// Direct supertypes of `c` as FQNs — the extended parent class plus every
621/// trait listed in `use` clauses, resolved through this file's imports and
622/// namespace. Order is stable: extends first, then traits in source order.
623/// Duplicates are removed.
624fn collect_direct_supertypes(
625    c: &php_ast::ClassDecl<'_, '_>,
626    doc: &ParsedDoc,
627    imports: &HashMap<String, String>,
628) -> Vec<String> {
629    let mut out: Vec<String> = Vec::new();
630    let mut resolve = |name: std::borrow::Cow<'_, str>| {
631        let fqn = resolve_fqn(doc, &name, imports)
632            .trim_start_matches('\\')
633            .to_string();
634        if !out.contains(&fqn) {
635            out.push(fqn);
636        }
637    };
638    if let Some(extends) = &c.extends {
639        resolve(extends.to_string_repr());
640    }
641    for member in c.body.members.iter() {
642        if let ClassMemberKind::TraitUse(t) = &member.kind {
643            for name in t.traits.iter() {
644                resolve(name.to_string_repr());
645            }
646        }
647    }
648    out
649}
650
651/// A method is a test if its name starts with `test` (PHPUnit convention),
652/// if its leading docblock contains `@test`, or if it carries a `#[Test]`
653/// or `#[PHPUnit\Framework\Attributes\Test]` PHP attribute.
654fn is_test_method(source: &str, m: &php_ast::MethodDecl<'_, '_>) -> bool {
655    if m.name
656        .as_str()
657        .map(|s| s.starts_with("test"))
658        .unwrap_or(false)
659    {
660        return true;
661    }
662    let has_test_attr = m.attributes.iter().any(|attr| {
663        let span = attr.name.span();
664        let attr_name = source
665            .get(span.start as usize..span.end as usize)
666            .unwrap_or("");
667        attr_name == "Test" || attr_name.ends_with("\\Test")
668    });
669    if has_test_attr {
670        return true;
671    }
672    m.doc_comment
673        .as_ref()
674        .is_some_and(|c| c.text.contains("@test"))
675}
676
677#[cfg(test)]
678mod tests {
679    use super::*;
680
681    /// `should_cancel = || true` must return an empty Vec immediately without
682    /// panicking, regardless of the document contents.
683    #[test]
684    fn code_lenses_cancelled_returns_empty() {
685        use crate::document::ast::ParsedDoc;
686
687        let source = "<?php\nclass Foo { public function bar(): void {} }\n";
688        let doc = std::sync::Arc::new(ParsedDoc::parse(source));
689        let uri = "file:///test.php".parse::<Uri>().unwrap();
690        let store = DocumentStore::new();
691
692        let scopes = CodeLensScopeCache::default();
693        let lenses = code_lenses(&uri, &doc, &store, &HashMap::new(), &scopes, || true);
694        assert!(lenses.is_empty(), "cancelled sweep must return empty");
695    }
696
697    #[test]
698    fn ref_count_data_roundtrips_every_symbol_kind() {
699        use mir_analyzer::Name;
700        let uri = "file:///a.php".parse::<Uri>().unwrap();
701        let symbols = [
702            Name::Class(Arc::from("App\\Foo")),
703            Name::Function(Arc::from("App\\run")),
704            Name::GlobalConstant(Arc::from("LIMIT")),
705            Name::method("App\\Foo", "Bar"),
706            Name::property("App\\Foo", "baz"),
707            Name::class_constant("App\\Foo", "Case"),
708        ];
709        for symbol in symbols {
710            let decoded = decode_ref_count_data(&ref_count_data(&uri, &symbol));
711            assert_eq!(decoded, Some((uri.clone(), symbol)));
712        }
713    }
714}