phpantom_lsp 0.7.0

Fast PHP language server with deep type intelligence. Generics, Laravel, PHPStan annotations. Ready in an instant.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
//! Precomputed symbol-location map for a single PHP file.
//!
//! During `update_ast`, every navigable symbol occurrence (class reference,
//! member access, variable, function call, etc.) is recorded as a
//! [`SymbolSpan`] in a flat, sorted vec.  At request time a binary search
//! on this vec replaces character-level backward-walking and
//! provides instant rejection when the cursor lands on whitespace, a
//! string literal, a comment, or any other non-navigable token.
//!
//! The map also stores variable definition sites ([`VarDefSite`]) and
//! scope boundaries so that go-to-definition for `$variable` can be
//! answered entirely from precomputed data without re-parsing.
//!
//! Docblock type references (from `@param`, `@return`, `@var`,
//! `@template`, `@method`, etc.) are extracted by a dedicated string
//! scanner during the AST walk, since docblocks are trivia in the
//! `mago_syntax` AST and produce no expression/statement nodes.
//!
//! The module is split into submodules:
//!
//! - [`docblock`] — Docblock symbol extraction helpers (type span
//!   emission, `@template` / `@method` tag scanning, navigability
//!   filtering, and `get_docblock_text_with_offset`)
//! - [`extraction`] — AST walk that builds a [`SymbolMap`] from a
//!   parsed PHP program (`extract_symbol_map` and all
//!   `extract_from_*` helpers)

pub(crate) mod docblock;
mod extraction;

use crate::php_type::PhpType;

// Re-export the public entry point from extraction.
pub(crate) use extraction::extract_symbol_map;

// ─── Data structures ────────────────────────────────────────────────────────

/// A single navigable symbol occurrence in a file.
///
/// Stored in a sorted vec keyed by `start` offset so that a binary
/// search can locate the symbol (or gap) at any byte position in O(log n).
#[derive(Debug, Clone)]
pub(crate) struct SymbolSpan {
    /// Byte offset of the first character of this symbol token.
    pub start: u32,
    /// Byte offset one past the last character of this symbol token.
    pub end: u32,
    /// What kind of navigable symbol this is.
    pub kind: SymbolKind,
}

/// Which flavour of class-self-reference keyword a `SelfStaticParent`
/// span represents.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SelfStaticParentKind {
    /// The `self` keyword.
    Self_,
    /// The `static` keyword (late static binding).
    Static,
    /// The `parent` keyword.
    Parent,
    /// The `$this` pseudo-variable.
    This,
}

#[derive(Debug, Clone)]
pub(crate) enum SymbolKind {
    /// Class/interface/trait/enum name in a type context:
    /// type hint, `new Foo`, `extends Foo`, `implements Foo`,
    /// `use` statement target, `catch (Foo $e)`, etc.
    ClassReference {
        name: String,
        /// `true` when the original PHP source used a leading `\`
        /// (fully-qualified name).  When set, the resolver should use the
        /// name as-is without prepending the file's namespace.
        is_fqn: bool,
    },
    /// Class/interface/trait/enum name at its *declaration* site
    /// (`class Foo`, `interface Bar`, etc.).  Go-to-definition returns
    /// the symbol's own location so editors can fall back to
    /// Find References.  Also useful for document highlights.
    ClassDeclaration { name: String },

    /// Member name on the RHS of `->`, `?->`, or `::`.
    /// `subject_text` is the source text of the LHS expression.
    MemberAccess {
        subject_text: String,
        member_name: String,
        is_static: bool,
        is_method_call: bool,
        /// `true` when this span was extracted from a docblock reference
        /// (e.g. `@see Order::$channel_type`) rather than real PHP code.
        /// Diagnostics skip these because the subject is a class name,
        /// not a runtime expression.
        is_docblock_reference: bool,
    },

    /// A `$variable` token (usage or definition site).
    Variable {
        /// Name without `$` prefix.
        name: String,
    },

    /// Standalone function call name (not a method call).
    ///
    /// When `is_definition` is `true`, the span covers the function name
    /// at its *declaration* site (`function foo() {}`).  When `false`, it
    /// covers a call site (`foo()`).  The distinction is needed by the
    /// unknown-function diagnostic (which must skip definitions) and by
    /// find-references / document-highlight (which may want to include
    /// both).
    FunctionCall { name: String, is_definition: bool },

    /// `self`, `static`, `parent`, or `$this` in a navigable context.
    SelfStaticParent(SelfStaticParentKind),

    /// A constant name in a navigable context (`define()` name,
    /// class constant access, standalone constant reference).
    ConstantReference { name: String },

    /// A method, property, or constant name at its *declaration* site.
    ///
    /// Go-to-definition returns the symbol's own location so editors
    /// can fall back to Find References.  Also needed for
    /// find-references and rename so that the declaration site
    /// participates in the match.
    MemberDeclaration {
        /// The member name (e.g. `"save"`, `"name"`, `"MAX_SIZE"`).
        /// For properties this is the name WITHOUT the `$` prefix.
        name: String,
        /// Whether this is a static member (`static function`, `static $prop`,
        /// or class constant — constants are always accessed statically).
        is_static: bool,
    },
}

// ─── Template parameter definition site structures ──────────────────────────

/// A `@template` parameter definition site discovered during docblock extraction.
///
/// Stored in `SymbolMap::template_defs`, sorted by `name_offset`.
/// When a `ClassReference` cannot be resolved to an actual class, the
/// resolver checks whether it matches a template parameter in scope and
/// jumps to the `@template` tag that declares it.
#[derive(Debug, Clone)]
pub(crate) struct TemplateParamDef {
    /// Byte offset of the template parameter *name* token (e.g. the `T`
    /// in `@template T of Foo`).
    pub name_offset: u32,
    /// Template parameter name (e.g. `"TKey"`, `"TModel"`).
    pub name: String,
    /// Upper bound from the `of` clause (e.g. `PhpType::Named("array-key")`
    /// for `@template TKey of array-key`), or `None` when unbounded.
    pub bound: Option<PhpType>,
    /// Variance annotation from the `@template` tag.
    pub variance: crate::types::TemplateVariance,
    /// Start of the scope where this template parameter is visible.
    /// For class-level templates this is the docblock start offset;
    /// for method/function-level templates it is the docblock start offset.
    pub scope_start: u32,
    /// End of the scope where this template parameter is visible.
    /// For class-level templates this is the class closing-brace offset;
    /// for method-level templates it is the method closing-brace offset;
    /// for function-level templates it is the function closing-brace offset.
    /// When the scope end cannot be determined (e.g. abstract method), this
    /// is set to `u32::MAX` so the parameter is visible to end-of-file.
    pub scope_end: u32,
}

// ─── Call site structures ───────────────────────────────────────────────────

/// A call expression site discovered during the AST walk.
///
/// Stored in `SymbolMap::call_sites`, sorted by `args_start`.
/// Used by signature help to find the innermost call whose argument
/// list contains the cursor and to compute the active parameter index
/// from precomputed comma offsets.
#[derive(Debug, Clone)]
pub(crate) struct CallSite {
    /// Byte offset immediately after the opening `(`.
    /// The cursor must be > `args_start` to be "inside" the call.
    pub args_start: u32,
    /// Byte offset of the closing `)`.
    /// When the parser recovered from an unclosed paren, this is the
    /// span end the parser chose.
    pub args_end: u32,
    /// The call expression in the format `resolve_callable` expects:
    ///   - `"functionName"` for standalone function calls
    ///   - `"$subject->method"` for instance/null-safe method calls
    ///   - `"ClassName::method"` for static method calls
    ///   - `"new ClassName"` for constructor calls
    pub call_expression: String,
    /// Byte offsets of each top-level comma separator inside the
    /// argument list.  Used to compute the active parameter index:
    /// count how many comma offsets are < cursor offset.
    pub comma_offsets: Vec<u32>,
    /// Byte offset of each argument expression's start token.
    ///
    /// One entry per argument in source order.  Used by inlay hints
    /// to place parameter-name annotations immediately before each
    /// argument.
    pub arg_offsets: Vec<u32>,
    /// Number of arguments passed at the call site.
    ///
    /// Computed from the AST argument list length during extraction.
    /// Unlike `comma_offsets.len() + 1`, this correctly handles empty
    /// argument lists (0) and trailing commas.
    pub arg_count: u32,
    /// Whether any argument uses the `...` spread/unpacking operator.
    ///
    /// When `true`, argument count diagnostics are suppressed because
    /// the actual number of arguments is unknown at static analysis time.
    pub has_unpacking: bool,
    /// Indices (into `arg_offsets`) of arguments that use named syntax
    /// (e.g. `name: $value`).  Inlay hints are suppressed for these
    /// because the parameter name is already visible in source.
    pub named_arg_indices: Vec<u32>,
    /// Parameter names (without `$` prefix) for each named argument,
    /// in the same order as `named_arg_indices`.  Used by inlay hints
    /// to determine which parameters are already consumed by named
    /// arguments so that positional arguments map to the correct
    /// remaining parameters.
    pub named_arg_names: Vec<String>,
    /// Indices (into `arg_offsets`) of arguments that use the `...`
    /// spread/unpacking operator.  Inlay hints are suppressed for these
    /// because a single spread argument may expand into multiple parameters.
    pub spread_arg_indices: Vec<u32>,
}

// ─── Variable definition site structures ────────────────────────────────────

/// A variable definition site discovered during the AST walk.
///
/// Stored in `SymbolMap::var_defs`, sorted by `(scope_start, offset)`,
/// so that go-to-definition for `$var` can be answered entirely from
/// the precomputed map without any scanning at request time.
#[derive(Debug, Clone)]
pub(crate) struct VarDefSite {
    /// Byte offset of the `$var` token at the definition site.
    pub offset: u32,
    /// Variable name *without* `$` prefix.
    pub name: String,
    /// What kind of definition this is.
    pub kind: VarDefKind,
    /// Byte offset of the enclosing scope's opening brace (method body,
    /// function body, closure body) or `0` for top-level code.  Used to
    /// scope the backward search to the correct function/method.
    pub scope_start: u32,
    /// Byte offset from which this definition becomes "visible".
    ///
    /// For **assignments** (`$x = expr;`), this is the end of the
    /// statement — the RHS of an assignment still sees the *previous*
    /// definition of the variable, not the one being written.
    ///
    /// For **parameters**, **foreach**, **catch**, **static**, **global**,
    /// and **destructuring** definitions this equals `offset` (the
    /// definition is immediately visible).
    pub effective_from: u32,
}

/// The kind of variable definition site.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum VarDefKind {
    Assignment,
    Parameter,
    Property,
    Foreach,
    Catch,
    StaticDecl,
    GlobalDecl,
    ArrayDestructuring,
    ListDestructuring,
    ClosureCapture,
    /// A `@param $varName` mention in a docblock.  Not a real variable
    /// definition, but recorded so that `find_variable_scope` can map
    /// the pre-body offset to the correct function body scope for
    /// rename and find-references.
    DocblockParam,
}

/// Per-file symbol location index.
///
/// The `spans` vec is sorted by `start` offset.  Gaps between spans
/// represent non-navigable regions (whitespace, operators, string
/// literal interiors, comment interiors, numeric literals, etc.).
/// When the cursor falls in a gap, the lookup returns `None`
/// immediately — no parsing, no text scanning.
#[derive(Debug, Clone, Default)]
pub(crate) struct SymbolMap {
    pub spans: Vec<SymbolSpan>,
    /// Variable definition sites, sorted by `(scope_start, offset)`.
    pub var_defs: Vec<VarDefSite>,
    /// Scope boundaries `(start_offset, end_offset)` for functions,
    /// methods, closures, and arrow functions.  Used by
    /// `find_enclosing_scope` to determine which scope the cursor is in.
    pub scopes: Vec<(u32, u32)>,
    /// Body boundaries `(body_start_offset, body_end_offset)` for
    /// closures and arrow functions only.
    ///
    /// For closures, `body_start` is the opening `{` offset (same as
    /// the scope start).  For arrow functions, `body_start` is the
    /// `=>` token offset, which is later than the scope start (the
    /// `fn` keyword).
    ///
    /// Used by signature help to suppress the outer call's popup once
    /// the cursor has entered a closure or arrow function body that is
    /// itself an argument to the call.  Separate from `scopes` because
    /// variable resolution needs the full `fn`..`end` range for arrow
    /// function parameter lookups.
    pub body_scopes: Vec<(u32, u32)>,
    /// Narrowing block boundaries `(start_offset, end_offset)` for
    /// if-body, elseif-body, else-body, match-arm, and switch-case
    /// blocks.  Sorted by start offset.
    ///
    /// Used by the diagnostic subject cache to determine whether two
    /// variable accesses are in the same narrowing context.  Accesses
    /// in the same block get the same instanceof narrowing applied and
    /// can share a cache entry, while accesses in different blocks
    /// (e.g. different if/else branches) may resolve to different types
    /// and need independent entries.
    pub narrowing_blocks: Vec<(u32, u32)>,
    /// Offsets of `assert($var instanceof ...)` statements, sorted.
    ///
    /// These act as sequential narrowing boundaries: accesses before and
    /// after an assert-instanceof in the same flat statement list should
    /// get different diagnostic cache entries because the assert changes
    /// the variable's resolved type.  Unlike `narrowing_blocks` (which
    /// model block-scoped if/else branches), these are point boundaries
    /// in a linear statement sequence.
    pub assert_narrowing_offsets: Vec<u32>,
    /// Template parameter definition sites from `@template` docblock tags,
    /// sorted by `name_offset`.  Used to resolve template parameter names
    /// (e.g. `TKey`, `TModel`) that appear in docblock types but are not
    /// actual class names.
    pub template_defs: Vec<TemplateParamDef>,
    /// Call expression sites, sorted by `args_start`.
    /// Used by signature help to find the innermost call containing the
    /// cursor and to compute the active parameter index from AST data.
    pub call_sites: Vec<CallSite>,
    /// Breakable block boundaries `(start_offset, end_offset)` where
    /// `break` is valid (loops and `switch`).
    pub breakable_scopes: Vec<(u32, u32)>,
    /// Loop block boundaries `(start_offset, end_offset)` where
    /// `continue` is valid (`while`, `do/while`, `for`, `foreach`).
    pub loop_scopes: Vec<(u32, u32)>,
    /// Switch body boundaries `(start_offset, end_offset)` where
    /// `case` / `default` labels are valid.
    pub switch_scopes: Vec<(u32, u32)>,
}

impl SymbolMap {
    /// Find the symbol span (if any) that contains `offset`.
    ///
    /// Uses binary search on the sorted `spans` vec.  Returns `None`
    /// when the offset falls in a gap between spans (whitespace,
    /// string interior, comment interior, etc.).
    pub fn lookup(&self, offset: u32) -> Option<&SymbolSpan> {
        let idx = self.spans.partition_point(|s| s.start <= offset);
        if idx == 0 {
            return None;
        }
        let candidate = &self.spans[idx - 1];
        if offset < candidate.end {
            Some(candidate)
        } else {
            None
        }
    }

    /// Find the innermost scope that contains `offset`.
    ///
    /// Returns the `scope_start` (opening brace offset) of the innermost
    /// function/method/closure body that contains the cursor, or `0` when
    /// the cursor is in top-level code.
    pub fn find_enclosing_scope(&self, offset: u32) -> u32 {
        let mut best: u32 = 0;
        for &(start, end) in &self.scopes {
            if start <= offset && offset <= end && start > best {
                best = start;
            }
        }
        best
    }

    /// Determine the effective scope for a variable reference at `offset`.
    ///
    /// For most variable spans this is the same as
    /// [`find_enclosing_scope`].  However, **parameters** and
    /// **docblock `@param $var` mentions** sit physically before the
    /// opening `{` of the function/method/closure body, so
    /// `find_enclosing_scope` returns the *parent* scope for them.
    ///
    /// This method detects those cases and returns the correct body
    /// scope instead:
    ///
    /// 1. If `offset` is on a `VarDefSite` with `VarDefKind::Parameter`,
    ///    return that definition's `scope_start`.
    /// 2. Otherwise, if `offset` is before a scope boundary and there is
    ///    a parameter `VarDefSite` for `var_name` whose `scope_start` is
    ///    the next scope after `offset`, return that scope.  This covers
    ///    docblock `@param` variable tokens that precede the parameter
    ///    list.
    /// 3. Otherwise, fall back to `find_enclosing_scope`.
    pub fn find_variable_scope(&self, var_name: &str, offset: u32) -> u32 {
        // Case 1: cursor is directly on a parameter or docblock @param
        // definition token.  Both sit physically before the body `{`,
        // but their `VarDefSite.scope_start` points to the correct
        // body scope.
        if let Some(def) = self.var_defs.iter().find(|d| {
            d.name == var_name
                && (d.kind == VarDefKind::Parameter || d.kind == VarDefKind::DocblockParam)
                && offset >= d.offset
                && offset < d.offset + 1 + d.name.len() as u32
        }) {
            return def.scope_start;
        }

        self.find_enclosing_scope(offset)
    }

    /// Find the innermost narrowing block (if/elseif/else body, match
    /// arm, switch case) that contains `offset`.
    ///
    /// Returns the block's start offset, or `0` when the offset is not
    /// inside any narrowing block.  Two variable accesses that return
    /// the same value from this method will have identical instanceof
    /// narrowing applied and can safely share a diagnostic cache entry.
    pub fn find_narrowing_block(&self, offset: u32) -> u32 {
        let mut best: u32 = 0;
        for &(start, end) in &self.narrowing_blocks {
            if start <= offset && offset <= end && start > best {
                best = start;
            }
        }
        best
    }

    /// Find the offset of the last `assert($var instanceof …)` statement
    /// that precedes `offset`, or `0` if there is none.
    ///
    /// This is used as a cache discriminator: accesses before and after
    /// an assert-instanceof in the same flat statement list must get
    /// separate cache entries because the assert changes the variable's
    /// resolved type.
    pub fn find_preceding_assert_offset(&self, offset: u32) -> u32 {
        // `assert_narrowing_offsets` is sorted, so binary search for
        // the last element that is strictly less than `offset`.
        match self
            .assert_narrowing_offsets
            .partition_point(|&o| o < offset)
        {
            0 => 0,
            i => self.assert_narrowing_offsets[i - 1],
        }
    }

    /// Find the `@template` definition for a template parameter name at
    /// the given cursor offset.
    ///
    /// Returns the closest (most specific) `TemplateParamDef` whose scope
    /// covers `cursor_offset` and whose name matches.  Method-level
    /// template params are preferred over class-level ones because their
    /// `scope_start` is larger (they are defined later in the file).
    pub fn find_template_def(&self, name: &str, cursor_offset: u32) -> Option<&TemplateParamDef> {
        // Iterate in reverse so that narrower / later-defined scopes
        // (method-level) are checked before broader ones (class-level).
        self.template_defs.iter().rev().find(|d| {
            d.name == name && cursor_offset >= d.scope_start && cursor_offset <= d.scope_end
        })
    }

    /// Find the most recent definition of `$var_name` before
    /// `cursor_offset` within the same scope.
    ///
    /// The caller should obtain `scope_start` via
    /// [`find_enclosing_scope`].
    pub fn find_var_definition(
        &self,
        var_name: &str,
        cursor_offset: u32,
        scope_start: u32,
    ) -> Option<&VarDefSite> {
        self.var_defs.iter().rev().find(|d| {
            d.name == var_name && d.scope_start == scope_start && d.effective_from <= cursor_offset
        })
    }

    /// Return the `effective_from` offset of the most recent definition
    /// of `$var_name` that is visible at `cursor_offset`, or `0` if no
    /// definition is found.
    ///
    /// This is used as a cache-key discriminator: two accesses to the
    /// same variable that fall under the same definition share a cache
    /// entry, but accesses before vs. after a reassignment get different
    /// entries.
    pub fn active_var_def_offset(&self, var_name: &str, cursor_offset: u32) -> u32 {
        let scope_start = self.find_enclosing_scope(cursor_offset);
        self.find_var_definition(var_name, cursor_offset, scope_start)
            .map(|d| d.effective_from)
            .unwrap_or(0)
    }

    /// Check whether `cursor_offset` is physically sitting on a variable
    /// definition token (the `$var` token of an assignment LHS, parameter,
    /// foreach binding, etc.).
    ///
    /// This is used to detect the "already at definition" case *before*
    /// the `effective_from`-based lookup, because the assignment LHS token
    /// exists at the definition site even though the definition hasn't
    /// "taken effect" yet (its `effective_from` is past the cursor).
    pub fn is_at_var_definition(&self, var_name: &str, cursor_offset: u32) -> bool {
        self.var_def_kind_at(var_name, cursor_offset).is_some()
    }

    /// If the cursor is physically on a variable definition token, return
    /// the [`VarDefKind`] of that definition.
    ///
    /// This is a more informative variant of [`is_at_var_definition`] that
    /// lets the caller decide how to handle different definition kinds
    /// (e.g. skip type-hint navigation for parameters and catch variables).
    pub fn var_def_kind_at(&self, var_name: &str, cursor_offset: u32) -> Option<&VarDefKind> {
        self.var_def_at(var_name, cursor_offset).map(|d| &d.kind)
    }

    /// If the cursor is physically on a variable definition token, return
    /// the full [`VarDefSite`].
    ///
    /// This is used by hover to retrieve the `effective_from` offset so
    /// that hovering on the `$` sign of `$x = new Foo()` uses a cursor
    /// offset that includes the assignment itself.
    pub fn var_def_at(&self, var_name: &str, cursor_offset: u32) -> Option<&VarDefSite> {
        // No scope check needed: if the cursor is physically within a
        // VarDefSite's `$var` token, it IS that definition — two different
        // definitions cannot occupy the same bytes.  This also correctly
        // handles parameters, which are physically before the opening
        // brace of the function body (outside `find_enclosing_scope`'s
        // range) but whose VarDefSite has scope_start set to that brace.
        self.var_defs.iter().find(|d| {
            d.name == var_name
                && cursor_offset >= d.offset
                && cursor_offset < d.offset + 1 + d.name.len() as u32
        })
    }

    /// Find the innermost call site whose argument list contains `offset`.
    ///
    /// `call_sites` is sorted by `args_start`.  We want the innermost
    /// (last) one whose range contains the cursor, so we iterate in
    /// reverse and return the first match.
    pub fn find_enclosing_call_site(&self, offset: u32) -> Option<&CallSite> {
        self.call_sites
            .iter()
            .rev()
            .find(|cs| offset >= cs.args_start && offset <= cs.args_end)
    }

    /// Check whether `offset` is inside a closure or arrow-function body
    /// that is nested within a call's argument list.
    ///
    /// Returns `true` when there is a scope (closure/arrow-fn) whose
    /// opening boundary falls inside (`args_start`..`args_end`) and
    /// whose range contains `offset`.  In that case the cursor is
    /// writing code *inside* the closure body, not filling in arguments
    /// to the outer call.
    ///
    /// Used by signature help to suppress the outer call's popup once
    /// the user has entered a closure or arrow function body argument.
    pub fn is_inside_nested_scope_of_call(&self, offset: u32, call: &CallSite) -> bool {
        self.body_scopes.iter().any(|&(body_start, body_end)| {
            body_start > call.args_start
                && body_start < call.args_end
                && offset >= body_start
                && offset <= body_end
        })
    }

    /// Whether `offset` is inside a function-like scope
    /// (function/method/closure/arrow function body).
    pub fn is_inside_function_like_scope(&self, offset: u32) -> bool {
        self.find_enclosing_scope(offset) != 0
    }

    /// Binary-search helper: check whether `offset` falls inside any
    /// `(start, end)` range in a vec sorted by start offset.
    fn offset_in_sorted_ranges(ranges: &[(u32, u32)], offset: u32) -> bool {
        // Find the first range whose start is past `offset`.
        let idx = ranges.partition_point(|&(start, _)| start <= offset);
        // Check all candidate ranges (those with start <= offset) from
        // the closest one backward.  Usually only one or two iterations
        // are needed since scopes are rarely deeply nested.
        ranges[..idx].iter().rev().any(|&(_, end)| offset <= end)
    }

    /// Whether `offset` is inside a breakable scope where `break` is valid.
    pub fn is_inside_breakable_scope(&self, offset: u32) -> bool {
        Self::offset_in_sorted_ranges(&self.breakable_scopes, offset)
    }

    /// Whether `offset` is inside a loop scope where `continue` is valid.
    pub fn is_inside_loop_scope(&self, offset: u32) -> bool {
        Self::offset_in_sorted_ranges(&self.loop_scopes, offset)
    }

    /// Whether `offset` is inside a switch scope where `case/default`
    /// labels are valid.
    pub fn is_inside_switch_scope(&self, offset: u32) -> bool {
        Self::offset_in_sorted_ranges(&self.switch_scopes, offset)
    }
}

#[cfg(test)]
mod tests;