/ src / analysis / resolver.rs
resolver.rs
   1  use crate::analysis::graph::{BindingGraph, EnvVarLocationKind};
   2  use crate::types::{
   3      BindingKind, EnvBinding, EnvBindingUsage, EnvReference, ResolvedEnv, ScopeId, Symbol, SymbolId,
   4      SymbolUsage,
   5  };
   6  use compact_str::CompactString;
   7  use tower_lsp::lsp_types::{Position, Range};
   8  use tracing::error;
   9  
  10  #[derive(Debug, Clone)]
  11  pub enum EnvHit<'a> {
  12      DirectReference(&'a EnvReference),
  13  
  14      ViaSymbol {
  15          symbol: &'a Symbol,
  16          resolved: ResolvedEnv,
  17      },
  18  
  19      ViaUsage {
  20          usage: &'a SymbolUsage,
  21          symbol: &'a Symbol,
  22          resolved: ResolvedEnv,
  23      },
  24  }
  25  
  26  impl<'a> EnvHit<'a> {
  27      pub fn env_var_name(&self) -> Option<CompactString> {
  28          match self {
  29              EnvHit::DirectReference(r) => Some(r.name.clone()),
  30              EnvHit::ViaSymbol { resolved, .. } | EnvHit::ViaUsage { resolved, .. } => {
  31                  match resolved {
  32                      ResolvedEnv::Variable(name) => Some(name.clone()),
  33                      ResolvedEnv::Object(_) => None,
  34                  }
  35              }
  36          }
  37      }
  38  
  39      pub fn canonical_name(&self) -> CompactString {
  40          match self {
  41              EnvHit::DirectReference(r) => r.name.clone(),
  42              EnvHit::ViaSymbol { resolved, .. } | EnvHit::ViaUsage { resolved, .. } => {
  43                  match resolved {
  44                      ResolvedEnv::Variable(name) => name.clone(),
  45                      ResolvedEnv::Object(name) => name.clone(),
  46                  }
  47              }
  48          }
  49      }
  50  
  51      pub fn range(&self) -> Range {
  52          match self {
  53              EnvHit::DirectReference(r) => r.name_range,
  54              EnvHit::ViaSymbol { symbol, .. } => symbol.name_range,
  55              EnvHit::ViaUsage { usage, .. } => usage.range,
  56          }
  57      }
  58  
  59      pub fn is_env_object(&self) -> bool {
  60          match self {
  61              EnvHit::DirectReference(_) => false,
  62              EnvHit::ViaSymbol { resolved, .. } | EnvHit::ViaUsage { resolved, .. } => {
  63                  matches!(resolved, ResolvedEnv::Object(_))
  64              }
  65          }
  66      }
  67  
  68      pub fn binding_name(&self) -> Option<&CompactString> {
  69          match self {
  70              EnvHit::DirectReference(_) => None,
  71              EnvHit::ViaSymbol { symbol, .. } => Some(&symbol.name),
  72              EnvHit::ViaUsage { symbol, .. } => Some(&symbol.name),
  73          }
  74      }
  75  }
  76  
  77  #[derive(Debug, Clone)]
  78  pub struct ResolvedBinding {
  79      pub binding_name: CompactString,
  80  
  81      pub env_var_name: CompactString,
  82  
  83      pub binding_range: Range,
  84  
  85      pub declaration_range: Range,
  86  
  87      pub kind: BindingKind,
  88  
  89      pub is_usage: bool,
  90  }
  91  
  92  impl ResolvedBinding {
  93      pub fn to_env_binding(&self, scope_range: Range) -> EnvBinding {
  94          EnvBinding {
  95              binding_name: self.binding_name.clone(),
  96              env_var_name: self.env_var_name.clone(),
  97              binding_range: self.binding_range,
  98              declaration_range: self.declaration_range,
  99              scope_range,
 100              is_valid: true,
 101              kind: self.kind.clone(),
 102              destructured_key_range: None,
 103          }
 104      }
 105  
 106      pub fn to_binding_usage(&self) -> EnvBindingUsage {
 107          EnvBindingUsage {
 108              name: self.binding_name.clone(),
 109              range: self.binding_range,
 110              declaration_range: self.declaration_range,
 111              env_var_name: self.env_var_name.clone(),
 112          }
 113      }
 114  }
 115  
 116  pub struct BindingResolver<'a> {
 117      graph: &'a BindingGraph,
 118  }
 119  
 120  impl<'a> BindingResolver<'a> {
 121      pub fn new(graph: &'a BindingGraph) -> Self {
 122          Self { graph }
 123      }
 124  
 125      pub fn env_at_position(&self, position: Position) -> Option<EnvHit<'a>> {
 126          tracing::trace!(
 127              "Looking up env at position line={}, char={}",
 128              position.line,
 129              position.character
 130          );
 131  
 132          for reference in self.graph.direct_references() {
 133              if BindingGraph::contains_position(reference.name_range, position) {
 134                  return Some(EnvHit::DirectReference(reference));
 135              }
 136          }
 137  
 138          if let Some(symbol) = self.graph.symbol_at_position(position) {
 139              if let Some(resolved) = self.graph.resolve_to_env(symbol.id) {
 140                  return Some(EnvHit::ViaSymbol { symbol, resolved });
 141              }
 142          }
 143  
 144          if let Some(symbol_id) = self.graph.symbol_at_destructure_key(position) {
 145              if let Some(symbol) = self.graph.get_symbol(symbol_id) {
 146                  if let Some(resolved) = self.graph.resolve_to_env(symbol_id) {
 147                      return Some(EnvHit::ViaSymbol { symbol, resolved });
 148                  }
 149              }
 150          }
 151  
 152          if let Some(usage) = self.graph.usage_at_position(position) {
 153              if let Some(symbol) = self.graph.get_symbol(usage.symbol_id) {
 154                  if let Some(property) = &usage.property_access {
 155                      if let Some(resolved) = self.graph.resolve_to_env(usage.symbol_id) {
 156                          if matches!(resolved, ResolvedEnv::Object(_)) {
 157                              return Some(EnvHit::ViaUsage {
 158                                  usage,
 159                                  symbol,
 160                                  resolved: ResolvedEnv::Variable(property.clone()),
 161                              });
 162                          }
 163                      }
 164                  } else if let Some(resolved) = self.graph.resolve_to_env(usage.symbol_id) {
 165                      return Some(EnvHit::ViaUsage {
 166                          usage,
 167                          symbol,
 168                          resolved,
 169                      });
 170                  }
 171              }
 172          }
 173  
 174          tracing::debug!(
 175              "No env var found at position line={}, char={}",
 176              position.line,
 177              position.character
 178          );
 179          None
 180      }
 181  
 182      pub fn binding_at_position(&self, position: Position) -> Option<ResolvedBinding> {
 183          let hit = self.env_at_position(position)?;
 184  
 185          match hit {
 186              EnvHit::DirectReference(_) => None,
 187  
 188              EnvHit::ViaSymbol { symbol, resolved } => {
 189                  let env_var_name = match &resolved {
 190                      ResolvedEnv::Variable(name) => name.clone(),
 191                      ResolvedEnv::Object(name) => name.clone(),
 192                  };
 193  
 194                  let kind = match &resolved {
 195                      ResolvedEnv::Variable(_) => BindingKind::Value,
 196                      ResolvedEnv::Object(_) => BindingKind::Object,
 197                  };
 198  
 199                  Some(ResolvedBinding {
 200                      binding_name: symbol.name.clone(),
 201                      env_var_name,
 202                      binding_range: symbol.name_range,
 203                      declaration_range: symbol.declaration_range,
 204                      kind,
 205                      is_usage: false,
 206                  })
 207              }
 208  
 209              EnvHit::ViaUsage {
 210                  usage,
 211                  symbol,
 212                  resolved,
 213              } => {
 214                  let env_var_name = match &resolved {
 215                      ResolvedEnv::Variable(name) => name.clone(),
 216                      ResolvedEnv::Object(name) => name.clone(),
 217                  };
 218  
 219                  let kind = match &resolved {
 220                      ResolvedEnv::Variable(_) => BindingKind::Value,
 221                      ResolvedEnv::Object(_) => BindingKind::Object,
 222                  };
 223  
 224                  Some(ResolvedBinding {
 225                      binding_name: symbol.name.clone(),
 226                      env_var_name,
 227                      binding_range: usage.range,
 228                      declaration_range: symbol.declaration_range,
 229                      kind,
 230                      is_usage: true,
 231                  })
 232              }
 233          }
 234      }
 235  
 236      pub fn direct_reference_at_position(&self, position: Position) -> Option<&'a EnvReference> {
 237          self.graph
 238              .direct_references()
 239              .iter()
 240              .find(|&reference| BindingGraph::contains_position(reference.name_range, position))
 241      }
 242  
 243      /// Find all usages of a given env var name.
 244      /// Uses O(1) index lookup if available, falls back to O(n) scan otherwise.
 245      pub fn find_env_var_usages(&self, env_var_name: &str) -> Vec<EnvVarUsageLocation> {
 246          // Try the pre-built index first for O(1) lookup
 247          if let Some(locations) = self.graph.get_env_var_locations(env_var_name) {
 248              return locations
 249                  .iter()
 250                  .map(|loc| EnvVarUsageLocation {
 251                      range: loc.range,
 252                      kind: Self::convert_location_kind(loc.kind),
 253                      binding_name: loc.binding_name.clone(),
 254                  })
 255                  .collect();
 256          }
 257  
 258          // Fall back to scanning (for when rebuild_range_index hasn't been called)
 259          self.find_env_var_usages_scan(env_var_name)
 260      }
 261  
 262      /// Scan-based fallback for finding env var usages (O(n))
 263      fn find_env_var_usages_scan(&self, env_var_name: &str) -> Vec<EnvVarUsageLocation> {
 264          let mut locations = Vec::new();
 265          let mut seen_ranges = std::collections::HashSet::new();
 266  
 267          for reference in self.graph.direct_references() {
 268              if reference.name == env_var_name {
 269                  let range_key = (
 270                      reference.name_range.start.line,
 271                      reference.name_range.start.character,
 272                      reference.name_range.end.line,
 273                      reference.name_range.end.character,
 274                  );
 275                  if seen_ranges.insert(range_key) {
 276                      locations.push(EnvVarUsageLocation {
 277                          range: reference.name_range,
 278                          kind: UsageKind::DirectReference,
 279                          binding_name: None,
 280                      });
 281                  }
 282              }
 283          }
 284  
 285          for symbol in self.graph.symbols() {
 286              if let Some(ResolvedEnv::Variable(name)) = self.graph.resolve_to_env(symbol.id) {
 287                  if name == env_var_name {
 288                      let rename_range = if let Some(key_range) = symbol.destructured_key_range {
 289                          Some(key_range)
 290                      } else if symbol.name.as_str() == env_var_name {
 291                          Some(symbol.name_range)
 292                      } else {
 293                          None
 294                      };
 295  
 296                      if let Some(range) = rename_range {
 297                          let range_key = (
 298                              range.start.line,
 299                              range.start.character,
 300                              range.end.line,
 301                              range.end.character,
 302                          );
 303                          if seen_ranges.insert(range_key) {
 304                              locations.push(EnvVarUsageLocation {
 305                                  range,
 306                                  kind: UsageKind::BindingDeclaration,
 307                                  binding_name: Some(symbol.name.clone()),
 308                              });
 309                          }
 310                      }
 311                  }
 312              }
 313          }
 314  
 315          for usage in self.graph.usages() {
 316              if let Some(resolved) = self.graph.resolve_to_env(usage.symbol_id) {
 317                  match &resolved {
 318                      ResolvedEnv::Variable(name) if name == env_var_name => {
 319                          let range_key = (
 320                              usage.range.start.line,
 321                              usage.range.start.character,
 322                              usage.range.end.line,
 323                              usage.range.end.character,
 324                          );
 325                          if seen_ranges.insert(range_key) {
 326                              let binding_name = self
 327                                  .graph
 328                                  .get_symbol(usage.symbol_id)
 329                                  .map(|s| s.name.clone());
 330                              locations.push(EnvVarUsageLocation {
 331                                  range: usage.range,
 332                                  kind: UsageKind::BindingUsage,
 333                                  binding_name,
 334                              });
 335                          }
 336                      }
 337                      ResolvedEnv::Object(_) => {
 338                          if let Some(prop) = &usage.property_access {
 339                              if prop == env_var_name {
 340                                  let range = usage.property_access_range.unwrap_or(usage.range);
 341  
 342                                  let range_key = (
 343                                      range.start.line,
 344                                      range.start.character,
 345                                      range.end.line,
 346                                      range.end.character,
 347                                  );
 348                                  if seen_ranges.insert(range_key) {
 349                                      let binding_name = self
 350                                          .graph
 351                                          .get_symbol(usage.symbol_id)
 352                                          .map(|s| s.name.clone());
 353                                      locations.push(EnvVarUsageLocation {
 354                                          range,
 355                                          kind: UsageKind::PropertyAccess,
 356                                          binding_name,
 357                                      });
 358                                  }
 359                              }
 360                          }
 361                      }
 362                      _ => {}
 363                  }
 364              }
 365          }
 366  
 367          locations
 368      }
 369  
 370      /// Convert from binding_graph's EnvVarLocationKind to resolver's UsageKind
 371      fn convert_location_kind(kind: EnvVarLocationKind) -> UsageKind {
 372          match kind {
 373              EnvVarLocationKind::DirectReference => UsageKind::DirectReference,
 374              EnvVarLocationKind::BindingDeclaration => UsageKind::BindingDeclaration,
 375              EnvVarLocationKind::BindingUsage => UsageKind::BindingUsage,
 376              EnvVarLocationKind::PropertyAccess => UsageKind::PropertyAccess,
 377          }
 378      }
 379  
 380      pub fn all_env_vars(&self) -> Vec<CompactString> {
 381          let mut vars = std::collections::HashSet::new();
 382  
 383          for reference in self.graph.direct_references() {
 384              vars.insert(reference.name.clone());
 385          }
 386  
 387          for symbol in self.graph.symbols() {
 388              if let Some(ResolvedEnv::Variable(name)) = self.graph.resolve_to_env(symbol.id) {
 389                  vars.insert(name);
 390              }
 391          }
 392  
 393          vars.into_iter().collect()
 394      }
 395  
 396      pub fn get_symbol(&self, id: SymbolId) -> Option<&'a Symbol> {
 397          self.graph.get_symbol(id)
 398      }
 399  
 400      pub fn lookup_symbol(&self, name: &str, scope: ScopeId) -> Option<&'a Symbol> {
 401          self.graph.lookup_symbol(name, scope)
 402      }
 403  
 404      pub fn scope_at_position(&self, position: Position) -> ScopeId {
 405          self.graph.scope_at_position(position)
 406      }
 407  
 408      pub fn is_env_object(&self, symbol_id: SymbolId) -> bool {
 409          self.graph.resolves_to_env_object(symbol_id)
 410      }
 411  
 412      pub fn get_env_reference_cloned(&self, position: Position) -> Option<EnvReference> {
 413          if let Some(reference) = self.direct_reference_at_position(position) {
 414              return Some(reference.clone());
 415          }
 416  
 417          if let Some(usage) = self.graph.usage_at_position(position) {
 418              if let Some(property) = &usage.property_access {
 419                  if let Some(resolved) = self.graph.resolve_to_env(usage.symbol_id) {
 420                      if matches!(resolved, ResolvedEnv::Object(_)) {
 421                          return Some(EnvReference {
 422                              name: property.clone(),
 423                              full_range: usage.range,
 424                              name_range: usage.range,
 425                              access_type: crate::types::AccessType::Property,
 426                              has_default: false,
 427                              default_value: None,
 428                          });
 429                      }
 430                  }
 431              }
 432          }
 433  
 434          None
 435      }
 436  
 437      pub fn get_env_binding_cloned(&self, position: Position) -> Option<EnvBinding> {
 438          let hit = self.env_at_position(position)?;
 439  
 440          match hit {
 441              EnvHit::DirectReference(_) => None,
 442              EnvHit::ViaUsage { .. } => None,
 443  
 444              EnvHit::ViaSymbol { symbol, resolved } => {
 445                  let env_var_name = match &resolved {
 446                      ResolvedEnv::Variable(name) => name.clone(),
 447                      ResolvedEnv::Object(name) => name.clone(),
 448                  };
 449  
 450                  let kind = match &resolved {
 451                      ResolvedEnv::Variable(_) => BindingKind::Value,
 452                      ResolvedEnv::Object(_) => BindingKind::Object,
 453                  };
 454  
 455                  let scope = match self.graph.get_scope(symbol.scope) {
 456                      Some(s) => s,
 457                      None => {
 458                          // Data consistency error - this should never happen in normal operation
 459                          error!(
 460                              symbol_name = %symbol.name,
 461                              scope_id = ?symbol.scope,
 462                              "Symbol references non-existent scope - data consistency error"
 463                          );
 464                          return None;
 465                      }
 466                  };
 467  
 468                  Some(EnvBinding {
 469                      binding_name: symbol.name.clone(),
 470                      env_var_name,
 471                      binding_range: symbol.name_range,
 472                      declaration_range: symbol.declaration_range,
 473                      scope_range: scope.range,
 474                      is_valid: symbol.is_valid,
 475                      kind,
 476                      destructured_key_range: symbol.destructured_key_range,
 477                  })
 478              }
 479          }
 480      }
 481  
 482      pub fn get_binding_usage_cloned(&self, position: Position) -> Option<EnvBindingUsage> {
 483          let binding = self.binding_at_position(position)?;
 484          if !binding.is_usage {
 485              return None;
 486          }
 487          Some(binding.to_binding_usage())
 488      }
 489  
 490      pub fn get_binding_kind(&self, name: &str) -> Option<BindingKind> {
 491          for symbol in self.graph.symbols() {
 492              if symbol.name == name && symbol.is_valid {
 493                  if let Some(resolved) = self.graph.resolve_to_env(symbol.id) {
 494                      return Some(match resolved {
 495                          ResolvedEnv::Variable(_) => BindingKind::Value,
 496                          ResolvedEnv::Object(_) => BindingKind::Object,
 497                      });
 498                  }
 499              }
 500          }
 501          None
 502      }
 503  }
 504  
 505  #[derive(Debug, Clone)]
 506  pub struct EnvVarUsageLocation {
 507      pub range: Range,
 508  
 509      pub kind: UsageKind,
 510  
 511      pub binding_name: Option<CompactString>,
 512  }
 513  
 514  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 515  pub enum UsageKind {
 516      DirectReference,
 517  
 518      BindingDeclaration,
 519  
 520      BindingUsage,
 521  
 522      PropertyAccess,
 523  }
 524  
 525  #[cfg(test)]
 526  mod tests {
 527      use super::*;
 528      use crate::types::{AccessType, ScopeKind, SymbolKind, SymbolOrigin};
 529  
 530      fn make_range(start_line: u32, start_char: u32, end_line: u32, end_char: u32) -> Range {
 531          Range::new(
 532              Position::new(start_line, start_char),
 533              Position::new(end_line, end_char),
 534          )
 535      }
 536  
 537      #[test]
 538      fn test_env_hit_direct_reference_methods() {
 539          let mut graph = BindingGraph::new();
 540  
 541          let reference = EnvReference {
 542              name: "DATABASE_URL".into(),
 543              full_range: make_range(0, 0, 0, 30),
 544              name_range: make_range(0, 12, 0, 24),
 545              access_type: AccessType::Property,
 546              has_default: false,
 547              default_value: None,
 548          };
 549          graph.add_direct_reference(reference);
 550  
 551          let resolver = BindingResolver::new(&graph);
 552          let hit = resolver.env_at_position(Position::new(0, 15)).unwrap();
 553  
 554          assert_eq!(hit.env_var_name(), Some("DATABASE_URL".into()));
 555          assert_eq!(hit.canonical_name().as_str(), "DATABASE_URL");
 556          assert_eq!(hit.range(), make_range(0, 12, 0, 24));
 557          assert!(!hit.is_env_object());
 558          assert!(hit.binding_name().is_none());
 559      }
 560  
 561      #[test]
 562      fn test_env_hit_via_symbol_env_var() {
 563          let mut graph = BindingGraph::new();
 564  
 565          let _id = graph.add_symbol(Symbol {
 566              id: SymbolId::new(1).unwrap(),
 567              name: "dbUrl".into(),
 568              declaration_range: make_range(0, 0, 0, 40),
 569              name_range: make_range(0, 6, 0, 11),
 570              scope: ScopeId::root(),
 571              origin: SymbolOrigin::EnvVar {
 572                  name: "DATABASE_URL".into(),
 573              },
 574              kind: SymbolKind::Value,
 575              is_valid: true,
 576              destructured_key_range: None,
 577          });
 578          graph.rebuild_range_index();
 579  
 580          let resolver = BindingResolver::new(&graph);
 581          let hit = resolver.env_at_position(Position::new(0, 8)).unwrap();
 582  
 583          assert_eq!(hit.env_var_name(), Some("DATABASE_URL".into()));
 584          assert_eq!(hit.canonical_name().as_str(), "DATABASE_URL");
 585          assert_eq!(hit.range(), make_range(0, 6, 0, 11));
 586          assert!(!hit.is_env_object());
 587          assert_eq!(hit.binding_name(), Some(&"dbUrl".into()));
 588      }
 589  
 590      #[test]
 591      fn test_env_hit_via_symbol_env_object() {
 592          let mut graph = BindingGraph::new();
 593  
 594          let _id = graph.add_symbol(Symbol {
 595              id: SymbolId::new(1).unwrap(),
 596              name: "env".into(),
 597              declaration_range: make_range(0, 0, 0, 25),
 598              name_range: make_range(0, 6, 0, 9),
 599              scope: ScopeId::root(),
 600              origin: SymbolOrigin::EnvObject {
 601                  canonical_name: "process.env".into(),
 602              },
 603              kind: SymbolKind::EnvObject,
 604              is_valid: true,
 605              destructured_key_range: None,
 606          });
 607          graph.rebuild_range_index();
 608  
 609          let resolver = BindingResolver::new(&graph);
 610          let hit = resolver.env_at_position(Position::new(0, 7)).unwrap();
 611  
 612          assert!(hit.env_var_name().is_none());
 613          assert_eq!(hit.canonical_name().as_str(), "process.env");
 614          assert!(hit.is_env_object());
 615          assert_eq!(hit.binding_name(), Some(&"env".into()));
 616      }
 617  
 618      #[test]
 619      fn test_env_hit_via_usage() {
 620          let mut graph = BindingGraph::new();
 621  
 622          let id = graph.add_symbol(Symbol {
 623              id: SymbolId::new(1).unwrap(),
 624              name: "apiKey".into(),
 625              declaration_range: make_range(0, 0, 0, 35),
 626              name_range: make_range(0, 6, 0, 12),
 627              scope: ScopeId::root(),
 628              origin: SymbolOrigin::EnvVar {
 629                  name: "API_KEY".into(),
 630              },
 631              kind: SymbolKind::Value,
 632              is_valid: true,
 633              destructured_key_range: None,
 634          });
 635  
 636          graph.add_usage(SymbolUsage {
 637              symbol_id: id,
 638              range: make_range(2, 10, 2, 16),
 639              scope: ScopeId::root(),
 640              property_access: None,
 641              property_access_range: None,
 642          });
 643          graph.rebuild_range_index();
 644  
 645          let resolver = BindingResolver::new(&graph);
 646          let hit = resolver.env_at_position(Position::new(2, 12)).unwrap();
 647  
 648          assert!(matches!(hit, EnvHit::ViaUsage { .. }));
 649          assert_eq!(hit.env_var_name(), Some("API_KEY".into()));
 650          assert_eq!(hit.canonical_name().as_str(), "API_KEY");
 651          assert_eq!(hit.range(), make_range(2, 10, 2, 16));
 652          assert!(!hit.is_env_object());
 653          assert_eq!(hit.binding_name(), Some(&"apiKey".into()));
 654      }
 655  
 656      #[test]
 657      fn test_env_hit_via_usage_property_access() {
 658          let mut graph = BindingGraph::new();
 659  
 660          let id = graph.add_symbol(Symbol {
 661              id: SymbolId::new(1).unwrap(),
 662              name: "env".into(),
 663              declaration_range: make_range(0, 0, 0, 25),
 664              name_range: make_range(0, 6, 0, 9),
 665              scope: ScopeId::root(),
 666              origin: SymbolOrigin::EnvObject {
 667                  canonical_name: "process.env".into(),
 668              },
 669              kind: SymbolKind::EnvObject,
 670              is_valid: true,
 671              destructured_key_range: None,
 672          });
 673  
 674          graph.add_usage(SymbolUsage {
 675              symbol_id: id,
 676              range: make_range(1, 0, 1, 16),
 677              scope: ScopeId::root(),
 678              property_access: Some("DATABASE_URL".into()),
 679              property_access_range: Some(make_range(1, 4, 1, 16)),
 680          });
 681          graph.rebuild_range_index();
 682  
 683          let resolver = BindingResolver::new(&graph);
 684          let hit = resolver.env_at_position(Position::new(1, 8)).unwrap();
 685  
 686          assert!(matches!(hit, EnvHit::ViaUsage { .. }));
 687  
 688          assert_eq!(hit.env_var_name(), Some("DATABASE_URL".into()));
 689          assert!(!hit.is_env_object());
 690      }
 691  
 692      #[test]
 693      fn test_resolved_binding_to_env_binding() {
 694          let binding = ResolvedBinding {
 695              binding_name: "dbUrl".into(),
 696              env_var_name: "DATABASE_URL".into(),
 697              binding_range: make_range(0, 6, 0, 11),
 698              declaration_range: make_range(0, 0, 0, 40),
 699              kind: BindingKind::Value,
 700              is_usage: false,
 701          };
 702  
 703          let scope_range = make_range(0, 0, 10, 0);
 704          let env_binding = binding.to_env_binding(scope_range);
 705  
 706          assert_eq!(env_binding.binding_name, "dbUrl");
 707          assert_eq!(env_binding.env_var_name, "DATABASE_URL");
 708          assert_eq!(env_binding.binding_range, make_range(0, 6, 0, 11));
 709          assert_eq!(env_binding.declaration_range, make_range(0, 0, 0, 40));
 710          assert_eq!(env_binding.scope_range, scope_range);
 711          assert!(env_binding.is_valid);
 712          assert_eq!(env_binding.kind, BindingKind::Value);
 713          assert!(env_binding.destructured_key_range.is_none());
 714      }
 715  
 716      #[test]
 717      fn test_resolved_binding_to_binding_usage() {
 718          let binding = ResolvedBinding {
 719              binding_name: "apiKey".into(),
 720              env_var_name: "API_KEY".into(),
 721              binding_range: make_range(2, 10, 2, 16),
 722              declaration_range: make_range(0, 0, 0, 35),
 723              kind: BindingKind::Value,
 724              is_usage: true,
 725          };
 726  
 727          let usage = binding.to_binding_usage();
 728  
 729          assert_eq!(usage.name, "apiKey");
 730          assert_eq!(usage.env_var_name, "API_KEY");
 731          assert_eq!(usage.range, make_range(2, 10, 2, 16));
 732          assert_eq!(usage.declaration_range, make_range(0, 0, 0, 35));
 733      }
 734  
 735      #[test]
 736      fn test_resolved_binding_object_kind() {
 737          let binding = ResolvedBinding {
 738              binding_name: "env".into(),
 739              env_var_name: "process.env".into(),
 740              binding_range: make_range(0, 6, 0, 9),
 741              declaration_range: make_range(0, 0, 0, 25),
 742              kind: BindingKind::Object,
 743              is_usage: false,
 744          };
 745  
 746          let env_binding = binding.to_env_binding(make_range(0, 0, 100, 0));
 747          assert_eq!(env_binding.kind, BindingKind::Object);
 748      }
 749  
 750      #[test]
 751      fn test_resolve_direct_reference() {
 752          let mut graph = BindingGraph::new();
 753  
 754          graph.add_direct_reference(EnvReference {
 755              name: "DATABASE_URL".into(),
 756              full_range: make_range(0, 0, 0, 30),
 757              name_range: make_range(0, 12, 0, 24),
 758              access_type: crate::types::AccessType::Property,
 759              has_default: false,
 760              default_value: None,
 761          });
 762  
 763          let resolver = BindingResolver::new(&graph);
 764  
 765          let hit = resolver.env_at_position(Position::new(0, 15)).unwrap();
 766          assert!(matches!(hit, EnvHit::DirectReference(_)));
 767          assert_eq!(hit.env_var_name(), Some("DATABASE_URL".into()));
 768      }
 769  
 770      #[test]
 771      fn test_resolve_via_symbol() {
 772          let mut graph = BindingGraph::new();
 773  
 774          let _id = graph.add_symbol(Symbol {
 775              id: SymbolId::new(1).unwrap(),
 776              name: "dbUrl".into(),
 777              declaration_range: make_range(0, 0, 0, 40),
 778              name_range: make_range(0, 6, 0, 11),
 779              scope: ScopeId::root(),
 780              origin: SymbolOrigin::EnvVar {
 781                  name: "DATABASE_URL".into(),
 782              },
 783              kind: SymbolKind::Value,
 784              is_valid: true,
 785              destructured_key_range: None,
 786          });
 787          graph.rebuild_range_index();
 788  
 789          let resolver = BindingResolver::new(&graph);
 790  
 791          let hit = resolver.env_at_position(Position::new(0, 8)).unwrap();
 792          assert!(matches!(hit, EnvHit::ViaSymbol { .. }));
 793          assert_eq!(hit.env_var_name(), Some("DATABASE_URL".into()));
 794          assert_eq!(hit.binding_name(), Some(&"dbUrl".into()));
 795      }
 796  
 797      #[test]
 798      fn test_resolve_chain() {
 799          let mut graph = BindingGraph::new();
 800  
 801          let env_id = graph.add_symbol(Symbol {
 802              id: SymbolId::new(1).unwrap(),
 803              name: "env".into(),
 804              declaration_range: make_range(0, 0, 0, 25),
 805              name_range: make_range(0, 6, 0, 9),
 806              scope: ScopeId::root(),
 807              origin: SymbolOrigin::EnvObject {
 808                  canonical_name: "process.env".into(),
 809              },
 810              kind: SymbolKind::EnvObject,
 811              is_valid: true,
 812              destructured_key_range: None,
 813          });
 814  
 815          let config_id = graph.add_symbol(Symbol {
 816              id: SymbolId::new(2).unwrap(),
 817              name: "config".into(),
 818              declaration_range: make_range(1, 0, 1, 20),
 819              name_range: make_range(1, 6, 1, 12),
 820              scope: ScopeId::root(),
 821              origin: SymbolOrigin::Symbol { target: env_id },
 822              kind: SymbolKind::Variable,
 823              is_valid: true,
 824              destructured_key_range: None,
 825          });
 826  
 827          let _db_url_id = graph.add_symbol(Symbol {
 828              id: SymbolId::new(3).unwrap(),
 829              name: "DB_URL".into(),
 830              declaration_range: make_range(2, 0, 2, 30),
 831              name_range: make_range(2, 8, 2, 14),
 832              scope: ScopeId::root(),
 833              origin: SymbolOrigin::DestructuredProperty {
 834                  source: config_id,
 835                  key: "DB_URL".into(),
 836              },
 837              kind: SymbolKind::DestructuredProperty,
 838              is_valid: true,
 839              destructured_key_range: Some(make_range(2, 8, 2, 14)),
 840          });
 841          graph.rebuild_range_index();
 842  
 843          let resolver = BindingResolver::new(&graph);
 844  
 845          let hit = resolver.env_at_position(Position::new(2, 10)).unwrap();
 846          assert!(matches!(hit, EnvHit::ViaSymbol { .. }));
 847          assert_eq!(hit.env_var_name(), Some("DB_URL".into()));
 848      }
 849  
 850      #[test]
 851      fn test_binding_at_position_symbol() {
 852          let mut graph = BindingGraph::new();
 853  
 854          let _id = graph.add_symbol(Symbol {
 855              id: SymbolId::new(1).unwrap(),
 856              name: "dbUrl".into(),
 857              declaration_range: make_range(0, 0, 0, 40),
 858              name_range: make_range(0, 6, 0, 11),
 859              scope: ScopeId::root(),
 860              origin: SymbolOrigin::EnvVar {
 861                  name: "DATABASE_URL".into(),
 862              },
 863              kind: SymbolKind::Value,
 864              is_valid: true,
 865              destructured_key_range: None,
 866          });
 867          graph.rebuild_range_index();
 868  
 869          let resolver = BindingResolver::new(&graph);
 870          let binding = resolver.binding_at_position(Position::new(0, 8)).unwrap();
 871  
 872          assert_eq!(binding.binding_name, "dbUrl");
 873          assert_eq!(binding.env_var_name, "DATABASE_URL");
 874          assert!(!binding.is_usage);
 875          assert_eq!(binding.kind, BindingKind::Value);
 876      }
 877  
 878      #[test]
 879      fn test_binding_at_position_usage() {
 880          let mut graph = BindingGraph::new();
 881  
 882          let id = graph.add_symbol(Symbol {
 883              id: SymbolId::new(1).unwrap(),
 884              name: "apiKey".into(),
 885              declaration_range: make_range(0, 0, 0, 35),
 886              name_range: make_range(0, 6, 0, 12),
 887              scope: ScopeId::root(),
 888              origin: SymbolOrigin::EnvVar {
 889                  name: "API_KEY".into(),
 890              },
 891              kind: SymbolKind::Value,
 892              is_valid: true,
 893              destructured_key_range: None,
 894          });
 895  
 896          graph.add_usage(SymbolUsage {
 897              symbol_id: id,
 898              range: make_range(2, 10, 2, 16),
 899              scope: ScopeId::root(),
 900              property_access: None,
 901              property_access_range: None,
 902          });
 903          graph.rebuild_range_index();
 904  
 905          let resolver = BindingResolver::new(&graph);
 906          let binding = resolver.binding_at_position(Position::new(2, 12)).unwrap();
 907  
 908          assert_eq!(binding.binding_name, "apiKey");
 909          assert_eq!(binding.env_var_name, "API_KEY");
 910          assert!(binding.is_usage);
 911      }
 912  
 913      #[test]
 914      fn test_binding_at_position_direct_reference_returns_none() {
 915          let mut graph = BindingGraph::new();
 916  
 917          graph.add_direct_reference(EnvReference {
 918              name: "DATABASE_URL".into(),
 919              full_range: make_range(0, 0, 0, 30),
 920              name_range: make_range(0, 12, 0, 24),
 921              access_type: AccessType::Property,
 922              has_default: false,
 923              default_value: None,
 924          });
 925  
 926          let resolver = BindingResolver::new(&graph);
 927  
 928          assert!(resolver.binding_at_position(Position::new(0, 15)).is_none());
 929      }
 930  
 931      #[test]
 932      fn test_direct_reference_at_position() {
 933          let mut graph = BindingGraph::new();
 934  
 935          graph.add_direct_reference(EnvReference {
 936              name: "API_KEY".into(),
 937              full_range: make_range(0, 0, 0, 25),
 938              name_range: make_range(0, 12, 0, 19),
 939              access_type: AccessType::Property,
 940              has_default: false,
 941              default_value: None,
 942          });
 943  
 944          let resolver = BindingResolver::new(&graph);
 945  
 946          let ref_at_full_only = resolver.direct_reference_at_position(Position::new(0, 5));
 947          assert!(
 948              ref_at_full_only.is_none(),
 949              "Position in full_range but outside name_range should return None"
 950          );
 951  
 952          let ref_at_name = resolver.direct_reference_at_position(Position::new(0, 15));
 953          assert!(ref_at_name.is_some());
 954          assert_eq!(ref_at_name.unwrap().name, "API_KEY");
 955  
 956          let ref_outside = resolver.direct_reference_at_position(Position::new(1, 0));
 957          assert!(ref_outside.is_none());
 958      }
 959  
 960      #[test]
 961      fn test_env_at_position_outside_any_env_returns_none() {
 962          let graph = BindingGraph::new();
 963          let resolver = BindingResolver::new(&graph);
 964  
 965          assert!(resolver.env_at_position(Position::new(0, 0)).is_none());
 966          assert!(resolver.env_at_position(Position::new(100, 100)).is_none());
 967      }
 968  
 969      #[test]
 970      fn test_env_at_position_with_destructure_key() {
 971          let mut graph = BindingGraph::new();
 972  
 973          let _id = graph.add_symbol(Symbol {
 974              id: SymbolId::new(1).unwrap(),
 975              name: "apiKey".into(),
 976              declaration_range: make_range(0, 0, 0, 40),
 977              name_range: make_range(0, 18, 0, 24),
 978              scope: ScopeId::root(),
 979              origin: SymbolOrigin::EnvVar {
 980                  name: "API_KEY".into(),
 981              },
 982              kind: SymbolKind::DestructuredProperty,
 983              is_valid: true,
 984              destructured_key_range: Some(make_range(0, 8, 0, 15)),
 985          });
 986          graph.rebuild_range_index();
 987  
 988          let resolver = BindingResolver::new(&graph);
 989  
 990          let hit = resolver.env_at_position(Position::new(0, 10));
 991          assert!(hit.is_some());
 992          let hit = hit.unwrap();
 993          assert_eq!(hit.env_var_name(), Some("API_KEY".into()));
 994  
 995          let hit2 = resolver.env_at_position(Position::new(0, 20));
 996          assert!(hit2.is_some());
 997      }
 998  
 999      #[test]
1000      fn test_find_usages() {
1001          let mut graph = BindingGraph::new();
1002  
1003          graph.add_direct_reference(EnvReference {
1004              name: "API_KEY".into(),
1005              full_range: make_range(0, 0, 0, 20),
1006              name_range: make_range(0, 12, 0, 19),
1007              access_type: crate::types::AccessType::Property,
1008              has_default: false,
1009              default_value: None,
1010          });
1011  
1012          let id = graph.add_symbol(Symbol {
1013              id: SymbolId::new(1).unwrap(),
1014              name: "apiKey".into(),
1015              declaration_range: make_range(1, 0, 1, 35),
1016              name_range: make_range(1, 6, 1, 12),
1017              scope: ScopeId::root(),
1018              origin: SymbolOrigin::EnvVar {
1019                  name: "API_KEY".into(),
1020              },
1021              kind: SymbolKind::Value,
1022              is_valid: true,
1023              destructured_key_range: None,
1024          });
1025  
1026          graph.add_usage(SymbolUsage {
1027              symbol_id: id,
1028              range: make_range(2, 10, 2, 16),
1029              scope: ScopeId::root(),
1030              property_access: None,
1031              property_access_range: None,
1032          });
1033  
1034          let resolver = BindingResolver::new(&graph);
1035  
1036          let usages = resolver.find_env_var_usages("API_KEY");
1037  
1038          assert_eq!(usages.len(), 2);
1039  
1040          assert!(usages.iter().any(|u| u.kind == UsageKind::DirectReference));
1041          assert!(usages.iter().any(|u| u.kind == UsageKind::BindingUsage));
1042      }
1043  
1044      #[test]
1045      fn test_find_usages_destructured() {
1046          let mut graph = BindingGraph::new();
1047  
1048          graph.add_symbol(Symbol {
1049              id: SymbolId::new(1).unwrap(),
1050              name: "API_KEY".into(),
1051              declaration_range: make_range(0, 0, 0, 35),
1052              name_range: make_range(0, 8, 0, 15),
1053              scope: ScopeId::root(),
1054              origin: SymbolOrigin::EnvVar {
1055                  name: "API_KEY".into(),
1056              },
1057              kind: SymbolKind::DestructuredProperty,
1058              is_valid: true,
1059              destructured_key_range: None,
1060          });
1061  
1062          let resolver = BindingResolver::new(&graph);
1063  
1064          let usages = resolver.find_env_var_usages("API_KEY");
1065  
1066          assert_eq!(usages.len(), 1);
1067          assert!(usages
1068              .iter()
1069              .any(|u| u.kind == UsageKind::BindingDeclaration));
1070      }
1071  
1072      #[test]
1073      fn test_find_usages_destructured_with_rename() {
1074          let mut graph = BindingGraph::new();
1075  
1076          graph.add_symbol(Symbol {
1077              id: SymbolId::new(1).unwrap(),
1078              name: "apiKey".into(),
1079              declaration_range: make_range(0, 0, 0, 45),
1080              name_range: make_range(0, 18, 0, 24),
1081              scope: ScopeId::root(),
1082              origin: SymbolOrigin::EnvVar {
1083                  name: "API_KEY".into(),
1084              },
1085              kind: SymbolKind::DestructuredProperty,
1086              is_valid: true,
1087              destructured_key_range: Some(make_range(0, 8, 0, 15)),
1088          });
1089  
1090          let resolver = BindingResolver::new(&graph);
1091  
1092          let usages = resolver.find_env_var_usages("API_KEY");
1093  
1094          assert_eq!(usages.len(), 1);
1095          assert!(usages
1096              .iter()
1097              .any(|u| u.kind == UsageKind::BindingDeclaration));
1098  
1099          assert_eq!(usages[0].range, make_range(0, 8, 0, 15));
1100      }
1101  
1102      #[test]
1103      fn test_find_usages_property_access() {
1104          let mut graph = BindingGraph::new();
1105  
1106          let id = graph.add_symbol(Symbol {
1107              id: SymbolId::new(1).unwrap(),
1108              name: "env".into(),
1109              declaration_range: make_range(0, 0, 0, 25),
1110              name_range: make_range(0, 6, 0, 9),
1111              scope: ScopeId::root(),
1112              origin: SymbolOrigin::EnvObject {
1113                  canonical_name: "process.env".into(),
1114              },
1115              kind: SymbolKind::EnvObject,
1116              is_valid: true,
1117              destructured_key_range: None,
1118          });
1119  
1120          graph.add_usage(SymbolUsage {
1121              symbol_id: id,
1122              range: make_range(1, 0, 1, 16),
1123              scope: ScopeId::root(),
1124              property_access: Some("DATABASE_URL".into()),
1125              property_access_range: Some(make_range(1, 4, 1, 16)),
1126          });
1127  
1128          let resolver = BindingResolver::new(&graph);
1129          let usages = resolver.find_env_var_usages("DATABASE_URL");
1130  
1131          assert_eq!(usages.len(), 1);
1132          assert!(usages.iter().any(|u| u.kind == UsageKind::PropertyAccess));
1133  
1134          assert_eq!(usages[0].range, make_range(1, 4, 1, 16));
1135      }
1136  
1137      #[test]
1138      fn test_find_usages_property_access_no_range() {
1139          let mut graph = BindingGraph::new();
1140  
1141          let id = graph.add_symbol(Symbol {
1142              id: SymbolId::new(1).unwrap(),
1143              name: "env".into(),
1144              declaration_range: make_range(0, 0, 0, 25),
1145              name_range: make_range(0, 6, 0, 9),
1146              scope: ScopeId::root(),
1147              origin: SymbolOrigin::EnvObject {
1148                  canonical_name: "process.env".into(),
1149              },
1150              kind: SymbolKind::EnvObject,
1151              is_valid: true,
1152              destructured_key_range: None,
1153          });
1154  
1155          graph.add_usage(SymbolUsage {
1156              symbol_id: id,
1157              range: make_range(2, 5, 2, 16),
1158              scope: ScopeId::root(),
1159              property_access: Some("API_KEY".into()),
1160              property_access_range: None,
1161          });
1162  
1163          let resolver = BindingResolver::new(&graph);
1164          let usages = resolver.find_env_var_usages("API_KEY");
1165  
1166          assert_eq!(usages.len(), 1);
1167  
1168          assert_eq!(usages[0].range, make_range(2, 5, 2, 16));
1169      }
1170  
1171      #[test]
1172      fn test_find_usages_deduplication() {
1173          let mut graph = BindingGraph::new();
1174  
1175          graph.add_direct_reference(EnvReference {
1176              name: "DUPLICATE".into(),
1177              full_range: make_range(0, 0, 0, 20),
1178              name_range: make_range(0, 12, 0, 20),
1179              access_type: AccessType::Property,
1180              has_default: false,
1181              default_value: None,
1182          });
1183          graph.add_direct_reference(EnvReference {
1184              name: "DUPLICATE".into(),
1185              full_range: make_range(0, 0, 0, 20),
1186              name_range: make_range(0, 12, 0, 20),
1187              access_type: AccessType::Property,
1188              has_default: false,
1189              default_value: None,
1190          });
1191  
1192          let resolver = BindingResolver::new(&graph);
1193          let usages = resolver.find_env_var_usages("DUPLICATE");
1194  
1195          assert_eq!(usages.len(), 1);
1196      }
1197  
1198      #[test]
1199      fn test_find_usages_nonexistent_env_var() {
1200          let graph = BindingGraph::new();
1201          let resolver = BindingResolver::new(&graph);
1202  
1203          let usages = resolver.find_env_var_usages("NONEXISTENT");
1204          assert!(usages.is_empty());
1205      }
1206  
1207      #[test]
1208      fn test_all_env_vars() {
1209          let mut graph = BindingGraph::new();
1210  
1211          graph.add_direct_reference(EnvReference {
1212              name: "VAR_ONE".into(),
1213              full_range: make_range(0, 0, 0, 20),
1214              name_range: make_range(0, 12, 0, 19),
1215              access_type: AccessType::Property,
1216              has_default: false,
1217              default_value: None,
1218          });
1219  
1220          graph.add_direct_reference(EnvReference {
1221              name: "VAR_TWO".into(),
1222              full_range: make_range(1, 0, 1, 20),
1223              name_range: make_range(1, 12, 1, 19),
1224              access_type: AccessType::Property,
1225              has_default: false,
1226              default_value: None,
1227          });
1228  
1229          let _id = graph.add_symbol(Symbol {
1230              id: SymbolId::new(1).unwrap(),
1231              name: "varThree".into(),
1232              declaration_range: make_range(2, 0, 2, 35),
1233              name_range: make_range(2, 6, 2, 14),
1234              scope: ScopeId::root(),
1235              origin: SymbolOrigin::EnvVar {
1236                  name: "VAR_THREE".into(),
1237              },
1238              kind: SymbolKind::Value,
1239              is_valid: true,
1240              destructured_key_range: None,
1241          });
1242  
1243          let resolver = BindingResolver::new(&graph);
1244          let all_vars = resolver.all_env_vars();
1245  
1246          assert_eq!(all_vars.len(), 3);
1247          assert!(all_vars.iter().any(|v| v == "VAR_ONE"));
1248          assert!(all_vars.iter().any(|v| v == "VAR_TWO"));
1249          assert!(all_vars.iter().any(|v| v == "VAR_THREE"));
1250      }
1251  
1252      #[test]
1253      fn test_all_env_vars_empty() {
1254          let graph = BindingGraph::new();
1255          let resolver = BindingResolver::new(&graph);
1256  
1257          let all_vars = resolver.all_env_vars();
1258          assert!(all_vars.is_empty());
1259      }
1260  
1261      #[test]
1262      fn test_all_env_vars_excludes_objects() {
1263          let mut graph = BindingGraph::new();
1264  
1265          let _id = graph.add_symbol(Symbol {
1266              id: SymbolId::new(1).unwrap(),
1267              name: "env".into(),
1268              declaration_range: make_range(0, 0, 0, 25),
1269              name_range: make_range(0, 6, 0, 9),
1270              scope: ScopeId::root(),
1271              origin: SymbolOrigin::EnvObject {
1272                  canonical_name: "process.env".into(),
1273              },
1274              kind: SymbolKind::EnvObject,
1275              is_valid: true,
1276              destructured_key_range: None,
1277          });
1278  
1279          let resolver = BindingResolver::new(&graph);
1280          let all_vars = resolver.all_env_vars();
1281  
1282          assert!(all_vars.is_empty());
1283      }
1284  
1285      #[test]
1286      fn test_get_symbol() {
1287          let mut graph = BindingGraph::new();
1288  
1289          let id = graph.add_symbol(Symbol {
1290              id: SymbolId::new(1).unwrap(),
1291              name: "testVar".into(),
1292              declaration_range: make_range(0, 0, 0, 20),
1293              name_range: make_range(0, 6, 0, 13),
1294              scope: ScopeId::root(),
1295              origin: SymbolOrigin::EnvVar {
1296                  name: "TEST".into(),
1297              },
1298              kind: SymbolKind::Value,
1299              is_valid: true,
1300              destructured_key_range: None,
1301          });
1302  
1303          let resolver = BindingResolver::new(&graph);
1304  
1305          let symbol = resolver.get_symbol(id);
1306          assert!(symbol.is_some());
1307          assert_eq!(symbol.unwrap().name, "testVar");
1308  
1309          let fake_id = SymbolId::new(999).unwrap();
1310          assert!(resolver.get_symbol(fake_id).is_none());
1311      }
1312  
1313      #[test]
1314      fn test_lookup_symbol() {
1315          let mut graph = BindingGraph::new();
1316  
1317          let _id = graph.add_symbol(Symbol {
1318              id: SymbolId::new(1).unwrap(),
1319              name: "myVar".into(),
1320              declaration_range: make_range(0, 0, 0, 20),
1321              name_range: make_range(0, 6, 0, 11),
1322              scope: ScopeId::root(),
1323              origin: SymbolOrigin::EnvVar {
1324                  name: "MY_VAR".into(),
1325              },
1326              kind: SymbolKind::Value,
1327              is_valid: true,
1328              destructured_key_range: None,
1329          });
1330  
1331          let resolver = BindingResolver::new(&graph);
1332  
1333          let symbol = resolver.lookup_symbol("myVar", ScopeId::root());
1334          assert!(symbol.is_some());
1335          assert_eq!(symbol.unwrap().name, "myVar");
1336  
1337          let not_found = resolver.lookup_symbol("nonexistent", ScopeId::root());
1338          assert!(not_found.is_none());
1339      }
1340  
1341      #[test]
1342      fn test_scope_at_position() {
1343          let graph = BindingGraph::new();
1344          let resolver = BindingResolver::new(&graph);
1345  
1346          let scope = resolver.scope_at_position(Position::new(5, 10));
1347          assert_eq!(scope, ScopeId::root());
1348      }
1349  
1350      #[test]
1351      fn test_is_env_object() {
1352          let mut graph = BindingGraph::new();
1353  
1354          let env_obj_id = graph.add_symbol(Symbol {
1355              id: SymbolId::new(1).unwrap(),
1356              name: "env".into(),
1357              declaration_range: make_range(0, 0, 0, 25),
1358              name_range: make_range(0, 6, 0, 9),
1359              scope: ScopeId::root(),
1360              origin: SymbolOrigin::EnvObject {
1361                  canonical_name: "process.env".into(),
1362              },
1363              kind: SymbolKind::EnvObject,
1364              is_valid: true,
1365              destructured_key_range: None,
1366          });
1367  
1368          let var_id = graph.add_symbol(Symbol {
1369              id: SymbolId::new(2).unwrap(),
1370              name: "dbUrl".into(),
1371              declaration_range: make_range(1, 0, 1, 35),
1372              name_range: make_range(1, 6, 1, 11),
1373              scope: ScopeId::root(),
1374              origin: SymbolOrigin::EnvVar {
1375                  name: "DATABASE_URL".into(),
1376              },
1377              kind: SymbolKind::Value,
1378              is_valid: true,
1379              destructured_key_range: None,
1380          });
1381  
1382          let resolver = BindingResolver::new(&graph);
1383  
1384          assert!(resolver.is_env_object(env_obj_id));
1385          assert!(!resolver.is_env_object(var_id));
1386      }
1387  
1388      #[test]
1389      fn test_get_env_reference_cloned() {
1390          let mut graph = BindingGraph::new();
1391  
1392          graph.add_direct_reference(EnvReference {
1393              name: "TEST_VAR".into(),
1394              full_range: make_range(0, 0, 0, 25),
1395              name_range: make_range(0, 12, 0, 20),
1396              access_type: AccessType::Property,
1397              has_default: true,
1398              default_value: Some("default".into()),
1399          });
1400  
1401          let resolver = BindingResolver::new(&graph);
1402  
1403          let cloned = resolver.get_env_reference_cloned(Position::new(0, 15));
1404          assert!(cloned.is_some());
1405          let cloned = cloned.unwrap();
1406          assert_eq!(cloned.name, "TEST_VAR");
1407          assert!(cloned.has_default);
1408          assert_eq!(cloned.default_value, Some("default".into()));
1409      }
1410  
1411      #[test]
1412      fn test_get_env_reference_cloned_from_property_access() {
1413          let mut graph = BindingGraph::new();
1414  
1415          let id = graph.add_symbol(Symbol {
1416              id: SymbolId::new(1).unwrap(),
1417              name: "env".into(),
1418              declaration_range: make_range(0, 0, 0, 25),
1419              name_range: make_range(0, 6, 0, 9),
1420              scope: ScopeId::root(),
1421              origin: SymbolOrigin::EnvObject {
1422                  canonical_name: "process.env".into(),
1423              },
1424              kind: SymbolKind::EnvObject,
1425              is_valid: true,
1426              destructured_key_range: None,
1427          });
1428  
1429          graph.add_usage(SymbolUsage {
1430              symbol_id: id,
1431              range: make_range(1, 0, 1, 10),
1432              scope: ScopeId::root(),
1433              property_access: Some("MY_VAR".into()),
1434              property_access_range: Some(make_range(1, 4, 1, 10)),
1435          });
1436          graph.rebuild_range_index();
1437  
1438          let resolver = BindingResolver::new(&graph);
1439  
1440          let cloned = resolver.get_env_reference_cloned(Position::new(1, 6));
1441          assert!(cloned.is_some());
1442          let cloned = cloned.unwrap();
1443          assert_eq!(cloned.name, "MY_VAR");
1444          assert_eq!(cloned.access_type, AccessType::Property);
1445      }
1446  
1447      #[test]
1448      fn test_get_env_reference_cloned_returns_none_for_binding() {
1449          let mut graph = BindingGraph::new();
1450  
1451          let _id = graph.add_symbol(Symbol {
1452              id: SymbolId::new(1).unwrap(),
1453              name: "dbUrl".into(),
1454              declaration_range: make_range(0, 0, 0, 40),
1455              name_range: make_range(0, 6, 0, 11),
1456              scope: ScopeId::root(),
1457              origin: SymbolOrigin::EnvVar {
1458                  name: "DATABASE_URL".into(),
1459              },
1460              kind: SymbolKind::Value,
1461              is_valid: true,
1462              destructured_key_range: None,
1463          });
1464  
1465          let resolver = BindingResolver::new(&graph);
1466  
1467          let cloned = resolver.get_env_reference_cloned(Position::new(0, 8));
1468          assert!(cloned.is_none());
1469      }
1470  
1471      #[test]
1472      fn test_get_env_binding_cloned() {
1473          let mut graph = BindingGraph::new();
1474  
1475          let scope_id = graph.add_scope(crate::types::Scope {
1476              id: ScopeId::new(1).unwrap(),
1477              parent: Some(ScopeId::root()),
1478              range: make_range(0, 0, 10, 0),
1479              kind: ScopeKind::Function,
1480          });
1481  
1482          let _id = graph.add_symbol(Symbol {
1483              id: SymbolId::new(1).unwrap(),
1484              name: "myVar".into(),
1485              declaration_range: make_range(1, 0, 1, 35),
1486              name_range: make_range(1, 6, 1, 11),
1487              scope: scope_id,
1488              origin: SymbolOrigin::EnvVar {
1489                  name: "MY_VAR".into(),
1490              },
1491              kind: SymbolKind::Value,
1492              is_valid: true,
1493              destructured_key_range: Some(make_range(1, 6, 1, 11)),
1494          });
1495          graph.rebuild_range_index();
1496  
1497          let resolver = BindingResolver::new(&graph);
1498  
1499          let binding = resolver.get_env_binding_cloned(Position::new(1, 8));
1500          assert!(binding.is_some());
1501          let binding = binding.unwrap();
1502          assert_eq!(binding.binding_name, "myVar");
1503          assert_eq!(binding.env_var_name, "MY_VAR");
1504          assert_eq!(binding.scope_range, make_range(0, 0, 10, 0));
1505          assert!(binding.is_valid);
1506          assert_eq!(
1507              binding.destructured_key_range,
1508              Some(make_range(1, 6, 1, 11))
1509          );
1510      }
1511  
1512      #[test]
1513      fn test_get_env_binding_cloned_returns_none_for_direct_ref() {
1514          let mut graph = BindingGraph::new();
1515  
1516          graph.add_direct_reference(EnvReference {
1517              name: "DIRECT".into(),
1518              full_range: make_range(0, 0, 0, 20),
1519              name_range: make_range(0, 12, 0, 18),
1520              access_type: AccessType::Property,
1521              has_default: false,
1522              default_value: None,
1523          });
1524  
1525          let resolver = BindingResolver::new(&graph);
1526  
1527          let binding = resolver.get_env_binding_cloned(Position::new(0, 15));
1528          assert!(binding.is_none());
1529      }
1530  
1531      #[test]
1532      fn test_get_binding_usage_cloned() {
1533          let mut graph = BindingGraph::new();
1534  
1535          let id = graph.add_symbol(Symbol {
1536              id: SymbolId::new(1).unwrap(),
1537              name: "usedVar".into(),
1538              declaration_range: make_range(0, 0, 0, 30),
1539              name_range: make_range(0, 6, 0, 13),
1540              scope: ScopeId::root(),
1541              origin: SymbolOrigin::EnvVar {
1542                  name: "USED_VAR".into(),
1543              },
1544              kind: SymbolKind::Value,
1545              is_valid: true,
1546              destructured_key_range: None,
1547          });
1548  
1549          graph.add_usage(SymbolUsage {
1550              symbol_id: id,
1551              range: make_range(2, 5, 2, 12),
1552              scope: ScopeId::root(),
1553              property_access: None,
1554              property_access_range: None,
1555          });
1556          graph.rebuild_range_index();
1557  
1558          let resolver = BindingResolver::new(&graph);
1559  
1560          let usage = resolver.get_binding_usage_cloned(Position::new(2, 8));
1561          assert!(usage.is_some());
1562          let usage = usage.unwrap();
1563          assert_eq!(usage.name, "usedVar");
1564          assert_eq!(usage.env_var_name, "USED_VAR");
1565          assert_eq!(usage.range, make_range(2, 5, 2, 12));
1566      }
1567  
1568      #[test]
1569      fn test_get_binding_usage_cloned_returns_none_for_declaration() {
1570          let mut graph = BindingGraph::new();
1571  
1572          let _id = graph.add_symbol(Symbol {
1573              id: SymbolId::new(1).unwrap(),
1574              name: "declVar".into(),
1575              declaration_range: make_range(0, 0, 0, 30),
1576              name_range: make_range(0, 6, 0, 13),
1577              scope: ScopeId::root(),
1578              origin: SymbolOrigin::EnvVar {
1579                  name: "DECL_VAR".into(),
1580              },
1581              kind: SymbolKind::Value,
1582              is_valid: true,
1583              destructured_key_range: None,
1584          });
1585  
1586          let resolver = BindingResolver::new(&graph);
1587  
1588          let usage = resolver.get_binding_usage_cloned(Position::new(0, 8));
1589          assert!(usage.is_none());
1590      }
1591  
1592      #[test]
1593      fn test_get_binding_kind() {
1594          let mut graph = BindingGraph::new();
1595  
1596          let _id1 = graph.add_symbol(Symbol {
1597              id: SymbolId::new(1).unwrap(),
1598              name: "valueBinding".into(),
1599              declaration_range: make_range(0, 0, 0, 40),
1600              name_range: make_range(0, 6, 0, 18),
1601              scope: ScopeId::root(),
1602              origin: SymbolOrigin::EnvVar {
1603                  name: "VALUE".into(),
1604              },
1605              kind: SymbolKind::Value,
1606              is_valid: true,
1607              destructured_key_range: None,
1608          });
1609  
1610          let _id2 = graph.add_symbol(Symbol {
1611              id: SymbolId::new(2).unwrap(),
1612              name: "objectBinding".into(),
1613              declaration_range: make_range(1, 0, 1, 30),
1614              name_range: make_range(1, 6, 1, 19),
1615              scope: ScopeId::root(),
1616              origin: SymbolOrigin::EnvObject {
1617                  canonical_name: "process.env".into(),
1618              },
1619              kind: SymbolKind::EnvObject,
1620              is_valid: true,
1621              destructured_key_range: None,
1622          });
1623  
1624          let _id3 = graph.add_symbol(Symbol {
1625              id: SymbolId::new(3).unwrap(),
1626              name: "invalidBinding".into(),
1627              declaration_range: make_range(2, 0, 2, 40),
1628              name_range: make_range(2, 6, 2, 20),
1629              scope: ScopeId::root(),
1630              origin: SymbolOrigin::EnvVar {
1631                  name: "INVALID".into(),
1632              },
1633              kind: SymbolKind::Value,
1634              is_valid: false,
1635              destructured_key_range: None,
1636          });
1637  
1638          let resolver = BindingResolver::new(&graph);
1639  
1640          let kind1 = resolver.get_binding_kind("valueBinding");
1641          assert!(kind1.is_some());
1642          assert_eq!(kind1.unwrap(), BindingKind::Value);
1643  
1644          let kind2 = resolver.get_binding_kind("objectBinding");
1645          assert!(kind2.is_some());
1646          assert_eq!(kind2.unwrap(), BindingKind::Object);
1647  
1648          let kind3 = resolver.get_binding_kind("invalidBinding");
1649          assert!(kind3.is_none());
1650  
1651          let kind4 = resolver.get_binding_kind("nonexistent");
1652          assert!(kind4.is_none());
1653      }
1654  
1655      #[test]
1656      fn test_usage_kind_equality() {
1657          assert_eq!(UsageKind::DirectReference, UsageKind::DirectReference);
1658          assert_eq!(UsageKind::BindingDeclaration, UsageKind::BindingDeclaration);
1659          assert_eq!(UsageKind::BindingUsage, UsageKind::BindingUsage);
1660          assert_eq!(UsageKind::PropertyAccess, UsageKind::PropertyAccess);
1661  
1662          assert_ne!(UsageKind::DirectReference, UsageKind::BindingUsage);
1663          assert_ne!(UsageKind::BindingDeclaration, UsageKind::PropertyAccess);
1664      }
1665  
1666      #[test]
1667      fn test_usage_kind_copy() {
1668          let kind = UsageKind::DirectReference;
1669          let copied = kind;
1670          assert_eq!(kind, copied);
1671      }
1672  
1673      #[test]
1674      fn test_env_var_usage_location_clone() {
1675          let location = EnvVarUsageLocation {
1676              range: make_range(0, 0, 0, 10),
1677              kind: UsageKind::DirectReference,
1678              binding_name: Some("myBinding".into()),
1679          };
1680  
1681          let cloned = location.clone();
1682          assert_eq!(cloned.range, location.range);
1683          assert_eq!(cloned.kind, location.kind);
1684          assert_eq!(cloned.binding_name, location.binding_name);
1685      }
1686  
1687      #[test]
1688      fn test_env_var_usage_location_debug() {
1689          let location = EnvVarUsageLocation {
1690              range: make_range(1, 5, 1, 15),
1691              kind: UsageKind::BindingUsage,
1692              binding_name: None,
1693          };
1694  
1695          let debug_str = format!("{:?}", location);
1696          assert!(debug_str.contains("EnvVarUsageLocation"));
1697          assert!(debug_str.contains("BindingUsage"));
1698      }
1699  
1700      #[test]
1701      fn test_complex_chain_resolution() {
1702          let mut graph = BindingGraph::new();
1703  
1704          let env_id = graph.add_symbol(Symbol {
1705              id: SymbolId::new(1).unwrap(),
1706              name: "env".into(),
1707              declaration_range: make_range(0, 0, 0, 25),
1708              name_range: make_range(0, 6, 0, 9),
1709              scope: ScopeId::root(),
1710              origin: SymbolOrigin::EnvObject {
1711                  canonical_name: "process.env".into(),
1712              },
1713              kind: SymbolKind::EnvObject,
1714              is_valid: true,
1715              destructured_key_range: None,
1716          });
1717  
1718          let config_id = graph.add_symbol(Symbol {
1719              id: SymbolId::new(2).unwrap(),
1720              name: "config".into(),
1721              declaration_range: make_range(1, 0, 1, 20),
1722              name_range: make_range(1, 6, 1, 12),
1723              scope: ScopeId::root(),
1724              origin: SymbolOrigin::Symbol { target: env_id },
1725              kind: SymbolKind::Variable,
1726              is_valid: true,
1727              destructured_key_range: None,
1728          });
1729  
1730          let settings_id = graph.add_symbol(Symbol {
1731              id: SymbolId::new(3).unwrap(),
1732              name: "settings".into(),
1733              declaration_range: make_range(2, 0, 2, 25),
1734              name_range: make_range(2, 6, 2, 14),
1735              scope: ScopeId::root(),
1736              origin: SymbolOrigin::Symbol { target: config_id },
1737              kind: SymbolKind::Variable,
1738              is_valid: true,
1739              destructured_key_range: None,
1740          });
1741  
1742          let _db_id = graph.add_symbol(Symbol {
1743              id: SymbolId::new(4).unwrap(),
1744              name: "DB_URL".into(),
1745              declaration_range: make_range(3, 0, 3, 30),
1746              name_range: make_range(3, 8, 3, 14),
1747              scope: ScopeId::root(),
1748              origin: SymbolOrigin::DestructuredProperty {
1749                  source: settings_id,
1750                  key: "DB_URL".into(),
1751              },
1752              kind: SymbolKind::DestructuredProperty,
1753              is_valid: true,
1754              destructured_key_range: Some(make_range(3, 8, 3, 14)),
1755          });
1756          graph.rebuild_range_index();
1757  
1758          let resolver = BindingResolver::new(&graph);
1759  
1760          let hit = resolver.env_at_position(Position::new(3, 10)).unwrap();
1761          assert_eq!(hit.env_var_name(), Some("DB_URL".into()));
1762  
1763          let hit2 = resolver.env_at_position(Position::new(2, 10)).unwrap();
1764          assert!(hit2.is_env_object());
1765          assert_eq!(hit2.canonical_name().as_str(), "process.env");
1766      }
1767  
1768      #[test]
1769      fn test_multiple_env_vars_same_document() {
1770          let mut graph = BindingGraph::new();
1771  
1772          graph.add_direct_reference(EnvReference {
1773              name: "VAR_A".into(),
1774              full_range: make_range(0, 0, 0, 20),
1775              name_range: make_range(0, 12, 0, 17),
1776              access_type: AccessType::Property,
1777              has_default: false,
1778              default_value: None,
1779          });
1780  
1781          graph.add_direct_reference(EnvReference {
1782              name: "VAR_B".into(),
1783              full_range: make_range(1, 0, 1, 20),
1784              name_range: make_range(1, 12, 1, 17),
1785              access_type: AccessType::Property,
1786              has_default: false,
1787              default_value: None,
1788          });
1789  
1790          let _id = graph.add_symbol(Symbol {
1791              id: SymbolId::new(1).unwrap(),
1792              name: "varC".into(),
1793              declaration_range: make_range(2, 0, 2, 30),
1794              name_range: make_range(2, 6, 2, 10),
1795              scope: ScopeId::root(),
1796              origin: SymbolOrigin::EnvVar {
1797                  name: "VAR_C".into(),
1798              },
1799              kind: SymbolKind::Value,
1800              is_valid: true,
1801              destructured_key_range: None,
1802          });
1803          graph.rebuild_range_index();
1804  
1805          let resolver = BindingResolver::new(&graph);
1806  
1807          let hit_a = resolver.env_at_position(Position::new(0, 15)).unwrap();
1808          assert_eq!(hit_a.canonical_name().as_str(), "VAR_A");
1809  
1810          let hit_b = resolver.env_at_position(Position::new(1, 15)).unwrap();
1811          assert_eq!(hit_b.canonical_name().as_str(), "VAR_B");
1812  
1813          let hit_c = resolver.env_at_position(Position::new(2, 8)).unwrap();
1814          assert_eq!(hit_c.canonical_name().as_str(), "VAR_C");
1815  
1816          let all_vars = resolver.all_env_vars();
1817          assert_eq!(all_vars.len(), 3);
1818      }
1819  
1820      #[test]
1821      fn test_binding_with_usage_at_different_scopes() {
1822          let mut graph = BindingGraph::new();
1823  
1824          let inner_scope = graph.add_scope(crate::types::Scope {
1825              id: ScopeId::new(1).unwrap(),
1826              parent: Some(ScopeId::root()),
1827              range: make_range(1, 0, 5, 0),
1828              kind: ScopeKind::Block,
1829          });
1830  
1831          let id = graph.add_symbol(Symbol {
1832              id: SymbolId::new(1).unwrap(),
1833              name: "rootVar".into(),
1834              declaration_range: make_range(0, 0, 0, 30),
1835              name_range: make_range(0, 6, 0, 13),
1836              scope: ScopeId::root(),
1837              origin: SymbolOrigin::EnvVar {
1838                  name: "ROOT_VAR".into(),
1839              },
1840              kind: SymbolKind::Value,
1841              is_valid: true,
1842              destructured_key_range: None,
1843          });
1844  
1845          graph.add_usage(SymbolUsage {
1846              symbol_id: id,
1847              range: make_range(2, 5, 2, 12),
1848              scope: inner_scope,
1849              property_access: None,
1850              property_access_range: None,
1851          });
1852          graph.rebuild_range_index();
1853  
1854          let resolver = BindingResolver::new(&graph);
1855  
1856          let hit = resolver.env_at_position(Position::new(2, 8)).unwrap();
1857          assert!(matches!(hit, EnvHit::ViaUsage { .. }));
1858          assert_eq!(hit.env_var_name(), Some("ROOT_VAR".into()));
1859      }
1860  
1861      #[test]
1862      fn test_binding_object_with_property_access_and_destructuring() {
1863          let mut graph = BindingGraph::new();
1864  
1865          let env_id = graph.add_symbol(Symbol {
1866              id: SymbolId::new(1).unwrap(),
1867              name: "env".into(),
1868              declaration_range: make_range(0, 0, 0, 25),
1869              name_range: make_range(0, 6, 0, 9),
1870              scope: ScopeId::root(),
1871              origin: SymbolOrigin::EnvObject {
1872                  canonical_name: "process.env".into(),
1873              },
1874              kind: SymbolKind::EnvObject,
1875              is_valid: true,
1876              destructured_key_range: None,
1877          });
1878  
1879          graph.add_usage(SymbolUsage {
1880              symbol_id: env_id,
1881              range: make_range(1, 0, 1, 11),
1882              scope: ScopeId::root(),
1883              property_access: Some("VAR_ONE".into()),
1884              property_access_range: Some(make_range(1, 4, 1, 11)),
1885          });
1886  
1887          let _var_two_id = graph.add_symbol(Symbol {
1888              id: SymbolId::new(2).unwrap(),
1889              name: "VAR_TWO".into(),
1890              declaration_range: make_range(2, 0, 2, 25),
1891              name_range: make_range(2, 8, 2, 15),
1892              scope: ScopeId::root(),
1893              origin: SymbolOrigin::DestructuredProperty {
1894                  source: env_id,
1895                  key: "VAR_TWO".into(),
1896              },
1897              kind: SymbolKind::DestructuredProperty,
1898              is_valid: true,
1899              destructured_key_range: Some(make_range(2, 8, 2, 15)),
1900          });
1901  
1902          let resolver = BindingResolver::new(&graph);
1903  
1904          let var_one_usages = resolver.find_env_var_usages("VAR_ONE");
1905          assert_eq!(var_one_usages.len(), 1);
1906          assert_eq!(var_one_usages[0].kind, UsageKind::PropertyAccess);
1907  
1908          let var_two_usages = resolver.find_env_var_usages("VAR_TWO");
1909          assert_eq!(var_two_usages.len(), 1);
1910          assert_eq!(var_two_usages[0].kind, UsageKind::BindingDeclaration);
1911      }
1912  }