diff --git a/.gitignore b/.gitignore index 939e49c96d..3ad45c9fbd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ tarpaulin-report.html /docs/tmp_snippets .vscode .DS_Store +*PLAN* diff --git a/Cargo.lock b/Cargo.lock index 9cd5ec4232..7a2f70bbd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -518,6 +518,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "countme" version = "3.0.1" @@ -738,6 +750,12 @@ dependencies = [ "log", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "equivalent" version = "1.0.2" @@ -886,6 +904,7 @@ dependencies = [ "fe-common", "fe-driver", "fe-hir", + "fe-parser", "fe-test-utils", "if_chain", "indexmap", @@ -918,6 +937,7 @@ dependencies = [ "fe-hir", "fe-hir-analysis", "fe-parser", + "fe-semantic-query", "fe-test-utils", "futures", "futures-batch", @@ -930,7 +950,7 @@ dependencies = [ "tower", "tracing", "tracing-subscriber", - "tracing-tree", + "tracing-tree 0.4.0", "url", ] @@ -966,14 +986,39 @@ dependencies = [ "url", ] +[[package]] +name = "fe-semantic-query" +version = "0.1.0" +dependencies = [ + "async-lsp", + "dir-test", + "fe-common", + "fe-driver", + "fe-hir", + "fe-hir-analysis", + "fe-parser", + "fe-test-utils", + "rustc-hash 2.1.1", + "salsa", + "tracing", + "url", +] + [[package]] name = "fe-test-utils" version = "0.1.0" dependencies = [ + "codespan-reporting", + "fe-common", + "fe-hir", + "fe-parser", "insta", + "rstest", + "rstest_reuse", + "termcolor", "tracing", "tracing-subscriber", - "tracing-tree", + "tracing-tree 0.3.1", "url", ] @@ -1146,6 +1191,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.3.3" @@ -1359,6 +1415,7 @@ version = "1.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" dependencies = [ + "console", "once_cell", "similar", ] @@ -1797,6 +1854,15 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.97" @@ -1829,6 +1895,36 @@ dependencies = [ "once_cell", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + [[package]] name = "rayon" version = "1.11.0" @@ -1902,6 +1998,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "rowan" version = "0.16.1" @@ -1914,6 +2016,47 @@ dependencies = [ "text-size", ] +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.105", + "unicode-ident", +] + +[[package]] +name = "rstest_reuse" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88530b681abe67924d42cca181d070e3ac20e0740569441a9e35a7cedd2b34a4" +dependencies = [ + "quote", + "rand", + "rustc_version", + "syn 2.0.105", +] + [[package]] name = "rust-embed" version = "8.7.2" @@ -2304,7 +2447,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.3", "once_cell", "rustix 1.0.8", "windows-sys 0.59.0", @@ -2481,6 +2624,7 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2536,6 +2680,18 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tracing-tree" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b56c62d2c80033cb36fae448730a2f2ef99410fe3ecbffc916681a32f6807dbe" +dependencies = [ + "nu-ansi-term 0.50.1", + "tracing-core", + "tracing-log", + "tracing-subscriber", +] + [[package]] name = "tracing-tree" version = "0.4.0" @@ -2995,6 +3151,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/crates/fe/src/main.rs b/crates/fe/src/main.rs index 7907e16389..a4a3b98a4d 100644 --- a/crates/fe/src/main.rs +++ b/crates/fe/src/main.rs @@ -1,3 +1,5 @@ +#![allow(clippy::print_stdout, clippy::print_stderr)] + mod check; mod tree; diff --git a/crates/hir-analysis/Cargo.toml b/crates/hir-analysis/Cargo.toml index 3710e8704f..7bef4e02b9 100644 --- a/crates/hir-analysis/Cargo.toml +++ b/crates/hir-analysis/Cargo.toml @@ -30,6 +30,7 @@ common.workspace = true test-utils.workspace = true hir.workspace = true url.workspace = true +parser.workspace = true [dev-dependencies] ascii_tree = "0.1" diff --git a/crates/hir-analysis/src/lib.rs b/crates/hir-analysis/src/lib.rs index 37fbc30c5d..8d5accf632 100644 --- a/crates/hir-analysis/src/lib.rs +++ b/crates/hir-analysis/src/lib.rs @@ -8,6 +8,7 @@ pub trait HirAnalysisDb: HirDb {} #[salsa::db] impl HirAnalysisDb for T where T: HirDb {} +pub mod lookup; pub mod name_resolution; pub mod ty; diff --git a/crates/hir-analysis/src/lookup.rs b/crates/hir-analysis/src/lookup.rs new file mode 100644 index 0000000000..030ad1b85d --- /dev/null +++ b/crates/hir-analysis/src/lookup.rs @@ -0,0 +1,387 @@ +use hir::hir_def::{scope_graph::ScopeId, ItemKind, PathId, TopLevelMod}; +use hir::SpannedHirDb; + +use crate::name_resolution::{resolve_with_policy, DomainPreference, PathRes}; +use crate::ty::{func_def::FuncDef, trait_resolution::PredicateListId}; +use crate::{diagnostics::SpannedHirAnalysisDb, HirAnalysisDb}; + +/// Generic semantic identity at a source offset. +/// This is compiler-facing and independent of any IDE layer types. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SymbolKey<'db> { + Scope(hir::hir_def::scope_graph::ScopeId<'db>), + EnumVariant(hir::hir_def::EnumVariant<'db>), + FuncParam(hir::hir_def::ItemKind<'db>, u16), + Method(FuncDef<'db>), + Local( + hir::hir_def::item::Func<'db>, + crate::ty::ty_check::BindingKey<'db>, + ), +} + +fn enclosing_func<'db>( + db: &'db dyn SpannedHirDb, + mut scope: ScopeId<'db>, +) -> Option> { + for _ in 0..16 { + if let Some(ItemKind::Func(f)) = scope.to_item() { + return Some(f); + } + scope = scope.parent(db)?; + } + None +} + +fn map_path_res<'db>(db: &'db dyn HirAnalysisDb, res: PathRes<'db>) -> Option> { + match res { + PathRes::EnumVariant(v) => Some(SymbolKey::EnumVariant(v.variant)), + PathRes::FuncParam(item, idx) => Some(SymbolKey::FuncParam(item, idx)), + PathRes::Method(..) => { + crate::name_resolution::method_func_def_from_res(&res).map(SymbolKey::Method) + } + _ => res.as_scope(db).map(SymbolKey::Scope), + } +} + +/// Resolve the semantic identity for a given occurrence payload. +/// This is the single source of truth for occurrence interpretation. +/// Returns multiple identities for ambiguous cases (e.g., ambiguous imports). +pub fn identity_for_occurrence<'db>( + db: &'db dyn SpannedHirAnalysisDb, + top_mod: TopLevelMod<'db>, + occ: &hir::source_index::OccurrencePayload<'db>, +) -> Vec> { + use hir::source_index::OccurrencePayload as OP; + + match *occ { + OP::ItemHeaderName { scope, .. } => match scope { + hir::hir_def::scope_graph::ScopeId::Item(ItemKind::Func(f)) => { + if let Some(fd) = crate::ty::func_def::lower_func(db, f) { + if fd.is_method(db) { + return vec![SymbolKey::Method(fd)]; + } + } + vec![SymbolKey::Scope(scope)] + } + hir::hir_def::scope_graph::ScopeId::FuncParam(item, idx) => { + vec![SymbolKey::FuncParam(item, idx)] + } + hir::hir_def::scope_graph::ScopeId::Variant(v) => vec![SymbolKey::EnumVariant(v)], + other => vec![SymbolKey::Scope(other)], + }, + OP::MethodName { + scope, + receiver, + ident, + body, + .. + } => { + if let Some(func) = enclosing_func(db, body.scope()) { + use crate::name_resolution::method_selection::{ + select_method_candidate, MethodSelectionError, + }; + use crate::ty::{canonical::Canonical, ty_check::check_func_body}; + + let (_diags, typed) = check_func_body(db, func).clone(); + let recv_ty = typed.expr_prop(db, receiver).ty; + let assumptions = PredicateListId::empty_list(db); + + match select_method_candidate( + db, + Canonical::new(db, recv_ty), + ident, + scope, + assumptions, + ) { + Ok(cand) => { + use crate::name_resolution::method_selection::MethodCandidate; + let fd = match cand { + MethodCandidate::InherentMethod(fd) => fd, + MethodCandidate::TraitMethod(tm) + | MethodCandidate::NeedsConfirmation(tm) => tm.method.0, + }; + vec![SymbolKey::Method(fd)] + } + Err(MethodSelectionError::AmbiguousInherentMethod(methods)) => { + methods.iter().map(|fd| SymbolKey::Method(*fd)).collect() + } + Err(MethodSelectionError::AmbiguousTraitMethod(traits)) => traits + .iter() + .filter_map(|trait_def| { + trait_def + .methods(db) + .get(&ident) + .map(|tm| SymbolKey::Method(tm.0)) + }) + .collect(), + Err(_) => vec![], + } + } else { + vec![] + } + } + OP::PathExprSeg { + body, + expr, + scope, + path, + seg_idx, + .. + } => { + if let Some(func) = enclosing_func(db, body.scope()) { + if let Some(bkey) = crate::ty::ty_check::expr_binding_key_for_expr(db, func, expr) { + return vec![match bkey { + crate::ty::ty_check::BindingKey::FuncParam(f, idx) => { + SymbolKey::FuncParam(ItemKind::Func(f), idx) + } + other => SymbolKey::Local(func, other), + }]; + } + } + let seg_path: PathId<'db> = path.segment(db, seg_idx).unwrap_or(path); + if let Ok(res) = resolve_with_policy( + db, + seg_path, + scope, + PredicateListId::empty_list(db), + DomainPreference::Either, + ) { + if let Some(identity) = map_path_res(db, res) { + vec![identity] + } else { + vec![] + } + } else { + // This is where the key insight comes: if resolve_with_policy fails, + // it might be due to ambiguous imports. Let's check for that case. + find_ambiguous_candidates_for_path_seg(db, top_mod, scope, path, seg_idx) + } + } + OP::PathPatSeg { body, pat, .. } => { + if let Some(func) = enclosing_func(db, body.scope()) { + vec![SymbolKey::Local( + func, + crate::ty::ty_check::BindingKey::LocalPat(pat), + )] + } else { + vec![] + } + } + OP::FieldAccessName { + body, + ident, + receiver, + .. + } => { + if let Some(func) = enclosing_func(db, body.scope()) { + let (_d, typed) = crate::ty::ty_check::check_func_body(db, func).clone(); + let recv_ty = typed.expr_prop(db, receiver).ty; + if let Some(sc) = + crate::ty::ty_check::RecordLike::from_ty(recv_ty).record_field_scope(db, ident) + { + return vec![SymbolKey::Scope(sc)]; + } + } + vec![] + } + OP::PatternLabelName { + scope, + ident, + constructor_path, + .. + } => { + if let Some(p) = constructor_path { + if let Ok(res) = resolve_with_policy( + db, + p, + scope, + PredicateListId::empty_list(db), + DomainPreference::Either, + ) { + use crate::name_resolution::PathRes as PR; + let target = match res { + PR::EnumVariant(v) => crate::ty::ty_check::RecordLike::from_variant(v) + .record_field_scope(db, ident), + PR::Ty(ty) => crate::ty::ty_check::RecordLike::from_ty(ty) + .record_field_scope(db, ident), + PR::TyAlias(_, ty) => crate::ty::ty_check::RecordLike::from_ty(ty) + .record_field_scope(db, ident), + _ => None, + }; + if let Some(target) = target { + return vec![SymbolKey::Scope(target)]; + } + } + } + vec![] + } + OP::UseAliasName { scope, ident, .. } => { + let ing = top_mod.ingot(db); + let (_d, imports) = crate::name_resolution::resolve_imports(db, ing); + if let Some(named) = imports.named_resolved.get(&scope) { + if let Some(bucket) = named.get(&ident) { + if let Ok(nr) = bucket + .pick_any(&[ + crate::name_resolution::NameDomain::TYPE, + crate::name_resolution::NameDomain::VALUE, + ]) + .as_ref() + { + match nr.kind { + crate::name_resolution::NameResKind::Scope(sc) => { + return vec![SymbolKey::Scope(sc)]; + } + crate::name_resolution::NameResKind::Prim(_) => {} + } + } + } + } + vec![] + } + OP::UsePathSeg { + scope, + path, + seg_idx, + .. + } => { + // Convert UsePathId to PathId for resolution using same logic as PathExprSeg + if let Some(path_id) = convert_use_path_to_path_id(db, path, seg_idx) { + if let Ok(res) = resolve_with_policy( + db, + path_id, + scope, + PredicateListId::empty_list(db), + DomainPreference::Either, + ) { + if let Some(identity) = map_path_res(db, res) { + vec![identity] + } else { + vec![] + } + } else { + // Try ambiguous candidates like regular PathSeg + find_ambiguous_candidates_for_path_seg(db, top_mod, scope, path_id, 0) + } + } else { + vec![] + } + } + OP::PathSeg { + scope, + path, + seg_idx, + .. + } => { + let seg_path: PathId<'db> = path.segment(db, seg_idx).unwrap_or(path); + if let Ok(res) = resolve_with_policy( + db, + seg_path, + scope, + PredicateListId::empty_list(db), + DomainPreference::Either, + ) { + if let Some(identity) = map_path_res(db, res) { + vec![identity] + } else { + vec![] + } + } else { + // For regular PathSeg, also check for ambiguous imports + find_ambiguous_candidates_for_path_seg(db, top_mod, scope, path, seg_idx) + } + } + } +} + +/// Convert UsePathId to PathId for resolution +fn convert_use_path_to_path_id<'db>( + db: &'db dyn SpannedHirAnalysisDb, + use_path: hir::hir_def::UsePathId<'db>, + up_to_seg_idx: usize, +) -> Option> { + // Build PathId by converting each UsePathSegment up to seg_idx to PathKind::Ident + let mut path_id: Option> = None; + + for (i, seg) in use_path.data(db).iter().enumerate() { + if i > up_to_seg_idx { + break; + } + + if let Some(hir::hir_def::UsePathSegment::Ident(ident)) = seg.to_opt() { + path_id = Some(match path_id { + Some(parent) => parent.push_ident(db, ident), + None => hir::hir_def::PathId::from_ident(db, ident), + }); + } else { + // Skip invalid segments + continue; + } + } + + path_id +} + +/// Find multiple candidates for ambiguous import cases +fn find_ambiguous_candidates_for_path_seg<'db>( + db: &'db dyn SpannedHirAnalysisDb, + top_mod: TopLevelMod<'db>, + scope: ScopeId<'db>, + path: PathId<'db>, + seg_idx: usize, +) -> Vec> { + use crate::name_resolution::NameDomain; + + // Get the identifier from the path segment + let seg_path = path.segment(db, seg_idx).unwrap_or(path); + let Some(ident) = seg_path.as_ident(db) else { + return vec![]; + }; + + // Check imports for this scope - walk up the scope hierarchy to find where imports are resolved + let ing = top_mod.ingot(db); + let (_diags, imports) = crate::name_resolution::resolve_imports(db, ing); + + // Try current scope first, then walk up the hierarchy + let mut current_scope = Some(scope); + let (_import_scope, named) = loop { + let Some(sc) = current_scope else { + return vec![]; + }; + + if let Some(named) = imports.named_resolved.get(&sc) { + break (sc, named); + } + + // Walk up to parent scope + current_scope = sc.parent(db); + }; + let Some(bucket) = named.get(&ident) else { + return vec![]; + }; + + let mut candidates = Vec::new(); + + // Check both TYPE and VALUE domains for multiple resolutions + for domain in [NameDomain::TYPE, NameDomain::VALUE] { + match bucket.pick(domain) { + Ok(name_res) => { + if let crate::name_resolution::NameResKind::Scope(sc) = name_res.kind { + candidates.push(SymbolKey::Scope(sc)); + } + } + Err(crate::name_resolution::NameResolutionError::Ambiguous(ambiguous_candidates)) => { + // This is exactly what we want for ambiguous imports! + for name_res in ambiguous_candidates { + if let crate::name_resolution::NameResKind::Scope(sc) = name_res.kind { + candidates.push(SymbolKey::Scope(sc)); + } + } + } + Err(_) => { + // Other errors (like NotFound) are ignored + } + } + } + + candidates +} diff --git a/crates/hir-analysis/src/name_resolution/method_api.rs b/crates/hir-analysis/src/name_resolution/method_api.rs new file mode 100644 index 0000000000..061dd46168 --- /dev/null +++ b/crates/hir-analysis/src/name_resolution/method_api.rs @@ -0,0 +1,20 @@ +use crate::{ + name_resolution::{method_selection::MethodCandidate, PathRes}, + ty::func_def::FuncDef, +}; + +/// Extract the underlying function definition for a resolved method PathRes. +/// Returns None if the PathRes is not a method. +pub fn method_func_def_from_res<'db>( + res: &crate::name_resolution::PathRes<'db>, +) -> Option> { + match res { + PathRes::Method(_, cand) => match cand { + MethodCandidate::InherentMethod(fd) => Some(*fd), + MethodCandidate::TraitMethod(tm) | MethodCandidate::NeedsConfirmation(tm) => { + Some(tm.method.0) + } + }, + _ => None, + } +} diff --git a/crates/hir-analysis/src/name_resolution/mod.rs b/crates/hir-analysis/src/name_resolution/mod.rs index 713d7906ed..75dc509d86 100644 --- a/crates/hir-analysis/src/name_resolution/mod.rs +++ b/crates/hir-analysis/src/name_resolution/mod.rs @@ -1,9 +1,12 @@ pub mod diagnostics; mod import_resolver; +// locals_api was removed; use ty::ty_check directly +mod method_api; pub(crate) mod method_selection; mod name_resolver; mod path_resolver; +mod policy; pub(crate) mod traits_in_scope; mod visibility_checker; @@ -14,10 +17,15 @@ pub use name_resolver::{ EarlyNameQueryId, NameDerivation, NameDomain, NameRes, NameResBucket, NameResKind, NameResolutionError, QueryDirective, }; +// NOTE: `resolve_path` is the low-level resolver that still requires callers to +// pass a boolean domain hint. Prefer `resolve_with_policy` for new call-sites +// to avoid boolean flags at API boundaries. +pub use method_api::method_func_def_from_res; pub use path_resolver::{ find_associated_type, resolve_ident_to_bucket, resolve_name_res, resolve_path, resolve_path_with_observer, PathRes, PathResError, PathResErrorKind, ResolvedVariant, }; +pub use policy::{resolve_with_policy, DomainPreference}; use tracing::debug; pub use traits_in_scope::available_traits_in_scope; pub(crate) use visibility_checker::is_scope_visible_from; diff --git a/crates/hir-analysis/src/name_resolution/path_resolver.rs b/crates/hir-analysis/src/name_resolution/path_resolver.rs index fecddab778..347a64c39c 100644 --- a/crates/hir-analysis/src/name_resolution/path_resolver.rs +++ b/crates/hir-analysis/src/name_resolution/path_resolver.rs @@ -2,8 +2,8 @@ use common::indexmap::IndexMap; use either::Either; use hir::{ hir_def::{ - scope_graph::ScopeId, Enum, EnumVariant, GenericParamOwner, IdentId, ImplTrait, ItemKind, - Partial, PathId, PathKind, Trait, TypeBound, TypeId, VariantKind, + scope_graph::ScopeId, Body, Enum, EnumVariant, ExprId, GenericParamOwner, IdentId, + ImplTrait, ItemKind, Partial, PathId, PathKind, Trait, TypeBound, TypeId, VariantKind, }, span::DynLazySpan, }; @@ -272,6 +272,77 @@ impl<'db> PathResError<'db> { } } +impl<'db> PathResError<'db> { + /// Compute an anchored DynLazySpan for a path error occurring in a body expression, + /// using centralized path anchor selection rules. + pub fn anchor_dyn_span_for_body_expr( + &self, + db: &'db dyn HirAnalysisDb, + body: Body<'db>, + expr: ExprId, + full_path: PathId<'db>, + ) -> DynLazySpan<'db> { + let seg_idx = self.failed_at.segment_index(db); + let path_lazy = expr.span(body).into_path_expr().path(); + let view = hir::path_view::HirPathAdapter::new(db, full_path); + let anchor = match &self.kind { + PathResErrorKind::ArgNumMismatch { .. } + | PathResErrorKind::ArgKindMisMatch { .. } + | PathResErrorKind::ArgTypeMismatch { .. } => hir::path_anchor::PathAnchor { + seg_idx, + kind: hir::path_anchor::PathAnchorKind::Segment, + }, + _ => hir::path_anchor::AnchorPicker::pick_invalid_segment(&view, seg_idx), + }; + hir::path_anchor::map_path_anchor_to_dyn_lazy(path_lazy, anchor) + } + + /// Anchor a path error occurring in a pattern path (e.g., `Foo::Bar` in patterns). + pub fn anchor_dyn_span_for_body_path_pat( + &self, + db: &'db dyn HirAnalysisDb, + body: Body<'db>, + pat: hir::hir_def::PatId, + full_path: PathId<'db>, + ) -> DynLazySpan<'db> { + let seg_idx = self.failed_at.segment_index(db); + let path_lazy = pat.span(body).into_path_pat().path(); + let view = hir::path_view::HirPathAdapter::new(db, full_path); + let anchor = hir::path_anchor::AnchorPicker::pick_invalid_segment(&view, seg_idx); + hir::path_anchor::map_path_anchor_to_dyn_lazy(path_lazy, anchor) + } + + /// Anchor a path error occurring in a tuple pattern path (e.g., `Variant(..)`). + pub fn anchor_dyn_span_for_body_path_tuple_pat( + &self, + db: &'db dyn HirAnalysisDb, + body: Body<'db>, + pat: hir::hir_def::PatId, + full_path: PathId<'db>, + ) -> DynLazySpan<'db> { + let seg_idx = self.failed_at.segment_index(db); + let path_lazy = pat.span(body).into_path_tuple_pat().path(); + let view = hir::path_view::HirPathAdapter::new(db, full_path); + let anchor = hir::path_anchor::AnchorPicker::pick_invalid_segment(&view, seg_idx); + hir::path_anchor::map_path_anchor_to_dyn_lazy(path_lazy, anchor) + } + + /// Anchor a path error occurring in a record pattern path (e.g., `Variant { .. }`). + pub fn anchor_dyn_span_for_body_record_pat( + &self, + db: &'db dyn HirAnalysisDb, + body: Body<'db>, + pat: hir::hir_def::PatId, + full_path: PathId<'db>, + ) -> DynLazySpan<'db> { + let seg_idx = self.failed_at.segment_index(db); + let path_lazy = pat.span(body).into_record_pat().path(); + let view = hir::path_view::HirPathAdapter::new(db, full_path); + let anchor = hir::path_anchor::AnchorPicker::pick_invalid_segment(&view, seg_idx); + hir::path_anchor::map_path_anchor_to_dyn_lazy(path_lazy, anchor) + } +} + fn func_not_found_err<'db>( span: DynLazySpan<'db>, ident: IdentId<'db>, diff --git a/crates/hir-analysis/src/name_resolution/policy.rs b/crates/hir-analysis/src/name_resolution/policy.rs new file mode 100644 index 0000000000..bcbf0476e8 --- /dev/null +++ b/crates/hir-analysis/src/name_resolution/policy.rs @@ -0,0 +1,37 @@ +use hir::hir_def::scope_graph::ScopeId; +use hir::hir_def::PathId; + +use crate::ty::trait_resolution::PredicateListId; +use crate::{ + name_resolution::{resolve_path, PathRes, PathResError}, + HirAnalysisDb, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DomainPreference { + Value, + Type, + Either, +} + +/// Thin facade over `resolve_path` that hides the boolean tail-domain flag +/// and allows callers to express intent declaratively. +pub fn resolve_with_policy<'db>( + db: &'db dyn HirAnalysisDb, + path: PathId<'db>, + scope: ScopeId<'db>, + assumptions: PredicateListId<'db>, + pref: DomainPreference, +) -> Result, PathResError<'db>> { + match pref { + DomainPreference::Value => resolve_path(db, path, scope, assumptions, true), + DomainPreference::Type => resolve_path(db, path, scope, assumptions, false), + DomainPreference::Either => { + // Try value first, then type. + match resolve_path(db, path, scope, assumptions, true) { + ok @ Ok(_) => ok, + Err(_) => resolve_path(db, path, scope, assumptions, false), + } + } + } +} diff --git a/crates/hir-analysis/src/ty/def_analysis.rs b/crates/hir-analysis/src/ty/def_analysis.rs index 2182003edf..5f84d471d7 100644 --- a/crates/hir-analysis/src/ty/def_analysis.rs +++ b/crates/hir-analysis/src/ty/def_analysis.rs @@ -21,7 +21,7 @@ use super::{ diagnostics::{ImplDiag, TraitConstraintDiag, TraitLowerDiag, TyDiagCollection, TyLowerDiag}, func_def::FuncDef, method_cmp::compare_impl_method, - method_table::probe_method, + method_table::probe_method, // TODO(deprecate): prefer method facade helpers for listing methods normalize::normalize_ty, trait_def::{ingot_trait_env, Implementor, TraitDef}, trait_lower::{lower_trait, lower_trait_ref, TraitRefLowerError}, @@ -35,7 +35,9 @@ use super::{ visitor::{walk_ty, TyVisitor}, }; use crate::{ - name_resolution::{diagnostics::PathResDiag, resolve_path, ExpectedPathKind, PathRes}, + name_resolution::{ + diagnostics::PathResDiag, resolve_with_policy, DomainPreference, ExpectedPathKind, PathRes, + }, ty::{ adt_def::AdtDef, binder::Binder, @@ -547,12 +549,12 @@ fn check_param_defined_in_parent<'db>( let parent_scope = scope.parent_item(db)?.scope(); let path = PathId::from_ident(db, name); - match resolve_path( + match resolve_with_policy( db, path, parent_scope, PredicateListId::empty_list(db), - false, + DomainPreference::Type, ) { Ok(r @ PathRes::Ty(ty)) if ty.is_param(db) => { Some(TyLowerDiag::GenericParamAlreadyDefinedInParent { @@ -1517,8 +1519,13 @@ fn find_const_ty_param<'db>( scope: ScopeId<'db>, ) -> Option> { let path = PathId::from_ident(db, ident); - let Ok(PathRes::Ty(ty)) = resolve_path(db, path, scope, PredicateListId::empty_list(db), true) - else { + let Ok(PathRes::Ty(ty)) = resolve_with_policy( + db, + path, + scope, + PredicateListId::empty_list(db), + DomainPreference::Value, + ) else { return None; }; match ty.data(db) { diff --git a/crates/hir-analysis/src/ty/simplified_pattern.rs b/crates/hir-analysis/src/ty/simplified_pattern.rs index a783424f77..54970c29c4 100644 --- a/crates/hir-analysis/src/ty/simplified_pattern.rs +++ b/crates/hir-analysis/src/ty/simplified_pattern.rs @@ -3,7 +3,7 @@ //! This module contains the conversion logic from HIR patterns to a simplified //! representation that's easier to work with during pattern analysis. -use crate::name_resolution::{resolve_path, PathRes, ResolvedVariant}; +use crate::name_resolution::{resolve_with_policy, DomainPreference, PathRes, ResolvedVariant}; use crate::ty::ty_def::TyId; use crate::HirAnalysisDb; use hir::hir_def::{ @@ -191,7 +191,13 @@ impl<'db> SimplifiedPattern<'db> { return None; }; - match resolve_path(db, *path_id, scope, PredicateListId::empty_list(db), true) { + match resolve_with_policy( + db, + *path_id, + scope, + PredicateListId::empty_list(db), + DomainPreference::Value, + ) { Ok(PathRes::EnumVariant(variant)) => { let ty = expected_ty.unwrap_or(variant.ty); let ctor = ConstructorKind::Variant(variant.variant, ty); diff --git a/crates/hir-analysis/src/ty/trait_lower.rs b/crates/hir-analysis/src/ty/trait_lower.rs index 28ae6d4c70..ff30649a4a 100644 --- a/crates/hir-analysis/src/ty/trait_lower.rs +++ b/crates/hir-analysis/src/ty/trait_lower.rs @@ -18,7 +18,7 @@ use super::{ ty_lower::{collect_generic_params, lower_hir_ty}, }; use crate::{ - name_resolution::{resolve_path, PathRes, PathResError}, + name_resolution::{resolve_with_policy, DomainPreference, PathRes, PathResError}, ty::{ func_def::lower_func, trait_resolution::constraint::collect_constraints, @@ -127,7 +127,7 @@ pub(crate) fn lower_trait_ref<'db>( return Err(TraitRefLowerError::Ignored); }; - match resolve_path(db, path, scope, assumptions, false) { + match resolve_with_policy(db, path, scope, assumptions, DomainPreference::Type) { Ok(PathRes::Trait(t)) => { let mut args = t.args(db).clone(); args[0] = self_ty; diff --git a/crates/hir-analysis/src/ty/ty_check/expr.rs b/crates/hir-analysis/src/ty/ty_check/expr.rs index 1060a200d1..ed23a94ba7 100644 --- a/crates/hir-analysis/src/ty/ty_check/expr.rs +++ b/crates/hir-analysis/src/ty/ty_check/expr.rs @@ -13,7 +13,7 @@ use crate::{ name_resolution::{ diagnostics::PathResDiag, is_scope_visible_from, - method_selection::{select_method_candidate, MethodCandidate, MethodSelectionError}, + method_selection::{MethodCandidate, MethodSelectionError}, resolve_name_res, resolve_query, EarlyNameQueryId, ExpectedPathKind, NameDomain, NameResBucket, PathRes, QueryDirective, }, @@ -321,7 +321,7 @@ impl<'db> TyChecker<'db> { let assumptions = self.env.assumptions(); let canonical_r_ty = Canonicalized::new(self.db, receiver_prop.ty); - let candidate = match select_method_candidate( + let candidate = match crate::name_resolution::method_selection::select_method_candidate( self.db, canonical_r_ty.value, method_name, @@ -437,11 +437,8 @@ impl<'db> TyChecker<'db> { match self.resolve_path(*path, true, span.clone().path()) { Ok(r) => ResolvedPathInBody::Reso(r), Err(err) => { - let span = expr - .span(self.body()) - .into_path_expr() - .path() - .segment(err.failed_at.segment_index(self.db)); + // Use centralized path anchor selection instead of a fixed segment span. + let span = err.anchor_dyn_span_for_body_expr(self.db, self.body(), expr, *path); let expected_kind = if matches!(self.parent_expr(), Some(Expr::Call(..))) { ExpectedPathKind::Function @@ -449,7 +446,7 @@ impl<'db> TyChecker<'db> { ExpectedPathKind::Value }; - if let Some(diag) = err.into_diag(self.db, *path, span.into(), expected_kind) { + if let Some(diag) = err.into_diag(self.db, *path, span, expected_kind) { self.push_diag(diag) } ResolvedPathInBody::Invalid @@ -557,7 +554,10 @@ impl<'db> TyChecker<'db> { }; ExprProp::new(self.table.instantiate_to_term(method_ty), true) } - PathRes::Mod(_) | PathRes::FuncParam(..) => todo!(), + PathRes::Mod(_) | PathRes::FuncParam(..) => { + // Not a value in expression position + ExprProp::invalid(self.db) + } }, } } diff --git a/crates/hir-analysis/src/ty/ty_check/mod.rs b/crates/hir-analysis/src/ty/ty_check/mod.rs index 8ea8680efb..71ae3780d4 100644 --- a/crates/hir-analysis/src/ty/ty_check/mod.rs +++ b/crates/hir-analysis/src/ty/ty_check/mod.rs @@ -38,6 +38,10 @@ use crate::{ ty::ty_def::{inference_keys, TyFlags}, HirAnalysisDb, }; +use hir::{ + path_anchor::{map_path_anchor_to_dyn_lazy, AnchorPicker}, + path_view::HirPathAdapter, +}; #[salsa::tracked(return_ref)] pub fn check_func_body<'db>( @@ -248,6 +252,11 @@ impl<'db> TyChecker<'db> { } } + // TODO(deprecate): Legacy internal resolver used by TyChecker that still + // takes `resolve_tail_as_value`. Prefer `name_resolution::resolve_with_policy` + // at call sites when possible. This function remains to handle TyChecker- + // specific concerns (observer visibility reporting, instantiation) and + // should be slimmed to delegate to the policy wrapper over time. fn resolve_path( &mut self, path: PathId<'db>, @@ -278,9 +287,12 @@ impl<'db> TyChecker<'db> { }; if let Some((path, deriv_span)) = invisible { - let span = span.clone().segment(path.segment_index(self.db)).ident(); + let seg_idx = path.segment_index(self.db); + let view = HirPathAdapter::new(self.db, path); + let anchor = AnchorPicker::pick_visibility_error(&view, seg_idx); + let anchored = map_path_anchor_to_dyn_lazy(span.clone(), anchor); let ident = path.ident(self.db); - let diag = PathResDiag::Invisible(span.into(), *ident.unwrap(), deriv_span); + let diag = PathResDiag::Invisible(anchored, *ident.unwrap(), deriv_span); self.diags.push(diag.into()); } @@ -376,6 +388,54 @@ impl<'db> TraitMethod<'db> { } } +/// Stable identity for a local binding within a function body: either a local pattern +/// or a function parameter at index. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum BindingKey<'db> { + LocalPat(hir::hir_def::PatId), + FuncParam(hir::hir_def::item::Func<'db>, u16), +} + +/// Get the binding key for an expression that references a local binding, if any. +#[salsa::tracked] +pub(crate) fn expr_binding_key_for_expr<'db>( + db: &'db dyn HirAnalysisDb, + func: hir::hir_def::item::Func<'db>, + expr: hir::hir_def::ExprId, +) -> Option> { + let (_diags, typed) = check_func_body(db, func).clone(); + let prop = typed.expr_prop(db, expr); + let binding = prop.binding()?; + match binding { + crate::ty::ty_check::env::LocalBinding::Local { pat, .. } => { + Some(BindingKey::LocalPat(pat)) + } + crate::ty::ty_check::env::LocalBinding::Param { idx, .. } => { + Some(BindingKey::FuncParam(func, idx as u16)) + } + } +} + +/// Return the declaration name span for a binding key in the given function. +#[salsa::tracked] +pub fn binding_def_span_in_func<'db>( + db: &'db dyn HirAnalysisDb, + func: hir::hir_def::item::Func<'db>, + key: BindingKey<'db>, +) -> Option> { + match key { + BindingKey::LocalPat(pat) => { + let (_d, typed) = check_func_body(db, func).clone(); + let body = typed.body?; + Some(pat.span(body).into()) + } + BindingKey::FuncParam(f, idx) => { + // param belongs to this function + Some(f.span().params().param(idx as usize).name().into()) + } + } +} + struct TyCheckerFinalizer<'db> { db: &'db dyn HirAnalysisDb, body: TypedBody<'db>, diff --git a/crates/hir-analysis/src/ty/ty_check/pat.rs b/crates/hir-analysis/src/ty/ty_check/pat.rs index 9e5d92c876..45386fee03 100644 --- a/crates/hir-analysis/src/ty/ty_check/pat.rs +++ b/crates/hir-analysis/src/ty/ty_check/pat.rs @@ -220,7 +220,20 @@ impl<'db> TyChecker<'db> { TyId::invalid(self.db, InvalidCause::Other) } - Err(_) => TyId::invalid(self.db, InvalidCause::Other), + Err(err) => { + // Anchor the failing segment using centralized picker. + let span = + err.anchor_dyn_span_for_body_path_pat(self.db, self.body(), pat, *path); + if let Some(diag) = err.into_diag( + self.db, + *path, + span, + crate::name_resolution::ExpectedPathKind::Value, + ) { + self.push_diag(diag); + } + TyId::invalid(self.db, InvalidCause::Other) + } } } } @@ -283,7 +296,19 @@ impl<'db> TyChecker<'db> { return TyId::invalid(self.db, InvalidCause::Other); } }, - Err(_) => return TyId::invalid(self.db, InvalidCause::Other), + Err(err) => { + let span = + err.anchor_dyn_span_for_body_path_tuple_pat(self.db, self.body(), pat, *path); + if let Some(diag) = err.into_diag( + self.db, + *path, + span, + crate::name_resolution::ExpectedPathKind::Value, + ) { + self.push_diag(diag); + } + return TyId::invalid(self.db, InvalidCause::Other); + } }; let expected_len = expected_elems.len(self.db); @@ -412,7 +437,19 @@ impl<'db> TyChecker<'db> { TyId::invalid(self.db, InvalidCause::Other) } }, - Err(_) => TyId::invalid(self.db, InvalidCause::Other), + Err(err) => { + let span = + err.anchor_dyn_span_for_body_record_pat(self.db, self.body(), pat, *path); + if let Some(diag) = err.into_diag( + self.db, + *path, + span, + crate::name_resolution::ExpectedPathKind::Value, + ) { + self.push_diag(diag); + } + TyId::invalid(self.db, InvalidCause::Other) + } } } diff --git a/crates/hir-analysis/src/ty/ty_error.rs b/crates/hir-analysis/src/ty/ty_error.rs index b71246ac25..d7e1235e87 100644 --- a/crates/hir-analysis/src/ty/ty_error.rs +++ b/crates/hir-analysis/src/ty/ty_error.rs @@ -1,5 +1,7 @@ use hir::{ hir_def::{scope_graph::ScopeId, PathId, TypeId}, + path_anchor::{map_path_anchor_to_dyn_lazy, AnchorPicker}, + path_view::HirPathAdapter, span::{path::LazyPathSpan, types::LazyTySpan}, visitor::{prelude::DynLazySpan, walk_path, walk_type, Visitor, VisitorCtxt}, }; @@ -113,20 +115,12 @@ impl<'db> Visitor<'db> for HirTyErrVisitor<'db> { Ok(res) => res, Err(err) => { - let segment_idx = err.failed_at.segment_index(self.db); - // Use the HIR path to check if the corresponding segment is a QualifiedType. - let seg_hir = path.segment(self.db, segment_idx).unwrap_or(path); - let segment = path_span.segment(segment_idx); - let segment_span = match seg_hir.kind(self.db) { - hir::hir_def::PathKind::QualifiedType { .. } => { - segment.qualified_type().trait_qualifier().name() - } - _ => segment.ident(), - }; - - if let Some(diag) = - err.into_diag(self.db, path, segment_span.into(), ExpectedPathKind::Type) - { + // Use centralized anchor selection to choose the best segment span. + let seg_idx = err.failed_at.segment_index(self.db); + let view = HirPathAdapter::new(self.db, path); + let anchor = AnchorPicker::pick_invalid_segment(&view, seg_idx); + let span = map_path_anchor_to_dyn_lazy(path_span, anchor); + if let Some(diag) = err.into_diag(self.db, path, span, ExpectedPathKind::Type) { self.diags.push(diag.into()); } return; @@ -140,9 +134,12 @@ impl<'db> Visitor<'db> for HirTyErrVisitor<'db> { .push(PathResDiag::ExpectedType(span.into(), ident, res.kind_name()).into()); } if let Some((path, deriv_span)) = invisible { - let span = path_span.segment(path.segment_index(self.db)).ident(); + let seg_idx = path.segment_index(self.db); + let view = HirPathAdapter::new(self.db, path); + let anchor = AnchorPicker::pick_visibility_error(&view, seg_idx); + let anchored = map_path_anchor_to_dyn_lazy(path_span.clone(), anchor); let ident = path.ident(self.db); - let diag = PathResDiag::Invisible(span.into(), *ident.unwrap(), deriv_span); + let diag = PathResDiag::Invisible(anchored, *ident.unwrap(), deriv_span); self.diags.push(diag.into()); } diff --git a/crates/hir-analysis/src/ty/ty_lower.rs b/crates/hir-analysis/src/ty/ty_lower.rs index 602c4ec363..f6211fb3eb 100644 --- a/crates/hir-analysis/src/ty/ty_lower.rs +++ b/crates/hir-analysis/src/ty/ty_lower.rs @@ -13,7 +13,8 @@ use super::{ ty_def::{InvalidCause, Kind, TyData, TyId, TyParam}, }; use crate::name_resolution::{ - resolve_ident_to_bucket, resolve_path, NameDomain, NameResKind, PathRes, + resolve_ident_to_bucket, resolve_with_policy, DomainPreference, NameDomain, NameResKind, + PathRes, }; use crate::{ty::binder::Binder, HirAnalysisDb}; @@ -81,7 +82,7 @@ fn lower_path<'db>( return TyId::invalid(db, InvalidCause::ParseError); }; - match resolve_path(db, path, scope, assumptions, false) { + match resolve_with_policy(db, path, scope, assumptions, DomainPreference::Type) { Ok(PathRes::Ty(ty) | PathRes::TyAlias(_, ty) | PathRes::Func(ty)) => ty, Ok(_) => TyId::invalid(db, InvalidCause::Other), Err(_) => TyId::invalid(db, InvalidCause::PathResolutionFailed { path }), diff --git a/crates/hir/src/hir_def/body.rs b/crates/hir/src/hir_def/body.rs index a4f4c3701f..210d8ecdcd 100644 --- a/crates/hir/src/hir_def/body.rs +++ b/crates/hir/src/hir_def/body.rs @@ -20,6 +20,7 @@ use crate::{ visitor::prelude::*, HirDb, }; +// duplicate imports removed #[salsa::tracked] #[derive(Debug)] @@ -45,6 +46,8 @@ pub struct Body<'db> { pub(crate) source_map: BodySourceMap, #[return_ref] pub(crate) origin: HirOrigin, + #[return_ref] + pub(crate) path_index: BodyPathIndex<'db>, } impl<'db> Body<'db> { @@ -235,6 +238,18 @@ where } } +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +pub struct BodyPathIndex<'db> { + pub entries: IndexMap>, +} + +unsafe impl<'db> Update for BodyPathIndex<'db> { + unsafe fn maybe_update(old_ptr: *mut Self, new_val: Self) -> bool { + let old_val = unsafe { &mut *old_ptr }; + Update::maybe_update(&mut old_val.entries, new_val.entries) + } +} + struct BlockOrderCalculator<'db> { db: &'db dyn HirDb, order: FxHashMap, diff --git a/crates/hir/src/hir_def/item.rs b/crates/hir/src/hir_def/item.rs index 8dc03a24e6..6be10c5e5f 100644 --- a/crates/hir/src/hir_def/item.rs +++ b/crates/hir/src/hir_def/item.rs @@ -430,7 +430,10 @@ impl<'db> TopLevelMod<'db> { pub fn child_top_mods( self, db: &'db dyn HirDb, - ) -> impl Iterator> + 'db { + ) -> Result< + impl Iterator> + 'db, + crate::hir_def::module_tree::StaleReferenceError, + > { // let ingot = self.index(db).containing_ingot(db, location) let module_tree = self.ingot(db).module_tree(db); module_tree.children(self) @@ -454,7 +457,10 @@ impl<'db> TopLevelMod<'db> { s_graph.items_dfs(db) } - pub fn parent(self, db: &'db dyn HirDb) -> Option> { + pub fn parent( + self, + db: &'db dyn HirDb, + ) -> Result>, crate::hir_def::module_tree::StaleReferenceError> { let module_tree = self.ingot(db).module_tree(db); module_tree.parent(self) } diff --git a/crates/hir/src/hir_def/module_tree.rs b/crates/hir/src/hir_def/module_tree.rs index 038b79506c..4a9026e677 100644 --- a/crates/hir/src/hir_def/module_tree.rs +++ b/crates/hir/src/hir_def/module_tree.rs @@ -8,6 +8,11 @@ use cranelift_entity::{entity_impl, EntityRef, PrimaryMap}; use salsa::Update; use super::{IdentId, TopLevelMod}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum StaleReferenceError { + StaleTopLevelMod, +} use crate::{lower::map_file_to_mod_impl, HirDb}; /// This tree represents the structure of an ingot. @@ -100,13 +105,21 @@ impl ModuleTree<'_> { } /// Returns the tree node id of the given top level module. - pub fn tree_node(&self, top_mod: TopLevelMod) -> ModuleTreeNodeId { - self.mod_map[&top_mod] + /// Returns Err if the TopLevelMod is stale (not found in this tree). + pub fn tree_node(&self, top_mod: TopLevelMod) -> Result { + self.mod_map + .get(&top_mod) + .copied() + .ok_or(StaleReferenceError::StaleTopLevelMod) } /// Returns the tree node data of the given top level module. - pub fn tree_node_data(&self, top_mod: TopLevelMod) -> &ModuleTreeNode<'_> { - &self.module_tree.0[self.tree_node(top_mod)] + /// Returns Err if the TopLevelMod is stale (not found in this tree). + pub fn tree_node_data( + &self, + top_mod: TopLevelMod, + ) -> Result<&ModuleTreeNode<'_>, StaleReferenceError> { + self.tree_node(top_mod).map(|id| &self.module_tree.0[id]) } /// Returns the root of the tree, which corresponds to the ingot root file. @@ -123,19 +136,23 @@ impl ModuleTree<'_> { self.mod_map.keys().copied() } - pub fn parent(&self, top_mod: TopLevelMod) -> Option> { - let node = self.tree_node_data(top_mod); - node.parent.map(|id| self.module_tree.0[id].top_mod) + pub fn parent( + &self, + top_mod: TopLevelMod, + ) -> Result>, StaleReferenceError> { + let node = self.tree_node_data(top_mod)?; + Ok(node.parent.map(|id| self.module_tree.0[id].top_mod)) } - pub fn children(&self, top_mod: TopLevelMod) -> impl Iterator> + '_ { - self.tree_node_data(top_mod) - .children - .iter() - .map(move |&id| { - let node = &self.module_tree.0[id]; - node.top_mod - }) + pub fn children( + &self, + top_mod: TopLevelMod, + ) -> Result> + '_, StaleReferenceError> { + let node = self.tree_node_data(top_mod)?; + Ok(node.children.iter().map(move |&id| { + let node = &self.module_tree.0[id]; + node.top_mod + })) } } @@ -291,17 +308,17 @@ mod tests { assert_eq!(root_node.children.len(), 2); for &child in &root_node.children { - if child == local_tree.tree_node(mod1_mod) { + if child == local_tree.tree_node(mod1_mod).unwrap() { let child = local_tree.node_data(child); assert_eq!(child.parent, Some(local_tree.root())); assert_eq!(child.children.len(), 1); - assert_eq!(child.children[0], local_tree.tree_node(foo_mod)); - } else if child == local_tree.tree_node(mod2_mod) { + assert_eq!(child.children[0], local_tree.tree_node(foo_mod).unwrap()); + } else if child == local_tree.tree_node(mod2_mod).unwrap() { let child = local_tree.node_data(child); assert_eq!(child.parent, Some(local_tree.root())); assert_eq!(child.children.len(), 2); - assert_eq!(child.children[0], local_tree.tree_node(bar_mod)); - assert_eq!(child.children[1], local_tree.tree_node(baz_mod)); + assert_eq!(child.children[0], local_tree.tree_node(bar_mod).unwrap()); + assert_eq!(child.children[1], local_tree.tree_node(baz_mod).unwrap()); } else { panic!("unexpected child") } diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index e82878ff97..1f08942054 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -3,6 +3,9 @@ pub use lower::parse::ParserError; pub mod hir_def; pub mod lower; +pub mod path_anchor; +pub mod path_view; +pub mod source_index; pub mod span; pub mod visitor; diff --git a/crates/hir/src/lower/body.rs b/crates/hir/src/lower/body.rs index 861af33e1d..1d2c05a5f0 100644 --- a/crates/hir/src/lower/body.rs +++ b/crates/hir/src/lower/body.rs @@ -3,8 +3,9 @@ use parser::ast; use super::FileLowerCtxt; use crate::{ hir_def::{ - Body, BodyKind, BodySourceMap, Expr, ExprId, NodeStore, Partial, Pat, PatId, Stmt, StmtId, - TrackedItemId, TrackedItemVariant, + params::{GenericArg, GenericArgListId}, + Body, BodyKind, BodyPathIndex, BodySourceMap, Expr, ExprId, NodeStore, Partial, Pat, PatId, + PathId, Stmt, StmtId, TrackedItemId, TrackedItemVariant, TupleTypeId, TypeId, TypeKind, }, span::HirOrigin, }; @@ -33,6 +34,7 @@ pub(super) struct BodyCtxt<'ctxt, 'db> { pub(super) exprs: NodeStore>>, pub(super) pats: NodeStore>>, pub(super) source_map: BodySourceMap, + pub(super) path_index: BodyPathIndex<'db>, } impl<'ctxt, 'db> BodyCtxt<'ctxt, 'db> { @@ -43,6 +45,78 @@ impl<'ctxt, 'db> BodyCtxt<'ctxt, 'db> { expr_id } + /// Record all PathId occurrences reachable from a TypeId, including nested + /// tuple/array elements and generic args on path segments. + pub(super) fn record_type_paths(&mut self, ty: TypeId<'db>) { + match ty.data(self.f_ctxt.db()) { + TypeKind::Path(p) => { + if let Partial::Present(pid) = p { + // Record the path itself. + let idx = self.path_index.entries.len(); + self.path_index.entries.insert(idx, *pid); + // Record any type paths inside generic args of each segment. + self.record_path_generic_arg_types(*pid); + } + } + TypeKind::Ptr(inner) => { + if let Partial::Present(inner) = inner { + self.record_type_paths(*inner); + } + } + TypeKind::Tuple(tup) => { + self.record_tuple_type_paths(*tup); + } + TypeKind::Array(elem, _len) => { + if let Partial::Present(elem) = elem { + self.record_type_paths(*elem); + } + } + TypeKind::Never => {} + } + } + + fn record_tuple_type_paths(&mut self, tup: TupleTypeId<'db>) { + for part in tup.data(self.f_ctxt.db()).iter() { + if let Partial::Present(ty) = part { + self.record_type_paths(*ty); + } + } + } + + /// Walk generic args of each segment in a PathId and record type paths. + fn record_path_generic_arg_types(&mut self, path: PathId<'db>) { + let db = self.f_ctxt.db(); + let segs = path.len(db); + for i in 0..segs { + if let Some(seg) = path.segment(db, i) { + let args = seg.generic_args(db); + self.record_generic_arg_types(args); + } + } + } + + /// Record type-paths present in a generic arg list. + pub(super) fn record_generic_arg_types(&mut self, args: GenericArgListId<'db>) { + let db = self.f_ctxt.db(); + for arg in args.data(db).iter() { + match arg { + GenericArg::Type(t) => { + if let Partial::Present(ty) = t.ty { + self.record_type_paths(ty); + } + } + GenericArg::AssocType(a) => { + if let Partial::Present(ty) = a.ty { + self.record_type_paths(ty); + } + } + GenericArg::Const(_c) => { + // no PathId inside const generic bodies yet + } + } + } + } + pub(super) fn push_invalid_expr(&mut self, origin: HirOrigin) -> ExprId { let expr_id = self.exprs.push(Partial::Absent); self.source_map.expr_map.insert(expr_id, origin); @@ -84,6 +158,7 @@ impl<'ctxt, 'db> BodyCtxt<'ctxt, 'db> { exprs: NodeStore::new(), pats: NodeStore::new(), source_map: BodySourceMap::default(), + path_index: BodyPathIndex::default(), } } @@ -100,6 +175,7 @@ impl<'ctxt, 'db> BodyCtxt<'ctxt, 'db> { self.f_ctxt.top_mod(), self.source_map, origin, + self.path_index, ); self.f_ctxt.leave_item_scope(body); diff --git a/crates/hir/src/lower/expr.rs b/crates/hir/src/lower/expr.rs index 8df114f931..321f39eaf2 100644 --- a/crates/hir/src/lower/expr.rs +++ b/crates/hir/src/lower/expr.rs @@ -70,6 +70,10 @@ impl<'db> Expr<'db> { IdentId::lower_token_partial(ctxt.f_ctxt, method_call.method_name()); let generic_args = GenericArgListId::lower_ast_opt(ctxt.f_ctxt, method_call.generic_args()); + // Record any type paths used in generic args. + if !generic_args.is_empty(ctxt.f_ctxt.db()) { + ctxt.record_generic_arg_types(generic_args); + } let args = method_call .args() .map(|args| { @@ -83,11 +87,19 @@ impl<'db> Expr<'db> { ast::ExprKind::Path(path) => { let path = PathId::lower_ast_partial(ctxt.f_ctxt, path.path()); + if let crate::hir_def::Partial::Present(pid) = path { + let idx = ctxt.path_index.entries.len(); + ctxt.path_index.entries.insert(idx, pid); + } Self::Path(path) } ast::ExprKind::RecordInit(record_init) => { let path = PathId::lower_ast_partial(ctxt.f_ctxt, record_init.path()); + if let crate::hir_def::Partial::Present(pid) = path { + let idx = ctxt.path_index.entries.len(); + ctxt.path_index.entries.insert(idx, pid); + } let fields = record_init .fields() .map(|fields| { diff --git a/crates/hir/src/lower/pat.rs b/crates/hir/src/lower/pat.rs index 71e7da794f..a7f5b8dd3e 100644 --- a/crates/hir/src/lower/pat.rs +++ b/crates/hir/src/lower/pat.rs @@ -31,11 +31,19 @@ impl<'db> Pat<'db> { ast::PatKind::Path(path_ast) => { let path = PathId::lower_ast_partial(ctxt.f_ctxt, path_ast.path()); + if let crate::hir_def::Partial::Present(pid) = path { + let idx = ctxt.path_index.entries.len(); + ctxt.path_index.entries.insert(idx, pid); + } Pat::Path(path, path_ast.mut_token().is_some()) } ast::PatKind::PathTuple(path_tup) => { let path = PathId::lower_ast_partial(ctxt.f_ctxt, path_tup.path()); + if let crate::hir_def::Partial::Present(pid) = path { + let idx = ctxt.path_index.entries.len(); + ctxt.path_index.entries.insert(idx, pid); + } let elems = match path_tup.elems() { Some(elems) => elems.iter().map(|pat| Pat::lower_ast(ctxt, pat)).collect(), None => vec![], @@ -45,6 +53,10 @@ impl<'db> Pat<'db> { ast::PatKind::Record(record) => { let path = PathId::lower_ast_partial(ctxt.f_ctxt, record.path()); + if let crate::hir_def::Partial::Present(pid) = path { + let idx = ctxt.path_index.entries.len(); + ctxt.path_index.entries.insert(idx, pid); + } let fields = match record.fields() { Some(fields) => fields .iter() diff --git a/crates/hir/src/lower/scope_builder.rs b/crates/hir/src/lower/scope_builder.rs index 2250d4ad96..f32b585bd7 100644 --- a/crates/hir/src/lower/scope_builder.rs +++ b/crates/hir/src/lower/scope_builder.rs @@ -84,14 +84,16 @@ impl<'db> ScopeGraphBuilder<'db> { ScopeId::Item(top_mod.ingot(self.db).root_mod(self.db).into()), EdgeKind::ingot(), ); - for child in top_mod.child_top_mods(self.db) { - let child_name = child.name(self.db); - let edge = EdgeKind::mod_(child_name); - self.graph - .add_external_edge(item_node, ScopeId::Item(child.into()), edge) + if let Ok(children) = top_mod.child_top_mods(self.db) { + for child in children { + let child_name = child.name(self.db); + let edge = EdgeKind::mod_(child_name); + self.graph + .add_external_edge(item_node, ScopeId::Item(child.into()), edge) + } } - if let Some(parent) = top_mod.parent(self.db) { + if let Ok(Some(parent)) = top_mod.parent(self.db) { let edge = EdgeKind::super_(); self.graph .add_external_edge(item_node, ScopeId::Item(parent.into()), edge); diff --git a/crates/hir/src/lower/stmt.rs b/crates/hir/src/lower/stmt.rs index ca3b84b714..f3a7dce4da 100644 --- a/crates/hir/src/lower/stmt.rs +++ b/crates/hir/src/lower/stmt.rs @@ -11,9 +11,12 @@ impl<'db> Stmt<'db> { let (stmt, origin_kind) = match ast.kind() { ast::StmtKind::Let(let_) => { let pat = Pat::lower_ast_opt(ctxt, let_.pat()); - let ty = let_ - .type_annotation() - .map(|ty| TypeId::lower_ast(ctxt.f_ctxt, ty)); + let ty = let_.type_annotation().map(|ty| { + let ty = TypeId::lower_ast(ctxt.f_ctxt, ty); + // Record type path occurrences in this body. + ctxt.record_type_paths(ty); + ty + }); let init = let_.initializer().map(|init| Expr::lower_ast(ctxt, init)); (Stmt::Let(pat, ty, init), HirOrigin::raw(&ast)) } diff --git a/crates/hir/src/path_anchor.rs b/crates/hir/src/path_anchor.rs new file mode 100644 index 0000000000..48ca4ddd9b --- /dev/null +++ b/crates/hir/src/path_anchor.rs @@ -0,0 +1,104 @@ +use crate::span::DynLazySpan; +use crate::{ + path_view::{PathView, SegmentKind}, + span::lazy_spans::LazyPathSpan, +}; + +/// The kind of sub-span to select within a path segment. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum PathAnchorKind { + Ident, + GenericArgs, + Segment, + /// The trait name in a qualified type segment, e.g., `` → `Trait`. + TraitName, +} + +/// A structural anchor describing which segment and which part to highlight. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct PathAnchor { + pub seg_idx: usize, + pub kind: PathAnchorKind, +} + +/// Heuristics for selecting anchors in typical error cases. These are pure and +/// depend only on `PathView` structure. +pub struct AnchorPicker; + +impl AnchorPicker { + /// Unresolved tail of a path. Prefer the last segment's ident; if the last + /// segment is qualified type, pick the trait name; otherwise segment. + pub fn pick_unresolved_tail(view: &V) -> PathAnchor { + let n = view.segments(); + let idx = n.saturating_sub(1); + Self::pick_preferred(view, idx) + } + + /// Invalid segment at `seg_idx`. + pub fn pick_invalid_segment(view: &V, seg_idx: usize) -> PathAnchor { + Self::pick_preferred(view, seg_idx) + } + + /// Visibility error at `seg_idx`: prefer ident if present. + pub fn pick_visibility_error(view: &V, seg_idx: usize) -> PathAnchor { + if let Some(info) = view.segment_info(seg_idx) { + if info.has_ident { + return PathAnchor { + seg_idx, + kind: PathAnchorKind::Ident, + }; + } + } + PathAnchor { + seg_idx, + kind: PathAnchorKind::Segment, + } + } + + fn pick_preferred(view: &V, seg_idx: usize) -> PathAnchor { + match view.segment_info(seg_idx) { + Some(info) => match info.kind { + SegmentKind::QualifiedType => PathAnchor { + seg_idx, + kind: PathAnchorKind::TraitName, + }, + SegmentKind::Plain => { + if info.has_ident { + PathAnchor { + seg_idx, + kind: PathAnchorKind::Ident, + } + } else if info.has_generic_args { + PathAnchor { + seg_idx, + kind: PathAnchorKind::GenericArgs, + } + } else { + PathAnchor { + seg_idx, + kind: PathAnchorKind::Segment, + } + } + } + }, + None => PathAnchor { + seg_idx, + kind: PathAnchorKind::Segment, + }, + } + } +} + +/// Map to a DynLazySpan without resolving to a concrete Span. +pub fn map_path_anchor_to_dyn_lazy<'db>( + lazy_path: LazyPathSpan<'db>, + anchor: PathAnchor, +) -> DynLazySpan<'db> { + let seg = lazy_path.segment(anchor.seg_idx); + match anchor.kind { + PathAnchorKind::Ident => seg.ident().into(), + PathAnchorKind::GenericArgs => seg.generic_args().into(), + PathAnchorKind::Segment => seg.into_atom().into(), + PathAnchorKind::TraitName => seg.qualified_type().trait_qualifier().name().into(), + } +} diff --git a/crates/hir/src/path_view.rs b/crates/hir/src/path_view.rs new file mode 100644 index 0000000000..9042926acc --- /dev/null +++ b/crates/hir/src/path_view.rs @@ -0,0 +1,68 @@ +use crate::{ + hir_def::{PathId, PathKind}, + HirDb, +}; + +/// Structural classification of a path segment. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum SegmentKind { + /// A regular identifier segment, possibly with generic arguments. + Plain, + /// A qualified type segment like ``. + QualifiedType, +} + +/// Minimal, structural facts about a path segment. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct SegmentInfo { + pub kind: SegmentKind, + pub has_ident: bool, + pub has_generic_args: bool, +} + +/// A unified, structural view over paths. Implementations may wrap HIR or AST +/// representations; consumers must not rely on concrete types. +pub trait PathView { + /// Number of segments in the path. + fn segments(&self) -> usize; + /// Facts about the `idx`-th segment, if it exists. + fn segment_info(&self, idx: usize) -> Option; +} + +/// HIR adapter implementing `PathView` for `PathId`. +pub struct HirPathAdapter<'db> { + pub db: &'db dyn HirDb, + pub path: PathId<'db>, +} + +impl<'db> HirPathAdapter<'db> { + pub fn new(db: &'db dyn HirDb, path: PathId<'db>) -> Self { + Self { db, path } + } +} + +impl PathView for HirPathAdapter<'_> { + fn segments(&self) -> usize { + self.path.len(self.db) + } + + fn segment_info(&self, idx: usize) -> Option { + let seg = self.path.segment(self.db, idx)?; + let info = match seg.kind(self.db) { + PathKind::Ident { + ident, + generic_args, + } => SegmentInfo { + kind: SegmentKind::Plain, + has_ident: ident.is_present(), + has_generic_args: !generic_args.is_empty(self.db), + }, + PathKind::QualifiedType { .. } => SegmentInfo { + kind: SegmentKind::QualifiedType, + has_ident: false, + has_generic_args: false, + }, + }; + Some(info) + } +} diff --git a/crates/hir/src/source_index.rs b/crates/hir/src/source_index.rs new file mode 100644 index 0000000000..f9aa0ddd9a --- /dev/null +++ b/crates/hir/src/source_index.rs @@ -0,0 +1,505 @@ +use parser::TextSize; + +use crate::{ + hir_def::{ + Body, Expr, ExprId, IdentId, Partial, Pat, PatId, PathId, TopLevelMod, UseAlias, UsePathId, + UsePathSegment, + }, + span::path::LazyPathSpan, + span::{DynLazySpan, LazySpan}, + visitor::{prelude::LazyPathSpan as VisitorLazyPathSpan, Visitor, VisitorCtxt}, + SpannedHirDb, +}; + +// (legacy segment-span projections removed) + +// ---------- Unified occurrence rangemap ---------- + +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum OccurrencePayload<'db> { + PathSeg { + path: PathId<'db>, + scope: crate::hir_def::scope_graph::ScopeId<'db>, + seg_idx: usize, + path_lazy: LazyPathSpan<'db>, + span: DynLazySpan<'db>, + }, + UsePathSeg { + scope: crate::hir_def::scope_graph::ScopeId<'db>, + path: UsePathId<'db>, + seg_idx: usize, + span: DynLazySpan<'db>, + }, + UseAliasName { + scope: crate::hir_def::scope_graph::ScopeId<'db>, + ident: IdentId<'db>, + span: DynLazySpan<'db>, + }, + MethodName { + scope: crate::hir_def::scope_graph::ScopeId<'db>, + body: Body<'db>, + ident: IdentId<'db>, + receiver: ExprId, + span: DynLazySpan<'db>, + }, + FieldAccessName { + scope: crate::hir_def::scope_graph::ScopeId<'db>, + body: Body<'db>, + ident: IdentId<'db>, + receiver: ExprId, + span: DynLazySpan<'db>, + }, + PatternLabelName { + scope: crate::hir_def::scope_graph::ScopeId<'db>, + body: Body<'db>, + ident: IdentId<'db>, + constructor_path: Option>, + span: DynLazySpan<'db>, + }, + PathExprSeg { + scope: crate::hir_def::scope_graph::ScopeId<'db>, + body: Body<'db>, + expr: ExprId, + path: PathId<'db>, + seg_idx: usize, + span: DynLazySpan<'db>, + }, + PathPatSeg { + scope: crate::hir_def::scope_graph::ScopeId<'db>, + body: Body<'db>, + pat: PatId, + path: PathId<'db>, + seg_idx: usize, + span: DynLazySpan<'db>, + }, + /// Name token of an item/variant/param header. Allows goto/hover via the unified index. + ItemHeaderName { + scope: crate::hir_def::scope_graph::ScopeId<'db>, + span: DynLazySpan<'db>, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct OccurrenceRangeEntry<'db> { + pub start: TextSize, + pub end: TextSize, + pub payload: OccurrencePayload<'db>, +} + +// (legacy MethodCallEntry removed; semantic-query consumes OccurrencePayload::MethodName directly) + +#[salsa::tracked(return_ref)] +pub fn unified_occurrence_rangemap_for_top_mod<'db>( + db: &'db dyn SpannedHirDb, + top_mod: TopLevelMod<'db>, +) -> Vec> { + let payloads = collect_unified_occurrences(db, top_mod); + let mut out: Vec> = Vec::new(); + for p in payloads.into_iter() { + let span = match &p { + OccurrencePayload::PathSeg { span, .. } => span, + OccurrencePayload::UsePathSeg { span, .. } => span, + OccurrencePayload::UseAliasName { span, .. } => span, + OccurrencePayload::MethodName { span, .. } => span, + OccurrencePayload::FieldAccessName { span, .. } => span, + OccurrencePayload::PatternLabelName { span, .. } => span, + OccurrencePayload::PathExprSeg { span, .. } => span, + OccurrencePayload::PathPatSeg { span, .. } => span, + OccurrencePayload::ItemHeaderName { span, .. } => span, + }; + if let Some(res) = span.clone().resolve(db) { + out.push(OccurrenceRangeEntry { + start: res.range.start(), + end: res.range.end(), + payload: p, + }); + } + } + out.sort_by(|a, b| match a.start.cmp(&b.start) { + core::cmp::Ordering::Equal => (a.end - a.start).cmp(&(b.end - b.start)), + ord => ord, + }); + out +} + +// ---------- Unified collector powering rangemap + path spans ---------- + +fn collect_unified_occurrences<'db>( + db: &'db dyn SpannedHirDb, + top_mod: TopLevelMod<'db>, +) -> Vec> { + #[derive(Default)] + struct Collector<'db> { + occ: Vec>, + suppress_generic_for_path: Option>, + } + + impl<'db, 'ast: 'db> Visitor<'ast> for Collector<'db> { + fn visit_path( + &mut self, + ctxt: &mut VisitorCtxt<'ast, VisitorLazyPathSpan<'ast>>, + path: PathId<'db>, + ) { + // Suppress generic PathSeg occurrences when this path is the same + // path that is already recorded as a contextual PathExprSeg/PathPatSeg. + if let Some(p) = self.suppress_generic_for_path { + if p == path { + return; + } + } + if let Some(span) = ctxt.span() { + let scope = ctxt.scope(); + let tail = path.segment_index(ctxt.db()); + for i in 0..=tail { + let seg_span: DynLazySpan<'db> = span.clone().segment(i).ident().into(); + self.occ.push(OccurrencePayload::PathSeg { + path, + scope, + seg_idx: i, + path_lazy: span.clone(), + span: seg_span, + }); + } + } + } + fn visit_use( + &mut self, + ctxt: &mut VisitorCtxt<'ast, crate::span::item::LazyUseSpan<'ast>>, + use_item: crate::hir_def::Use<'db>, + ) { + // Record alias name if present + if let Some(Partial::Present(UseAlias::Ident(ident))) = use_item.alias(ctxt.db()) { + if let Some(span) = ctxt.span() { + let scope = ctxt.scope(); + let alias_span: DynLazySpan<'db> = span.alias().name().into(); + self.occ.push(OccurrencePayload::UseAliasName { + scope, + ident, + span: alias_span, + }); + } + } + // Traverse use path segments and collect occurrences + if let Partial::Present(path) = use_item.path(ctxt.db()) { + if let Some(lazy) = ctxt.span() { + let scope = ctxt.scope(); + let use_path_span = lazy.path(); + for (i, seg) in path.data(ctxt.db()).iter().enumerate() { + if matches!(seg.to_opt(), Some(UsePathSegment::Glob)) { + continue; + } + let seg_span: DynLazySpan<'db> = + use_path_span.clone().segment(i).into_atom().into(); + self.occ.push(OccurrencePayload::UsePathSeg { + scope, + path, + seg_idx: i, + span: seg_span, + }); + } + } + } + } + fn visit_expr( + &mut self, + ctxt: &mut VisitorCtxt<'ast, crate::span::expr::LazyExprSpan<'ast>>, + id: ExprId, + expr: &Expr<'db>, + ) { + match expr { + Expr::MethodCall(receiver, method_name, _gargs, _args) => { + if let Some(name) = method_name.to_opt() { + if let Some(span) = ctxt.span() { + let scope = ctxt.scope(); + let body = ctxt.body(); + let name_span: DynLazySpan<'db> = + span.into_method_call_expr().method_name().into(); + self.occ.push(OccurrencePayload::MethodName { + scope, + body, + ident: name, + receiver: *receiver, + span: name_span, + }); + } + } + } + Expr::Field( + receiver, + Partial::Present(crate::hir_def::FieldIndex::Ident(ident)), + ) => { + if let Some(span) = ctxt.span() { + let scope = ctxt.scope(); + let body = ctxt.body(); + let name_span: DynLazySpan<'db> = span.into_field_expr().accessor().into(); + self.occ.push(OccurrencePayload::FieldAccessName { + scope, + body, + ident: *ident, + receiver: *receiver, + span: name_span, + }); + } + } + Expr::Path(Partial::Present(path)) => { + if let Some(span) = ctxt.span() { + let scope = ctxt.scope(); + let body = ctxt.body(); + let tail = path.segment_index(ctxt.db()); + for i in 0..=tail { + let seg_span: DynLazySpan<'db> = span + .clone() + .into_path_expr() + .path() + .segment(i) + .ident() + .into(); + self.occ.push(OccurrencePayload::PathExprSeg { + scope, + body, + expr: id, + path: *path, + seg_idx: i, + span: seg_span, + }); + } + // Avoid emitting generic PathSeg for this path by suppressing + // it during the recursive walk of this expression. + let prev = self.suppress_generic_for_path; + self.suppress_generic_for_path = Some(*path); + crate::visitor::walk_expr(self, ctxt, id); + self.suppress_generic_for_path = prev; + return; + } + } + _ => {} + } + crate::visitor::walk_expr(self, ctxt, id); + } + fn visit_pat( + &mut self, + ctxt: &mut VisitorCtxt<'ast, crate::span::pat::LazyPatSpan<'ast>>, + pat: PatId, + pat_data: &Pat<'db>, + ) { + match pat_data { + Pat::Record(path, fields) => { + if let Some(span) = ctxt.span() { + let scope = ctxt.scope(); + let body = ctxt.body(); + let ctor_path = match path { + Partial::Present(p) => Some(*p), + _ => None, + }; + for (i, fld) in fields.iter().enumerate() { + if let Some(ident) = fld.label(ctxt.db(), body) { + let name_span: DynLazySpan<'db> = span + .clone() + .into_record_pat() + .fields() + .field(i) + .name() + .into(); + self.occ.push(OccurrencePayload::PatternLabelName { + scope, + body, + ident, + constructor_path: ctor_path, + span: name_span, + }); + } + } + } + } + Pat::Path(Partial::Present(path), _is_mut) => { + if let Some(span) = ctxt.span() { + let scope = ctxt.scope(); + let body = ctxt.body(); + let tail = path.segment_index(ctxt.db()); + for i in 0..=tail { + let seg_span: DynLazySpan<'db> = span + .clone() + .into_path_pat() + .path() + .segment(i) + .ident() + .into(); + self.occ.push(OccurrencePayload::PathPatSeg { + scope, + body, + pat, + path: *path, + seg_idx: i, + span: seg_span, + }); + } + // Suppress generic PathSeg emission for this pattern path. + let prev = self.suppress_generic_for_path; + self.suppress_generic_for_path = Some(*path); + crate::visitor::walk_pat(self, ctxt, pat); + self.suppress_generic_for_path = prev; + return; + } + } + _ => {} + } + crate::visitor::walk_pat(self, ctxt, pat) + } + } + + let mut coll = Collector::default(); + let mut ctxt = VisitorCtxt::with_top_mod(db, top_mod); + coll.visit_top_mod(&mut ctxt, top_mod); + // Add item/variant/param header name occurrences + for it in top_mod.all_items(db).iter() { + if let Some(name) = it.name_span() { + let sc = crate::hir_def::scope_graph::ScopeId::from_item(*it); + let name_dyn: DynLazySpan<'db> = name; + coll.occ.push(OccurrencePayload::ItemHeaderName { + scope: sc, + span: name_dyn, + }); + } + if let crate::hir_def::ItemKind::Enum(e) = *it { + let vars = e.variants(db); + for (idx, vdef) in vars.data(db).iter().enumerate() { + if vdef.name.to_opt().is_none() { + continue; + } + let variant = crate::hir_def::EnumVariant::new(e, idx); + let sc = variant.scope(); + let name_dyn: DynLazySpan<'db> = variant.span().name().into(); + coll.occ.push(OccurrencePayload::ItemHeaderName { + scope: sc, + span: name_dyn, + }); + } + } + if let crate::hir_def::ItemKind::Func(f) = *it { + if let Some(params) = f.params(db).to_opt() { + for (idx, _p) in params.data(db).iter().enumerate() { + let sc = crate::hir_def::scope_graph::ScopeId::FuncParam(*it, idx as u16); + let name_dyn: DynLazySpan<'db> = f.span().params().param(idx).name().into(); + coll.occ.push(OccurrencePayload::ItemHeaderName { + scope: sc, + span: name_dyn, + }); + } + } + } + } + // Prefer contextual occurrences (PathExprSeg/PathPatSeg) over generic PathSeg + // when both cover the exact same textual span. Build a set of spans covered + // by contextual occurrences, then drop PathSeg entries that overlap exactly. + use rustc_hash::FxHashSet; + let mut contextual_spans: FxHashSet<(parser::TextSize, parser::TextSize)> = + FxHashSet::default(); + for o in coll.occ.iter() { + match o { + OccurrencePayload::PathExprSeg { span, .. } + | OccurrencePayload::PathPatSeg { span, .. } => { + if let Some(sp) = span.clone().resolve(db) { + contextual_spans.insert((sp.range.start(), sp.range.end())); + } + } + _ => {} + } + } + + let mut filtered: Vec> = Vec::with_capacity(coll.occ.len()); + for o in coll.occ.into_iter() { + match &o { + OccurrencePayload::PathSeg { span, .. } => { + if let Some(sp) = span.clone().resolve(db) { + let key = (sp.range.start(), sp.range.end()); + if contextual_spans.contains(&key) { + // Skip generic PathSeg if there is a contextual occurrence for this span + continue; + } + } + filtered.push(o); + } + _ => filtered.push(o), + } + } + + filtered +} + +// (legacy entry structs removed; semantic-query derives hits from OccurrencePayload) + +/// Return all occurrences whose resolved range contains the given offset. +/// Note: linear scan; callers should prefer small files or pre-filtered contexts. +pub fn occurrences_at_offset<'db>( + db: &'db dyn SpannedHirDb, + top_mod: TopLevelMod<'db>, + offset: parser::TextSize, +) -> Vec> { + // Half-open containment: [start, end) + unified_occurrence_rangemap_for_top_mod(db, top_mod) + .iter() + .filter(|e| e.start <= offset && offset < e.end) + .map(|e| e.payload.clone()) + .collect() +} + +impl<'db> OccurrencePayload<'db> { + /// Returns true if this occurrence should be included in rename operations. + /// Filters out language keywords like 'self', 'Self', 'super', etc. + pub fn rename_allowed(&self, db: &'db dyn crate::SpannedHirDb) -> bool { + use crate::hir_def::scope_graph::ScopeId; + + match self { + // Path-based occurrences: check if they resolve to language keywords + OccurrencePayload::PathSeg { path, seg_idx, .. } + | OccurrencePayload::PathExprSeg { path, seg_idx, .. } + | OccurrencePayload::PathPatSeg { path, seg_idx, .. } => { + Self::check_path_segment_keyword(db, *path, *seg_idx) + } + + // Direct IdentId occurrences: check for language keywords + OccurrencePayload::UseAliasName { ident, .. } + | OccurrencePayload::MethodName { ident, .. } + | OccurrencePayload::FieldAccessName { ident, .. } + | OccurrencePayload::PatternLabelName { ident, .. } => { + !Self::is_language_keyword(db, *ident) + } + + // Use path segments are always safe to rename + OccurrencePayload::UsePathSeg { .. } => true, + + // ItemHeaderName: exclude definitions, but allow function parameters (except self) + OccurrencePayload::ItemHeaderName { scope, .. } => { + match scope { + ScopeId::FuncParam(_, _) => { + if let Some(param_name) = scope.name(db) { + !param_name.is_self(db) + } else { + true + } + } + _ => false, // Item definitions should not be renamed + } + } + } + } + + /// Helper: check if a path segment at the given index is a language keyword + fn check_path_segment_keyword( + db: &'db dyn crate::SpannedHirDb, + path: crate::hir_def::PathId<'db>, + seg_idx: usize, + ) -> bool { + if let Some(seg) = path.segment(db, seg_idx) { + if let Some(ident) = seg.as_ident(db) { + return !Self::is_language_keyword(db, ident); + } + } + true // Default to allow if we can't resolve the segment + } + + /// Helper: check if an IdentId represents a language keyword + fn is_language_keyword(db: &'db dyn crate::SpannedHirDb, ident: IdentId<'db>) -> bool { + ident.is_self(db) || ident.is_super(db) || ident.is_self_ty(db) + } +} diff --git a/crates/hir/src/view.rs b/crates/hir/src/view.rs new file mode 100644 index 0000000000..68c9595698 --- /dev/null +++ b/crates/hir/src/view.rs @@ -0,0 +1,3 @@ +// This module will contain the shared `PathView` and `SegmentView` traits, +// which are the foundational contract for unifying path handling across +// the AST and HIR, as per our architectural plan. diff --git a/crates/language-server/Cargo.toml b/crates/language-server/Cargo.toml index 2981ea0348..a41269fda5 100644 --- a/crates/language-server/Cargo.toml +++ b/crates/language-server/Cargo.toml @@ -8,6 +8,7 @@ description = "An LSP language server for Fe lang" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + [dependencies] act-locally = "0.1.1" anyhow = "1.0.95" @@ -36,6 +37,7 @@ hir.workspace = true hir-analysis.workspace = true parser.workspace = true tempfile = "3.20.0" +fe-semantic-query = { package = "fe-semantic-query", path = "../semantic-query" } [dev-dependencies] test-utils.workspace = true diff --git a/crates/language-server/src/functionality/capabilities.rs b/crates/language-server/src/functionality/capabilities.rs index e1b1c32de2..841a394bf2 100644 --- a/crates/language-server/src/functionality/capabilities.rs +++ b/crates/language-server/src/functionality/capabilities.rs @@ -12,6 +12,14 @@ pub(crate) fn server_capabilities() -> ServerCapabilities { )), // goto definition definition_provider: Some(async_lsp::lsp_types::OneOf::Left(true)), + // find all references + references_provider: Some(async_lsp::lsp_types::OneOf::Left(true)), + // goto implementation + implementation_provider: Some( + async_lsp::lsp_types::ImplementationProviderCapability::Simple(true), + ), + // rename symbols + rename_provider: Some(async_lsp::lsp_types::OneOf::Left(true)), // support for workspace add/remove changes workspace: Some(async_lsp::lsp_types::WorkspaceServerCapabilities { workspace_folders: Some(async_lsp::lsp_types::WorkspaceFoldersServerCapabilities { diff --git a/crates/language-server/src/functionality/goto.rs b/crates/language-server/src/functionality/goto.rs index eb7b800d54..3fb67d9508 100644 --- a/crates/language-server/src/functionality/goto.rs +++ b/crates/language-server/src/functionality/goto.rs @@ -1,136 +1,21 @@ use async_lsp::ResponseError; use common::InputDb; -use hir::{ - hir_def::{scope_graph::ScopeId, ItemKind, PathId, TopLevelMod}, - lower::map_file_to_mod, - span::{DynLazySpan, LazySpan}, - visitor::{prelude::LazyPathSpan, Visitor, VisitorCtxt}, - SpannedHirDb, -}; -use hir_analysis::{ - name_resolution::{resolve_path, PathResErrorKind}, - ty::trait_resolution::PredicateListId, -}; -use tracing::error; +use fe_semantic_query::SemanticQuery; +use hir::{lower::map_file_to_mod, span::LazySpan}; +// use tracing::error; -use crate::{ - backend::Backend, - util::{to_lsp_location_from_scope, to_offset_from_position}, -}; -use driver::DriverDataBase; +use crate::{backend::Backend, util::to_offset_from_position}; +// Note: DriverDataBase and tracing are only used in tests below. pub type Cursor = parser::TextSize; -#[derive(Default)] -struct PathSpanCollector<'db> { - paths: Vec<(PathId<'db>, ScopeId<'db>, LazyPathSpan<'db>)>, -} - -impl<'db, 'ast: 'db> Visitor<'ast> for PathSpanCollector<'db> { - fn visit_path(&mut self, ctxt: &mut VisitorCtxt<'ast, LazyPathSpan<'ast>>, path: PathId<'db>) { - let Some(span) = ctxt.span() else { - return; - }; - - let scope = ctxt.scope(); - self.paths.push((path, scope, span)); - } -} - -fn find_path_surrounding_cursor<'db>( - db: &'db DriverDataBase, - cursor: Cursor, - full_paths: Vec<(PathId<'db>, ScopeId<'db>, LazyPathSpan<'db>)>, -) -> Option<(PathId<'db>, bool, ScopeId<'db>)> { - for (path, scope, lazy_span) in full_paths { - let span = lazy_span.resolve(db).unwrap(); - if span.range.contains(cursor) { - for idx in 0..=path.segment_index(db) { - let seg_span = lazy_span.clone().segment(idx).resolve(db).unwrap(); - if seg_span.range.contains(cursor) { - return Some(( - path.segment(db, idx).unwrap(), - idx != path.segment_index(db), - scope, - )); - } - } - } - } - None -} - -pub fn find_enclosing_item<'db>( - db: &'db dyn SpannedHirDb, - top_mod: TopLevelMod<'db>, - cursor: Cursor, -) -> Option> { - let items = top_mod.scope_graph(db).items_dfs(db); - - let mut smallest_enclosing_item = None; - let mut smallest_range_size = None; - - for item in items { - let lazy_item_span = DynLazySpan::from(item.span()); - let item_span = lazy_item_span.resolve(db).unwrap(); - - if item_span.range.contains(cursor) { - let range_size = item_span.range.end() - item_span.range.start(); - if smallest_range_size.is_none() || range_size < smallest_range_size.unwrap() { - smallest_enclosing_item = Some(item); - smallest_range_size = Some(range_size); - } - } - } - - smallest_enclosing_item -} - -pub fn get_goto_target_scopes_for_cursor<'db>( - db: &'db DriverDataBase, - top_mod: TopLevelMod<'db>, - cursor: Cursor, -) -> Option>> { - let item: ItemKind = find_enclosing_item(db, top_mod, cursor)?; - - let mut visitor_ctxt = VisitorCtxt::with_item(db, item); - let mut path_segment_collector = PathSpanCollector::default(); - path_segment_collector.visit_item(&mut visitor_ctxt, item); - - let (path, _is_intermediate, scope) = - find_path_surrounding_cursor(db, cursor, path_segment_collector.paths)?; - - let resolved = resolve_path(db, path, scope, PredicateListId::empty_list(db), false); // xxx fixme - let scopes = match resolved { - Ok(r) => r.as_scope(db).into_iter().collect::>(), - Err(err) => match err.kind { - PathResErrorKind::NotFound { parent: _, bucket } => { - bucket.iter_ok().flat_map(|r| r.scope()).collect() - } - PathResErrorKind::Ambiguous(vec) => vec.into_iter().flat_map(|r| r.scope()).collect(), - _ => vec![], - }, - }; - - Some(scopes) -} - pub async fn handle_goto_definition( backend: &mut Backend, params: async_lsp::lsp_types::GotoDefinitionParams, ) -> Result, ResponseError> { - // Convert the position to an offset in the file + // Convert the position to an offset in the file using the workspace's current content let params = params.text_document_position_params; - let file_text = std::fs::read_to_string(params.text_document.uri.path()).ok(); - let cursor: Cursor = to_offset_from_position(params.position, file_text.unwrap().as_str()); - - // Get the module and the goto info - let file_path_str = params.text_document.uri.path(); - let url = url::Url::from_file_path(file_path_str).map_err(|()| { - ResponseError::new( - async_lsp::ErrorCode::INTERNAL_ERROR, - format!("Invalid file path: {file_path_str}"), - ) - })?; + // Use URI directly to avoid path/encoding/case issues + let url = params.text_document.uri.clone(); let file = backend .db .workspace() @@ -138,243 +23,33 @@ pub async fn handle_goto_definition( .ok_or_else(|| { ResponseError::new( async_lsp::ErrorCode::INTERNAL_ERROR, - format!("File not found in index: {url} (original path: {file_path_str})"), + format!("File not found in index: {url}"), ) })?; + let file_text = file.text(&backend.db); + let cursor: Cursor = to_offset_from_position(params.position, file_text.as_str()); let top_mod = map_file_to_mod(&backend.db, file); - let scopes = - get_goto_target_scopes_for_cursor(&backend.db, top_mod, cursor).unwrap_or_default(); - - let locations = scopes - .iter() - .map(|scope| to_lsp_location_from_scope(&backend.db, *scope)) - .collect::>(); - - let result: Result, ()> = - Ok(Some(async_lsp::lsp_types::GotoDefinitionResponse::Array( - locations - .into_iter() - .filter_map(std::result::Result::ok) - .collect(), - ))); - let response = match result { - Ok(response) => response, - Err(e) => { - error!("Error handling goto definition: {:?}", e); - None - } - }; - Ok(response) -} -// } -#[cfg(test)] -mod tests { - use common::ingot::IngotKind; - use dir_test::{dir_test, Fixture}; - use std::collections::BTreeMap; - use test_utils::snap_test; - use url::Url; - - use super::*; - use crate::test_utils::load_ingot_from_directory; - use driver::DriverDataBase; - - // given a cursor position and a string, convert to cursor line and column - fn line_col_from_cursor(cursor: Cursor, s: &str) -> (usize, usize) { - let mut line = 0; - let mut col = 0; - for (i, c) in s.chars().enumerate() { - if i == Into::::into(cursor) { - return (line, col); - } - if c == '\n' { - line += 1; - col = 0; - } else { - col += 1; - } - } - (line, col) - } - - fn extract_multiple_cursor_positions_from_spans( - db: &DriverDataBase, - top_mod: TopLevelMod, - ) -> Vec { - let mut visitor_ctxt = VisitorCtxt::with_top_mod(db, top_mod); - let mut path_collector = PathSpanCollector::default(); - path_collector.visit_top_mod(&mut visitor_ctxt, top_mod); - - let mut cursors = Vec::new(); - for (path, _, lazy_span) in path_collector.paths { - for idx in 0..=path.segment_index(db) { - let seg_span = lazy_span.clone().segment(idx).resolve(db).unwrap(); - cursors.push(seg_span.range.start()); - } - } - - cursors.sort(); - cursors.dedup(); - - error!("Found cursors: {:?}", cursors); - cursors - } - - fn make_goto_cursors_snapshot( - db: &DriverDataBase, - fixture: &Fixture<&str>, - top_mod: TopLevelMod, - ) -> String { - let cursors = extract_multiple_cursor_positions_from_spans(db, top_mod); - let mut cursor_path_map: BTreeMap = BTreeMap::default(); - - for cursor in &cursors { - let scopes = - get_goto_target_scopes_for_cursor(db, top_mod, *cursor).unwrap_or_default(); - - if !scopes.is_empty() { - cursor_path_map.insert( - *cursor, - scopes - .iter() - .flat_map(|x| x.pretty_path(db)) - .collect::>() - .join("\n"), - ); - } - } - - let cursor_lines = cursor_path_map - .iter() - .map(|(cursor, path)| { - let (cursor_line, cursor_col) = line_col_from_cursor(*cursor, fixture.content()); - format!("cursor position ({cursor_line:?}, {cursor_col:?}), path: {path}") - }) - .collect::>(); - - format!( - "{}\n---\n{}", - fixture - .content() - .lines() - .enumerate() - .map(|(i, line)| format!("{i:?}: {line}")) - .collect::>() - .join("\n"), - cursor_lines.join("\n") - ) - } - - #[dir_test( - dir: "$CARGO_MANIFEST_DIR/test_files/single_ingot", - glob: "**/lib.fe", - )] - fn test_goto_multiple_files(fixture: Fixture<&str>) { - let cargo_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - let ingot_base_dir = - std::path::Path::new(&cargo_manifest_dir).join("test_files/single_ingot"); - - let mut db = DriverDataBase::default(); - - // Load all files from the ingot directory - load_ingot_from_directory(&mut db, &ingot_base_dir); - - // Get our specific test file - let fe_source_path = fixture.path(); - let file_url = Url::from_file_path(fe_source_path).unwrap(); - - // Get the containing ingot - should be Local now - let ingot = db.workspace().containing_ingot(&db, file_url).unwrap(); - assert_eq!(ingot.kind(&db), IngotKind::Local); - - // Introduce a new scope to limit the lifetime of `top_mod` - { - // Get the file directly from the file index - let file_url = Url::from_file_path(fe_source_path).unwrap(); - let file = db.workspace().get(&db, &file_url).unwrap(); - let top_mod = map_file_to_mod(&db, file); - - let snapshot = make_goto_cursors_snapshot(&db, &fixture, top_mod); - snap_test!(snapshot, fixture.path()); - } - - // Get the containing ingot for the file path - let file_url = Url::from_file_path(fixture.path()).unwrap(); - let ingot = db.workspace().containing_ingot(&db, file_url); - assert_eq!(ingot.unwrap().kind(&db), IngotKind::Local); - } - - #[dir_test( - dir: "$CARGO_MANIFEST_DIR/test_files", - glob: "goto*.fe" - )] - fn test_goto_cursor_target(fixture: Fixture<&str>) { - let mut db = DriverDataBase::default(); // Changed to mut - let file = db.workspace().touch( - &mut db, - Url::from_file_path(fixture.path()).unwrap(), - Some(fixture.content().to_string()), - ); - let top_mod = map_file_to_mod(&db, file); - - let snapshot = make_goto_cursors_snapshot(&db, &fixture, top_mod); - snap_test!(snapshot, fixture.path()); - } - - #[dir_test( - dir: "$CARGO_MANIFEST_DIR/test_files", - glob: "smallest_enclosing*.fe" - )] - fn test_find_path_surrounding_cursor(fixture: Fixture<&str>) { - let mut db = DriverDataBase::default(); // Changed to mut - - let file = db.workspace().touch( - &mut db, - Url::from_file_path(fixture.path()).unwrap(), - Some(fixture.content().to_string()), - ); - let top_mod = map_file_to_mod(&db, file); - - let cursors = extract_multiple_cursor_positions_from_spans(&db, top_mod); - - let mut cursor_paths: Vec<(Cursor, String)> = vec![]; - - for cursor in &cursors { - let mut visitor_ctxt = VisitorCtxt::with_top_mod(&db, top_mod); - let mut path_collector = PathSpanCollector::default(); - path_collector.visit_top_mod(&mut visitor_ctxt, top_mod); - - let full_paths = path_collector.paths; - - if let Some((path, _, scope)) = find_path_surrounding_cursor(&db, *cursor, full_paths) { - let resolved_enclosing_path = - resolve_path(&db, path, scope, PredicateListId::empty_list(&db), false); - - let res = match resolved_enclosing_path { - Ok(res) => res.pretty_path(&db).unwrap(), - Err(err) => match err.kind { - PathResErrorKind::Ambiguous(vec) => vec - .iter() - .map(|r| r.pretty_path(&db).unwrap()) - .collect::>() - .join("\n"), - _ => "".into(), - }, - }; - cursor_paths.push((*cursor, res)); - } - } - - let result = format!( - "{}\n---\n{}", - fixture.content(), - cursor_paths - .iter() - .map(|(cursor, path)| { format!("cursor position: {cursor:?}, path: {path}") }) - .collect::>() - .join("\n") - ); - snap_test!(result, fixture.path()); + // Use unified SemanticQuery API + let mut locs: Vec = Vec::new(); + let query = SemanticQuery::at_cursor(&backend.db, top_mod, cursor); + let candidates = query.goto_definition(); + for def in candidates.into_iter() { + if let Some(span) = def.span.resolve(&backend.db) { + let url = span.file.url(&backend.db).expect("Failed to get file URL"); + let range = crate::util::to_lsp_range_from_span(span, &backend.db).map_err(|e| { + ResponseError::new(async_lsp::ErrorCode::INTERNAL_ERROR, format!("{e}")) + })?; + locs.push(async_lsp::lsp_types::Location { uri: url, range }); + } + } + match locs.len() { + 0 => Ok(None), + 1 => Ok(Some(async_lsp::lsp_types::GotoDefinitionResponse::Scalar( + locs.remove(0), + ))), + _ => Ok(Some(async_lsp::lsp_types::GotoDefinitionResponse::Array( + locs, + ))), } } diff --git a/crates/language-server/src/functionality/handlers.rs b/crates/language-server/src/functionality/handlers.rs index de16ef03b4..a0f11e8a25 100644 --- a/crates/language-server/src/functionality/handlers.rs +++ b/crates/language-server/src/functionality/handlers.rs @@ -21,7 +21,9 @@ use tracing::{error, info, warn}; pub struct FilesNeedDiagnostics(pub Vec); #[derive(Debug)] -pub struct NeedsDiagnostics(pub url::Url); +pub struct NeedsDiagnostics { + pub uri: url::Url, +} impl std::fmt::Display for FilesNeedDiagnostics { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -31,7 +33,7 @@ impl std::fmt::Display for FilesNeedDiagnostics { impl std::fmt::Display for NeedsDiagnostics { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "FileNeedsDiagnostics({})", self.0) + write!(f, "FileNeedsDiagnostics({})", self.uri) } } @@ -148,7 +150,7 @@ pub async fn initialized( .collect(); for url in all_files { - let _ = backend.client.emit(NeedsDiagnostics(url)); + let _ = backend.client.emit(NeedsDiagnostics { uri: url }); } let _ = backend.client.clone().log_message(LogMessageParams { @@ -315,7 +317,7 @@ pub async fn handle_file_change( } } - let _ = backend.client.emit(NeedsDiagnostics(message.uri)); + let _ = backend.client.emit(NeedsDiagnostics { uri: message.uri }); Ok(()) } @@ -352,7 +354,7 @@ async fn load_ingot_files( .collect(); for url in all_files { - let _ = backend.client.emit(NeedsDiagnostics(url)); + let _ = backend.client.emit(NeedsDiagnostics { uri: url }); } Ok(()) @@ -367,7 +369,7 @@ pub async fn handle_files_need_diagnostics( let ingots_need_diagnostics: FxHashSet<_> = need_diagnostics .iter() - .filter_map(|NeedsDiagnostics(url)| { + .filter_map(|NeedsDiagnostics { uri: url }| { // url is already a url::Url backend .db diff --git a/crates/language-server/src/functionality/hover.rs b/crates/language-server/src/functionality/hover.rs index 0b59980bd7..9b98b5d570 100644 --- a/crates/language-server/src/functionality/hover.rs +++ b/crates/language-server/src/functionality/hover.rs @@ -2,13 +2,12 @@ use anyhow::Error; use async_lsp::lsp_types::Hover; use common::file::File; +use fe_semantic_query::SemanticQuery; use hir::lower::map_file_to_mod; +use hir::span::LazySpan; use tracing::info; -use super::{ - goto::{get_goto_target_scopes_for_cursor, Cursor}, - item_info::{get_docstring, get_item_definition_markdown, get_item_path_markdown}, -}; +use super::goto::Cursor; use crate::util::to_offset_from_position; use driver::DriverDataBase; @@ -26,36 +25,36 @@ pub fn hover_helper( ); let top_mod = map_file_to_mod(db, file); - let goto_info = &get_goto_target_scopes_for_cursor(db, top_mod, cursor).unwrap_or_default(); - let scopes_info = goto_info - .iter() - .map(|scope| { - let item = scope.item(); - let pretty_path = get_item_path_markdown(db, item); - let definition_source = get_item_definition_markdown(db, item); - let docs = get_docstring(db, *scope); - - let result = [pretty_path, definition_source, docs] - .iter() - .filter_map(|info| info.clone().map(|info| format!("{info}\n"))) - .collect::>() - .join("\n"); - - result - }) - .collect::>(); - - let info = scopes_info.join("\n---\n"); - - let result = async_lsp::lsp_types::Hover { - contents: async_lsp::lsp_types::HoverContents::Markup( - async_lsp::lsp_types::MarkupContent { - kind: async_lsp::lsp_types::MarkupKind::Markdown, - value: info, - }, - ), - range: None, - }; - Ok(Some(result)) + // Use unified SemanticQuery API + let query = SemanticQuery::at_cursor(db, top_mod, cursor); + if let Some(h) = query.hover_info() { + let mut parts: Vec = Vec::new(); + if let Some(sig) = h.signature { + parts.push(format!("```fe\n{}\n```", sig)); + } + if let Some(doc) = h.documentation { + parts.push(doc); + } + let value = if parts.is_empty() { + String::new() + } else { + parts.join("\n\n") + }; + let range = h + .span + .resolve(db) + .and_then(|sp| crate::util::to_lsp_range_from_span(sp, db).ok()); + let result = async_lsp::lsp_types::Hover { + contents: async_lsp::lsp_types::HoverContents::Markup( + async_lsp::lsp_types::MarkupContent { + kind: async_lsp::lsp_types::MarkupKind::Markdown, + value, + }, + ), + range, + }; + return Ok(Some(result)); + } + Ok(None) } diff --git a/crates/language-server/src/functionality/implementations.rs b/crates/language-server/src/functionality/implementations.rs new file mode 100644 index 0000000000..a3c41c296c --- /dev/null +++ b/crates/language-server/src/functionality/implementations.rs @@ -0,0 +1,63 @@ +use async_lsp::ResponseError; +use common::InputDb; +use fe_semantic_query::SemanticQuery; +use hir::{lower::map_file_to_mod, span::LazySpan}; + +use crate::{backend::Backend, util::to_offset_from_position}; + +pub type Cursor = parser::TextSize; + +// Custom LSP request for goto implementation +pub enum GotoImplementation {} + +impl async_lsp::lsp_types::request::Request for GotoImplementation { + type Params = async_lsp::lsp_types::TextDocumentPositionParams; + type Result = Option; + const METHOD: &'static str = "textDocument/implementation"; +} + +pub async fn handle_goto_implementation( + backend: &Backend, + params: async_lsp::lsp_types::TextDocumentPositionParams, +) -> Result, ResponseError> { + // Use URI directly to avoid path/encoding/case issues + let url = params.text_document.uri.clone(); + let file = backend + .db + .workspace() + .get(&backend.db, &url) + .ok_or_else(|| { + ResponseError::new( + async_lsp::ErrorCode::INTERNAL_ERROR, + format!("File not found in index: {url}"), + ) + })?; + let file_text = file.text(&backend.db); + let cursor: Cursor = to_offset_from_position(params.position, file_text.as_str()); + let top_mod = map_file_to_mod(&backend.db, file); + + // Use unified SemanticQuery API + let query = SemanticQuery::at_cursor(&backend.db, top_mod, cursor); + let implementations = query.find_implementations(); + + let mut locs: Vec = Vec::new(); + for impl_def in implementations { + if let Some(span) = impl_def.span.resolve(&backend.db) { + let url = span.file.url(&backend.db).expect("Failed to get file URL"); + let range = crate::util::to_lsp_range_from_span(span, &backend.db).map_err(|e| { + ResponseError::new(async_lsp::ErrorCode::INTERNAL_ERROR, format!("{e}")) + })?; + locs.push(async_lsp::lsp_types::Location { uri: url, range }); + } + } + + match locs.len() { + 0 => Ok(None), + 1 => Ok(Some(async_lsp::lsp_types::GotoDefinitionResponse::Scalar( + locs.into_iter().next().unwrap(), + ))), + _ => Ok(Some(async_lsp::lsp_types::GotoDefinitionResponse::Array( + locs, + ))), + } +} diff --git a/crates/language-server/src/functionality/item_info.rs b/crates/language-server/src/functionality/item_info.rs deleted file mode 100644 index 3553613fe2..0000000000 --- a/crates/language-server/src/functionality/item_info.rs +++ /dev/null @@ -1,59 +0,0 @@ -use hir::{ - hir_def::{scope_graph::ScopeId, Attr, ItemKind}, - span::LazySpan, - HirDb, SpannedHirDb, -}; - -pub fn get_docstring(db: &dyn HirDb, scope: ScopeId) -> Option { - scope - .attrs(db)? - .data(db) - .iter() - .filter_map(|attr| { - if let Attr::DocComment(doc) = attr { - Some(doc.text.data(db).clone()) - } else { - None - } - }) - .reduce(|a, b| a + "\n" + &b) -} - -pub fn get_item_path_markdown(db: &dyn HirDb, item: ItemKind) -> Option { - item.scope() - .pretty_path(db) - .map(|path| format!("```fe\n{path}\n```")) -} - -pub fn get_item_definition_markdown(db: &dyn SpannedHirDb, item: ItemKind) -> Option { - // TODO: use pending AST features to get the definition without all this text manipulation - let span = item.span().resolve(db)?; - - let mut start: usize = span.range.start().into(); - let mut end: usize = span.range.end().into(); - - // if the item has a body or children, cut that stuff out - let body_start = match item { - ItemKind::Func(func) => Some(func.body(db)?.span().resolve(db)?.range.start()), - ItemKind::Mod(module) => Some(module.scope().name_span(db)?.resolve(db)?.range.end()), - // TODO: handle other item types - _ => None, - }; - if let Some(body_start) = body_start { - end = body_start.into(); - } - - // let's start at the beginning of the line where the name is defined - let name_span = item.name_span()?.resolve(db); - if let Some(name_span) = name_span { - let mut name_line_start = name_span.range.start().into(); - let file_text = span.file.text(db).as_str(); - while name_line_start > 0 && file_text.chars().nth(name_line_start - 1).unwrap() != '\n' { - name_line_start -= 1; - } - start = name_line_start; - } - - let item_definition = span.file.text(db).as_str()[start..end].to_string(); - Some(format!("```fe\n{}\n```", item_definition.trim())) -} diff --git a/crates/language-server/src/functionality/mod.rs b/crates/language-server/src/functionality/mod.rs index 42b8d45bee..df3541f401 100644 --- a/crates/language-server/src/functionality/mod.rs +++ b/crates/language-server/src/functionality/mod.rs @@ -2,4 +2,6 @@ mod capabilities; pub(super) mod goto; pub(super) mod handlers; pub(super) mod hover; -pub(super) mod item_info; +pub(super) mod implementations; +pub(super) mod references; +pub(super) mod rename; diff --git a/crates/language-server/src/functionality/references.rs b/crates/language-server/src/functionality/references.rs new file mode 100644 index 0000000000..93e7c430fa --- /dev/null +++ b/crates/language-server/src/functionality/references.rs @@ -0,0 +1,55 @@ +use async_lsp::lsp_types::{Location, ReferenceParams}; +use async_lsp::ResponseError; +use common::InputDb; +use fe_semantic_query::SemanticQuery; +use hir::{lower::map_file_to_mod, span::LazySpan}; + +use crate::{backend::Backend, util::to_offset_from_position}; + +pub async fn handle_references( + backend: &Backend, + params: ReferenceParams, +) -> Result>, ResponseError> { + // Locate file and module and convert position to offset using workspace content + // Use the URI directly to avoid path/encoding issues + let url = params.text_document_position.text_document.uri.clone(); + let file = backend + .db + .workspace() + .get(&backend.db, &url) + .ok_or_else(|| { + ResponseError::new( + async_lsp::ErrorCode::INTERNAL_ERROR, + format!("File not found: {url}"), + ) + })?; + let file_text = file.text(&backend.db); + let cursor = + to_offset_from_position(params.text_document_position.position, file_text.as_str()); + let top_mod = map_file_to_mod(&backend.db, file); + + // Use unified SemanticQuery API + let query = SemanticQuery::at_cursor(&backend.db, top_mod, cursor); + let mut found = query + .find_references() + .into_iter() + .filter_map(|r| r.span.resolve(&backend.db)) + .filter_map(|sp| { + crate::util::to_lsp_range_from_span(sp.clone(), &backend.db) + .ok() + .map(|range| (sp, range)) + }) + .map(|(sp, range)| Location { + uri: sp.file.url(&backend.db).expect("url"), + range, + }) + .collect::>(); + + // TODO: Honor includeDeclaration: if false, remove the def location when present + // This would require exposing definition lookup on SemanticQuery + // Deduplicate identical locations + found.sort_by_key(|l| (l.uri.clone(), l.range.start, l.range.end)); + found.dedup_by(|a, b| a.uri == b.uri && a.range == b.range); + + Ok(Some(found)) +} diff --git a/crates/language-server/src/functionality/rename.rs b/crates/language-server/src/functionality/rename.rs new file mode 100644 index 0000000000..032b482933 --- /dev/null +++ b/crates/language-server/src/functionality/rename.rs @@ -0,0 +1,78 @@ +use async_lsp::ResponseError; +use common::InputDb; +use fe_semantic_query::SemanticQuery; +use hir::{lower::map_file_to_mod, span::LazySpan}; +use std::collections::HashMap; + +use crate::{backend::Backend, util::to_offset_from_position}; + +pub type Cursor = parser::TextSize; + +// Custom LSP request for rename +pub enum Rename {} + +impl async_lsp::lsp_types::request::Request for Rename { + type Params = async_lsp::lsp_types::RenameParams; + type Result = Option; + const METHOD: &'static str = "textDocument/rename"; +} + +pub async fn handle_rename( + backend: &Backend, + params: async_lsp::lsp_types::RenameParams, +) -> Result, ResponseError> { + // Use URI directly to avoid path/encoding/case issues + let url = params.text_document_position.text_document.uri.clone(); + let file = backend + .db + .workspace() + .get(&backend.db, &url) + .ok_or_else(|| { + ResponseError::new( + async_lsp::ErrorCode::INTERNAL_ERROR, + format!("File not found in index: {url}"), + ) + })?; + let file_text = file.text(&backend.db); + let cursor: Cursor = + to_offset_from_position(params.text_document_position.position, file_text.as_str()); + let top_mod = map_file_to_mod(&backend.db, file); + + // Use unified SemanticQuery API + let query = SemanticQuery::at_cursor(&backend.db, top_mod, cursor); + let rename_locations = query.find_rename_locations(); + + if rename_locations.is_empty() { + return Ok(None); + } + + // Group edits by file URL + let mut changes: HashMap> = + HashMap::new(); + + for location in rename_locations { + if let Some(span) = location.span.resolve(&backend.db) { + let file_url = span.file.url(&backend.db).expect("Failed to get file URL"); + let range = crate::util::to_lsp_range_from_span(span, &backend.db).map_err(|e| { + ResponseError::new(async_lsp::ErrorCode::INTERNAL_ERROR, format!("{e}")) + })?; + + let text_edit = async_lsp::lsp_types::TextEdit { + range, + new_text: params.new_name.clone(), + }; + + changes.entry(file_url).or_default().push(text_edit); + } + } + + if changes.is_empty() { + Ok(None) + } else { + Ok(Some(async_lsp::lsp_types::WorkspaceEdit { + changes: Some(changes), + document_changes: None, + change_annotations: None, + })) + } +} diff --git a/crates/language-server/src/lsp_diagnostics.rs b/crates/language-server/src/lsp_diagnostics.rs index cc5ce28293..afc8dbf52a 100644 --- a/crates/language-server/src/lsp_diagnostics.rs +++ b/crates/language-server/src/lsp_diagnostics.rs @@ -3,7 +3,6 @@ use camino::Utf8Path; use codespan_reporting::files as cs_files; use common::{diagnostics::CompleteDiagnostic, file::File}; use driver::DriverDataBase; -use hir::lower::map_file_to_mod; use hir::Ingot; use hir_analysis::analysis_pass::{AnalysisPassManager, ParsingPass}; use hir_analysis::name_resolution::ImportAnalysisPass; @@ -33,11 +32,16 @@ impl LspDiagnostics for DriverDataBase { let ingot_files = ingot.files(self); for (url, file) in ingot_files.iter() { + // Only analyze source files; skip config and non-source entries + if file.kind(self) != Some(common::file::IngotFileKind::Source) { + continue; + } + // initialize an empty diagnostic list for this file // (to clear any previous diagnostics) result.entry(url.clone()).or_default(); - let top_mod = map_file_to_mod(self, file); + let top_mod = hir::lower::map_file_to_mod(self, file); let diagnostics = pass_manager.run_on_module(self, top_mod); let mut finalized_diags: Vec = diagnostics .iter() @@ -134,3 +138,105 @@ fn initialize_analysis_pass() -> AnalysisPassManager { pass_manager.add_module_pass(Box::new(BodyAnalysisPass {})); pass_manager } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::load_ingot_from_directory; + use async_lsp::lsp_types::{Diagnostic, NumberOrString}; + use common::InputDb; + use driver::DriverDataBase; + use std::collections::BTreeMap; + use std::path::PathBuf; + use test_utils::snap_test; + use url::Url; + + fn code_to_string(code: &Option) -> String { + match code { + Some(NumberOrString::String(s)) => s.clone(), + Some(NumberOrString::Number(n)) => n.to_string(), + None => String::new(), + } + } + + // Produce a stable, human-readable snapshot of diagnostics per file. + fn format_diagnostics(map: &rustc_hash::FxHashMap>) -> String { + // Sort by URI for determinism + let mut by_uri: BTreeMap> = BTreeMap::new(); + for (uri, diags) in map.iter() { + by_uri + .entry(uri.to_string()) + .or_default() + .extend(diags.iter().cloned()); + } + + let mut out = String::new(); + for (uri, mut diags) in by_uri { + // Stable sort: code, start line/char, end line/char, message prefix + diags.sort_by(|a, b| { + let ac = code_to_string(&a.code); + let bc = code_to_string(&b.code); + ( + ac, + a.range.start.line, + a.range.start.character, + a.range.end.line, + a.range.end.character, + a.message.clone(), + ) + .cmp(&( + bc, + b.range.start.line, + b.range.start.character, + b.range.end.line, + b.range.end.character, + b.message.clone(), + )) + }); + + out.push_str(&format!("File: {}\n", uri)); + for d in diags { + let code = code_to_string(&d.code); + let sev = d.severity.map(|s| format!("{:?}", s)).unwrap_or_default(); + out.push_str(&format!( + " - code:{} severity:{} @ {}:{}..{}:{}\n {}\n", + code, + sev, + d.range.start.line + 1, + d.range.start.character + 1, + d.range.end.line + 1, + d.range.end.character + 1, + d.message.trim() + )); + } + out.push('\n'); + } + out + } + + #[test] + fn diagnostics_snapshot_comprehensive_project() { + let mut db = DriverDataBase::default(); + let project_dir: PathBuf = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_projects/comprehensive"); + + // Load the test project into the DB (mirrors server initialization) + load_ingot_from_directory(&mut db, &project_dir); + + // Find the ingot from the directory URL + let ingot_url = + Url::from_directory_path(&project_dir).expect("failed to convert project dir to URL"); + let ingot = db + .workspace() + .containing_ingot(&db, ingot_url) + .expect("ingot should be discoverable"); + + // Compute diagnostics per file using the server path + let map = db.diagnostics_for_ingot(ingot); + let snapshot = format_diagnostics(&map); + + // Write snapshot alongside the project, like other dir-test layouts + let snap_path = project_dir.join("diagnostics.snap"); + snap_test!(snapshot, snap_path.to_str().unwrap()); + } +} diff --git a/crates/language-server/src/server.rs b/crates/language-server/src/server.rs index f0b429113a..77830bbdb9 100644 --- a/crates/language-server/src/server.rs +++ b/crates/language-server/src/server.rs @@ -9,7 +9,7 @@ use async_lsp::lsp_types::notification::{ self, DidChangeTextDocument, DidChangeWatchedFiles, DidOpenTextDocument, DidSaveTextDocument, Initialized, }; -use async_lsp::lsp_types::request::{GotoDefinition, HoverRequest, Shutdown}; +use async_lsp::lsp_types::request::{GotoDefinition, HoverRequest, References, Shutdown}; use async_lsp::ClientSocket; use async_std::stream::StreamExt; use futures_batch::ChunksTimeoutStreamExt; @@ -42,6 +42,13 @@ pub(crate) fn setup( // mutating handlers .handle_request_mut::(handlers::initialize) .handle_request_mut::(goto::handle_goto_definition) + .handle_request::(crate::functionality::references::handle_references) + .handle_request::( + crate::functionality::implementations::handle_goto_implementation, + ) + .handle_request::( + crate::functionality::rename::handle_rename, + ) .handle_event_mut::(handlers::handle_file_change) .handle_event::(handlers::handle_files_need_diagnostics) // non-mutating handlers @@ -64,6 +71,7 @@ pub(crate) fn setup( fn setup_streams(client: ClientSocket, router: &mut Router<()>) { info!("setting up streams"); + // Simple approach: just use the file-based diagnostics API which is already safer let mut diagnostics_stream = router .event_stream::() .chunks_timeout(500, std::time::Duration::from_millis(30)) diff --git a/crates/language-server/src/test_utils.rs b/crates/language-server/src/test_utils.rs index 1a82455013..b24df6fcc9 100644 --- a/crates/language-server/src/test_utils.rs +++ b/crates/language-server/src/test_utils.rs @@ -21,7 +21,7 @@ pub fn load_ingot_from_directory(db: &mut DriverDataBase, ingot_dir: &Path) { } _ => { // Log other diagnostics but don't panic - eprintln!("Test ingot diagnostic for {ingot_dir:?}: {diagnostic}"); + tracing::debug!("Test ingot diagnostic for {ingot_dir:?}: {diagnostic}"); } } } diff --git a/crates/language-server/src/util.rs b/crates/language-server/src/util.rs index b3004d3311..7c27d8f472 100644 --- a/crates/language-server/src/util.rs +++ b/crates/language-server/src/util.rs @@ -5,7 +5,7 @@ use common::{ diagnostics::{CompleteDiagnostic, Severity, Span}, InputDb, }; -use hir::{hir_def::scope_graph::ScopeId, span::LazySpan, SpannedHirDb}; +// (hir scope helpers no longer used here) use rustc_hash::FxHashMap; use tracing::error; @@ -53,14 +53,7 @@ pub fn to_lsp_range_from_span( }) } -pub fn to_lsp_location_from_scope( - db: &dyn SpannedHirDb, - scope: ScopeId, -) -> Result> { - let lazy_span = scope.name_span(db).ok_or("Failed to get name span")?; - let span = lazy_span.resolve(db).ok_or("Failed to resolve span")?; - to_lsp_location_from_span(db, span) -} +// (removed unused to_lsp_location_from_scope) pub fn severity_to_lsp(is_primary: bool, severity: Severity) -> DiagnosticSeverity { // We set the severity to `HINT` for a secondary diags. diff --git a/crates/language-server/test_files/goto.fe b/crates/language-server/test_files/goto.fe deleted file mode 100644 index 39d020cc88..0000000000 --- a/crates/language-server/test_files/goto.fe +++ /dev/null @@ -1,15 +0,0 @@ -use core - -struct Foo {} -struct Bar {} - -fn main() { - let x: Foo - let y: Bar - let z: baz::Baz - core::todo() -} - -mod baz { - pub struct Baz {} -} diff --git a/crates/language-server/test_files/goto.snap b/crates/language-server/test_files/goto.snap deleted file mode 100644 index 90ed091800..0000000000 --- a/crates/language-server/test_files/goto.snap +++ /dev/null @@ -1,27 +0,0 @@ ---- -source: crates/language-server/src/functionality/goto.rs -expression: snapshot -input_file: test_files/goto.fe ---- -0: use core -1: -2: struct Foo {} -3: struct Bar {} -4: -5: fn main() { -6: let x: Foo -7: let y: Bar -8: let z: baz::Baz -9: core::todo() -10: } -11: -12: mod baz { -13: pub struct Baz {} -14: } ---- -cursor position (6, 11), path: goto::Foo -cursor position (7, 11), path: goto::Bar -cursor position (8, 11), path: goto::baz -cursor position (8, 16), path: goto::baz::Baz -cursor position (9, 4), path: lib -cursor position (9, 10), path: lib::todo diff --git a/crates/language-server/test_files/lol.fe b/crates/language-server/test_files/lol.fe deleted file mode 100644 index f08c02f075..0000000000 --- a/crates/language-server/test_files/lol.fe +++ /dev/null @@ -1,12 +0,0 @@ -struct Foo {} -struct Bar {} - -fn main() { - let x: Foo - let y: Barrr - let z: baz::Bazzz -} - -mod baz { - pub struct Baz {} -} \ No newline at end of file diff --git a/crates/language-server/test_files/messy/dangling.fe b/crates/language-server/test_files/messy/dangling.fe deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/language-server/test_files/messy/foo/bar/fe.toml b/crates/language-server/test_files/messy/foo/bar/fe.toml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/language-server/test_files/messy/foo/bar/src/main.fe b/crates/language-server/test_files/messy/foo/bar/src/main.fe deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/language-server/test_files/nested_ingots/fe.toml b/crates/language-server/test_files/nested_ingots/fe.toml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/language-server/test_files/nested_ingots/ingots/foo/fe.toml b/crates/language-server/test_files/nested_ingots/ingots/foo/fe.toml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/language-server/test_files/nested_ingots/ingots/foo/src/main.fe b/crates/language-server/test_files/nested_ingots/ingots/foo/src/main.fe deleted file mode 100644 index 5b5a7b8335..0000000000 --- a/crates/language-server/test_files/nested_ingots/ingots/foo/src/main.fe +++ /dev/null @@ -1 +0,0 @@ -let foo = 1; \ No newline at end of file diff --git a/crates/language-server/test_files/nested_ingots/src/lib.fe b/crates/language-server/test_files/nested_ingots/src/lib.fe deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/language-server/test_files/single_ingot/fe.toml b/crates/language-server/test_files/single_ingot/fe.toml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/language-server/test_files/single_ingot/src/foo.fe b/crates/language-server/test_files/single_ingot/src/foo.fe deleted file mode 100644 index 99e9264c32..0000000000 --- a/crates/language-server/test_files/single_ingot/src/foo.fe +++ /dev/null @@ -1,8 +0,0 @@ -pub fn why() { - let x = 5 - x -} - -pub struct Why { - pub x: i32 -} \ No newline at end of file diff --git a/crates/language-server/test_files/single_ingot/src/lib.fe b/crates/language-server/test_files/single_ingot/src/lib.fe deleted file mode 100644 index 5669526f6d..0000000000 --- a/crates/language-server/test_files/single_ingot/src/lib.fe +++ /dev/null @@ -1,23 +0,0 @@ -use ingot::foo::Why - -mod who { - use super::Why - pub mod what { - pub fn how() {} - pub mod how { - use ingot::Why - pub struct When { - x: Why - } - } - } - pub struct Bar { - x: Why - } -} - -fn bar() -> () { - let y: Why - let z = who::what::how - let z: who::what::how::When -} \ No newline at end of file diff --git a/crates/language-server/test_files/single_ingot/src/lib.snap b/crates/language-server/test_files/single_ingot/src/lib.snap deleted file mode 100644 index a6b73ab514..0000000000 --- a/crates/language-server/test_files/single_ingot/src/lib.snap +++ /dev/null @@ -1,39 +0,0 @@ ---- -source: crates/language-server/src/functionality/goto.rs -expression: snapshot -input_file: crates/language-server/test_files/single_ingot/src/lib.fe ---- -0: use ingot::foo::Why -1: -2: mod who { -3: use super::Why -4: pub mod what { -5: pub fn how() {} -6: pub mod how { -7: use ingot::Why -8: pub struct When { -9: x: Why -10: } -11: } -12: } -13: pub struct Bar { -14: x: Why -15: } -16: } -17: -18: fn bar() -> () { -19: let y: Why -20: let z = who::what::how -21: let z: who::what::how::When -22: } ---- -cursor position (9, 11), path: lib::foo::Why -cursor position (14, 7), path: lib::foo::Why -cursor position (19, 11), path: lib::foo::Why -cursor position (20, 12), path: lib::who -cursor position (20, 17), path: lib::who::what -cursor position (20, 23), path: lib::who::what::how -cursor position (21, 11), path: lib::who -cursor position (21, 16), path: lib::who::what -cursor position (21, 22), path: lib::who::what::how -cursor position (21, 27), path: lib::who::what::how::When diff --git a/crates/language-server/test_files/smallest_enclosing.fe b/crates/language-server/test_files/smallest_enclosing.fe deleted file mode 100644 index fa1ae4c2ff..0000000000 --- a/crates/language-server/test_files/smallest_enclosing.fe +++ /dev/null @@ -1,7 +0,0 @@ -struct Foo {} -struct Bar {} - -fn main() { - let x: Foo - let y: Bar -} \ No newline at end of file diff --git a/crates/language-server/test_files/smallest_enclosing.snap b/crates/language-server/test_files/smallest_enclosing.snap deleted file mode 100644 index ba892314a7..0000000000 --- a/crates/language-server/test_files/smallest_enclosing.snap +++ /dev/null @@ -1,17 +0,0 @@ ---- -source: crates/language-server/src/functionality/goto.rs -expression: result -input_file: test_files/smallest_enclosing.fe ---- -struct Foo {} -struct Bar {} - -fn main() { - let x: Foo - let y: Bar -} ---- -cursor position: 49, path: -cursor position: 52, path: smallest_enclosing::Foo -cursor position: 64, path: -cursor position: 67, path: smallest_enclosing::Bar diff --git a/crates/language-server/test_projects/comprehensive/diagnostics.snap b/crates/language-server/test_projects/comprehensive/diagnostics.snap new file mode 100644 index 0000000000..cf7efa67b0 --- /dev/null +++ b/crates/language-server/test_projects/comprehensive/diagnostics.snap @@ -0,0 +1,9 @@ +--- +source: crates/language-server/src/lsp_diagnostics.rs +expression: snapshot +input_file: test_projects/comprehensive/diagnostics.snap +--- +File: file:///home/micah/hacker-stuff-2023/fe-stuff/fe-B/crates/language-server/test_projects/comprehensive/src/lib.fe + - code:8-0000 severity:Error @ 38:3..38:8 + type mismatch +expected `()`, but `i32` is given diff --git a/crates/language-server/test_projects/comprehensive/fe.toml b/crates/language-server/test_projects/comprehensive/fe.toml new file mode 100644 index 0000000000..d88515c7a1 --- /dev/null +++ b/crates/language-server/test_projects/comprehensive/fe.toml @@ -0,0 +1,4 @@ +[ingot] +name = "comprehensive" +version = "0.1.0" + diff --git a/crates/language-server/test_projects/comprehensive/src/lib.fe b/crates/language-server/test_projects/comprehensive/src/lib.fe new file mode 100644 index 0000000000..147b795493 --- /dev/null +++ b/crates/language-server/test_projects/comprehensive/src/lib.fe @@ -0,0 +1,40 @@ +/// A comprehensive single-project fixture to exercise diagnostics + +mod stuff { + pub mod calculations { + pub fn return_three() -> i32 { 3 } + pub fn return_four() -> i32 { 4 } + pub fn return_five() -> i32 { 5 } + + /// Intentionally ambiguous: both a module and a function named `ambiguous` + pub mod ambiguous { } + pub fn ambiguous() {} + } + + pub mod shapes { + pub struct Point { x: i32, y: i32 } + + pub trait ContainerTrait { + fn get(self) -> i32 + } + + impl ContainerTrait for Point { + // Intentionally written with `self` usage to exercise method diagnostics + fn get(self) -> i32 { self.x + self.y } + } + } +} + +use stuff::calculations::return_three +use stuff::calculations::return_four +use stuff::calculations::ambiguous + +pub fn compute() { + let x = return_three() + let y = return_four() + // call the function named ambiguous, not the module + ambiguous() + // simple arithmetic to keep values used + x + y +} + diff --git a/crates/semantic-query/Cargo.toml b/crates/semantic-query/Cargo.toml new file mode 100644 index 0000000000..189b6f8c47 --- /dev/null +++ b/crates/semantic-query/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "fe-semantic-query" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/ethereum/fe" +description = "High-level semantic query orchestration for Fe" + +[lib] +doctest = false + +[dependencies] +common.workspace = true +parser.workspace = true +hir.workspace = true +hir-analysis.workspace = true +salsa.workspace = true +tracing.workspace = true +url.workspace = true +rustc-hash.workspace = true + +[dev-dependencies] +async-lsp = { git = "https://github.com/micahscopes/async-lsp", branch = "pub-inner-type-id" } +driver.workspace = true +dir-test.workspace = true +test-utils.workspace = true diff --git a/crates/semantic-query/src/anchor.rs b/crates/semantic-query/src/anchor.rs new file mode 100644 index 0000000000..c9579a56cc --- /dev/null +++ b/crates/semantic-query/src/anchor.rs @@ -0,0 +1,30 @@ +use hir_analysis::diagnostics::SpannedHirAnalysisDb; +use hir_analysis::name_resolution::{resolve_with_policy, DomainPreference}; + +use hir::hir_def::{scope_graph::ScopeId, PathId}; + +pub(crate) fn anchor_for_scope_match<'db>( + db: &'db dyn SpannedHirAnalysisDb, + view: &hir::path_view::HirPathAdapter<'db>, + lazy_path: hir::span::path::LazyPathSpan<'db>, + p: PathId<'db>, + s: ScopeId<'db>, + target_sc: ScopeId<'db>, +) -> hir::span::DynLazySpan<'db> { + use hir_analysis::ty::trait_resolution::PredicateListId; + let assumptions = PredicateListId::empty_list(db); + let tail = p.segment_index(db); + for i in 0..=tail { + let seg_path = p.segment(db, i).unwrap_or(p); + if let Ok(seg_res) = + resolve_with_policy(db, seg_path, s, assumptions, DomainPreference::Either) + { + if seg_res.as_scope(db) == Some(target_sc) { + let anchor = hir::path_anchor::AnchorPicker::pick_visibility_error(view, i); + return hir::path_anchor::map_path_anchor_to_dyn_lazy(lazy_path.clone(), anchor); + } + } + } + let anchor = hir::path_anchor::AnchorPicker::pick_unresolved_tail(view); + hir::path_anchor::map_path_anchor_to_dyn_lazy(lazy_path.clone(), anchor) +} diff --git a/crates/semantic-query/src/hover.rs b/crates/semantic-query/src/hover.rs new file mode 100644 index 0000000000..46b0f24639 --- /dev/null +++ b/crates/semantic-query/src/hover.rs @@ -0,0 +1,116 @@ +use hir_analysis::diagnostics::SpannedHirAnalysisDb; +use hir_analysis::lookup::SymbolKey; + +use hir::hir_def::scope_graph::ScopeId; +use hir::source_index::OccurrencePayload; +use hir::span::DynLazySpan; + +#[derive(Debug, Clone)] +pub struct HoverSemantics<'db> { + pub span: DynLazySpan<'db>, + pub signature: Option, + pub documentation: Option, + pub kind: &'static str, +} + +pub(crate) fn hover_for_occurrence<'db>( + db: &'db dyn SpannedHirAnalysisDb, + occ: &OccurrencePayload<'db>, + top_mod: hir::hir_def::TopLevelMod<'db>, +) -> Option> { + // Use the canonical occurrence interpreter to get the symbol target + let symbol_key = crate::identity::occurrence_symbol_target(db, top_mod, occ)?; + + // Get the span from the occurrence + let span = get_span_from_occurrence(occ); + + // Convert symbol key to hover data + hover_data_from_symbol_key(db, symbol_key, span) +} + +pub(crate) fn get_span_from_occurrence<'db>(occ: &OccurrencePayload<'db>) -> DynLazySpan<'db> { + match occ { + OccurrencePayload::PathSeg { span, .. } + | OccurrencePayload::UsePathSeg { span, .. } + | OccurrencePayload::UseAliasName { span, .. } + | OccurrencePayload::MethodName { span, .. } + | OccurrencePayload::FieldAccessName { span, .. } + | OccurrencePayload::PatternLabelName { span, .. } + | OccurrencePayload::PathExprSeg { span, .. } + | OccurrencePayload::PathPatSeg { span, .. } + | OccurrencePayload::ItemHeaderName { span, .. } => span.clone(), + } +} + +fn hover_data_from_symbol_key<'db>( + db: &'db dyn SpannedHirAnalysisDb, + symbol_key: SymbolKey<'db>, + span: DynLazySpan<'db>, +) -> Option> { + match symbol_key { + SymbolKey::Scope(sc) => { + let signature = sc.pretty_path(db); + let documentation = get_docstring(db, sc); + let kind = sc.kind_name(); + Some(HoverSemantics { + span, + signature, + documentation, + kind, + }) + } + SymbolKey::Method(fd) => { + let meth = fd.name(db).data(db).to_string(); + let signature = Some(format!("method: {}", meth)); + let documentation = get_docstring(db, fd.scope(db)); + Some(HoverSemantics { + span, + signature, + documentation, + kind: "method", + }) + } + SymbolKey::Local(_func, bkey) => { + let signature = Some(format!("local binding: {:?}", bkey)); + Some(HoverSemantics { + span, + signature, + documentation: None, + kind: "local", + }) + } + SymbolKey::FuncParam(item, idx) => { + let signature = Some(format!("parameter {} of {:?}", idx, item)); + Some(HoverSemantics { + span, + signature, + documentation: None, + kind: "parameter", + }) + } + SymbolKey::EnumVariant(v) => { + let sc = v.scope(); + let signature = sc.pretty_path(db); + let documentation = get_docstring(db, sc); + Some(HoverSemantics { + span, + signature, + documentation, + kind: "enum_variant", + }) + } + } +} + +fn get_docstring(db: &dyn hir::HirDb, scope: ScopeId) -> Option { + use hir::hir_def::Attr; + scope + .attrs(db)? + .data(db) + .iter() + .filter_map(|attr| match attr { + Attr::DocComment(doc) => Some(doc.text.data(db).clone()), + _ => None, + }) + .reduce(|a, b| a + "\n" + &b) +} diff --git a/crates/semantic-query/src/identity.rs b/crates/semantic-query/src/identity.rs new file mode 100644 index 0000000000..d967517a11 --- /dev/null +++ b/crates/semantic-query/src/identity.rs @@ -0,0 +1,25 @@ +use hir::{hir_def::TopLevelMod, source_index::OccurrencePayload}; + +use hir_analysis::{diagnostics::SpannedHirAnalysisDb, lookup::SymbolKey}; + +/// Returns all possible symbol targets for an occurrence, including ambiguous cases. +/// Now directly returns SymbolIdentity from hir-analysis without translation. +pub(crate) fn occurrence_symbol_targets<'db>( + db: &'db dyn SpannedHirAnalysisDb, + top_mod: TopLevelMod<'db>, + occ: &OccurrencePayload<'db>, +) -> Vec> { + // Use hir-analysis as the single source of truth for occurrence interpretation + hir_analysis::lookup::identity_for_occurrence(db, top_mod, occ) +} + +/// Returns the first symbol target for an occurrence (backward compatibility). +pub(crate) fn occurrence_symbol_target<'db>( + db: &'db dyn SpannedHirAnalysisDb, + top_mod: TopLevelMod<'db>, + occ: &OccurrencePayload<'db>, +) -> Option> { + occurrence_symbol_targets(db, top_mod, occ) + .into_iter() + .next() +} diff --git a/crates/semantic-query/src/lib.rs b/crates/semantic-query/src/lib.rs new file mode 100644 index 0000000000..4553b9ca06 --- /dev/null +++ b/crates/semantic-query/src/lib.rs @@ -0,0 +1,504 @@ +mod anchor; +mod hover; +mod identity; +mod refs; + +use crate::identity::{occurrence_symbol_target, occurrence_symbol_targets}; +use hir::{ + hir_def::{scope_graph::ScopeId, HirIngot, TopLevelMod}, + source_index::{unified_occurrence_rangemap_for_top_mod, OccurrencePayload}, + span::{DynLazySpan, LazySpan}, +}; +use hir_analysis::diagnostics::SpannedHirAnalysisDb; +use hir_analysis::lookup::SymbolKey; +use parser::TextSize; +use rustc_hash::{FxHashMap, FxHashSet}; + +/// Unified semantic query API. Performs occurrence lookup once and provides +/// all IDE features (goto, hover, references) from that single resolution. +pub struct SemanticQuery<'db> { + db: &'db dyn SpannedHirAnalysisDb, + top_mod: TopLevelMod<'db>, + + // Cached results from single occurrence lookup + occurrence: Option>, + symbol_key: Option>, +} + +impl<'db> SemanticQuery<'db> { + pub fn at_cursor( + db: &'db dyn SpannedHirAnalysisDb, + top_mod: TopLevelMod<'db>, + cursor: TextSize, + ) -> Self { + let occurrence = pick_best_occurrence_at_cursor(db, top_mod, cursor); + let symbol_key = occurrence + .as_ref() + .and_then(|occ| occurrence_symbol_target(db, top_mod, occ)); + + Self { + db, + top_mod, + occurrence, + symbol_key, + } + } + + pub fn goto_definition(&self) -> Vec> { + // Always check for all possible identities (including ambiguous cases) + if let Some(ref occ) = self.occurrence { + let identities = + hir_analysis::lookup::identity_for_occurrence(self.db, self.top_mod, occ); + + let mut definitions = Vec::new(); + for identity in identities { + if let Some((top_mod, span)) = def_span_for_symbol(self.db, identity) { + definitions.push(DefinitionLocation { top_mod, span }); + } + } + return definitions; + } + + Vec::new() + } + + pub fn hover_info(&self) -> Option> { + let occ = self.occurrence.as_ref()?; + let hs = crate::hover::hover_for_occurrence(self.db, occ, self.top_mod)?; + Some(HoverData { + top_mod: self.top_mod, + span: hs.span, + signature: hs.signature, + documentation: hs.documentation, + kind: hs.kind, + }) + } + + pub fn find_references(&self) -> Vec> { + let Some(key) = self.symbol_key else { + return Vec::new(); + }; + find_refs_for_symbol(self.db, self.top_mod, key) + } + + pub fn find_rename_locations(&self) -> Vec> { + let Some(key) = self.symbol_key else { + return Vec::new(); + }; + + // Check for special cases that should block or require special handling + if self.is_rename_blocked(&key) { + return Vec::new(); + } + + // For rename, we want only actual symbol name occurrences, not semantic references + self.find_symbol_occurrences() + } + + pub fn find_symbol_occurrences(&self) -> Vec> { + let Some(key) = self.symbol_key else { + return Vec::new(); + }; + + // Use the shared implementation, filtering for rename-allowed occurrences only + find_refs_for_symbol_with_filter(self.db, self.top_mod, key, true) + } + + pub fn find_implementations(&self) -> Vec> { + let Some(key) = self.symbol_key else { + return Vec::new(); + }; + + match key { + SymbolKey::Method(fd) => { + // Find implementing methods for trait methods + let mut implementations = Vec::new(); + for impl_method in + crate::refs::implementing_methods_for_trait_method(self.db, self.top_mod, fd) + { + if let Some(span) = impl_method.scope(self.db).name_span(self.db) { + if let Some(tm) = span.top_mod(self.db) { + implementations.push(DefinitionLocation { top_mod: tm, span }); + } + } + } + implementations + } + SymbolKey::Scope(scope_id) => { + // Check if this is a trait scope + if let Some(hir::hir_def::ItemKind::Trait(trait_def)) = scope_id.to_item() { + return self.find_trait_implementations(trait_def); + } + Vec::new() + } + _ => { + // For other symbol types, there are no implementations + Vec::new() + } + } + } + + fn find_trait_implementations( + &self, + trait_def: hir::hir_def::item::Trait<'db>, + ) -> Vec> { + let mut implementations = Vec::new(); + + // Find all impl blocks that implement this trait + for impl_trait in self.top_mod.all_impl_traits(self.db) { + let Some(trait_ref) = impl_trait.trait_ref(self.db).to_opt() else { + continue; + }; + let hir::hir_def::Partial::Present(path) = trait_ref.path(self.db) else { + continue; + }; + + // Resolve the trait reference to see if it matches our trait + let assumptions = + hir_analysis::ty::trait_resolution::PredicateListId::empty_list(self.db); + let Ok(hir_analysis::name_resolution::PathRes::Trait(trait_inst)) = + hir_analysis::name_resolution::resolve_with_policy( + self.db, + path, + impl_trait.scope(), + assumptions, + hir_analysis::name_resolution::DomainPreference::Type, + ) + else { + continue; + }; + + if trait_inst.def(self.db).trait_(self.db) == trait_def { + // This impl block implements our trait - use the trait_ref span + let span = impl_trait.span().trait_ref(); + if let Some(tm) = span.top_mod(self.db) { + implementations.push(DefinitionLocation { + top_mod: tm, + span: hir::span::DynLazySpan::from(span), + }); + } + } + } + + implementations + } + + pub fn symbol_key(&self) -> Option> { + self.symbol_key + } + + /// Check if renaming this symbol should be blocked or requires special handling + fn is_rename_blocked(&self, key: &SymbolKey<'db>) -> bool { + match key { + SymbolKey::Scope(scope_id) => { + // Check if this is a module scope that might need special handling + if let Some(item) = scope_id.to_item() { + if let hir::hir_def::ItemKind::Mod(_mod_def) = item { + // Get the module name to check for special cases + if let Some(name) = item.name(self.db) { + let name_str = name.data(self.db); + + // Block renaming if this looks like an ingot root or special module + if name_str == "ingot" || name_str == "main" || name_str == "lib" { + return true; + } + } + + // For now, block all module renames as they may require file system operations + // TODO: In the future, implement proper module rename with file operations + return true; + } + } + false + } + SymbolKey::Method(_func_def) => { + // Allow renaming all functions including main + false + } + // Allow renaming other symbols (EnumVariant, FuncParam, Local) + _ => false, + } + } + + // Test support methods + pub fn definition_for_symbol( + db: &'db dyn SpannedHirAnalysisDb, + key: SymbolKey<'db>, + ) -> Option<(TopLevelMod<'db>, DynLazySpan<'db>)> { + def_span_for_symbol(db, key) + } + + pub fn references_for_symbol( + db: &'db dyn SpannedHirAnalysisDb, + top_mod: TopLevelMod<'db>, + key: SymbolKey<'db>, + ) -> Vec> { + find_refs_for_symbol(db, top_mod, key) + } + + pub fn build_symbol_index_for_modules( + db: &'db dyn SpannedHirAnalysisDb, + modules: &[TopLevelMod<'db>], + ) -> FxHashMap, Vec>> { + let mut map: FxHashMap, Vec>> = FxHashMap::default(); + for &m in modules { + for occ in unified_occurrence_rangemap_for_top_mod(db, m).iter() { + // Skip header occurrences - we only want references, not definitions + if matches!(&occ.payload, OccurrencePayload::ItemHeaderName { .. }) { + continue; + } + + // Use the canonical occurrence interpreter to get all symbol targets (including ambiguous) + let targets = occurrence_symbol_targets(db, m, &occ.payload); + for target in targets { + let span = compute_reference_span(db, &occ.payload, target, m); + map.entry(target) + .or_default() + .push(Reference { top_mod: m, span }); + } + } + } + map + } +} + +pub struct DefinitionLocation<'db> { + pub top_mod: TopLevelMod<'db>, + pub span: DynLazySpan<'db>, +} + +/// Structured hover data for public API consumption. Semantic, not presentation. +pub struct HoverData<'db> { + pub top_mod: TopLevelMod<'db>, + pub span: DynLazySpan<'db>, + pub signature: Option, + pub documentation: Option, + pub kind: &'static str, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Reference<'db> { + pub top_mod: TopLevelMod<'db>, + pub span: DynLazySpan<'db>, +} + +// Simple helper functions +fn pick_best_occurrence_at_cursor<'db>( + db: &'db dyn hir::SpannedHirDb, + top_mod: TopLevelMod<'db>, + cursor: TextSize, +) -> Option> { + use hir::source_index::occurrences_at_offset; + + let occs = occurrences_at_offset(db, top_mod, cursor); + let mut best: Option<(OccurrencePayload<'db>, TextSize, u8)> = None; + + for occ in occs { + let span = crate::hover::get_span_from_occurrence(&occ); + let w = if let Some(sp) = span.resolve(db) { + sp.range.end() - sp.range.start() + } else { + TextSize::from(1u32) + }; + let pr = kind_priority(&occ); + + match best { + None => best = Some((occ, w, pr)), + Some((_, bw, bpr)) if pr < bpr || (pr == bpr && w < bw) => best = Some((occ, w, pr)), + _ => {} + } + } + + best.map(|(occ, _, _)| occ) +} + +fn kind_priority(occ: &OccurrencePayload<'_>) -> u8 { + match occ { + OccurrencePayload::PathExprSeg { .. } | OccurrencePayload::PathPatSeg { .. } => 0, + OccurrencePayload::MethodName { .. } + | OccurrencePayload::FieldAccessName { .. } + | OccurrencePayload::PatternLabelName { .. } + | OccurrencePayload::UseAliasName { .. } + | OccurrencePayload::UsePathSeg { .. } => 1, + OccurrencePayload::PathSeg { .. } => 2, + OccurrencePayload::ItemHeaderName { .. } => 3, + } +} + +// Definition span lookup - needed by goto +fn def_span_for_symbol<'db>( + db: &'db dyn SpannedHirAnalysisDb, + key: SymbolKey<'db>, +) -> Option<(TopLevelMod<'db>, DynLazySpan<'db>)> { + match key { + SymbolKey::Local(func, bkey) => { + let span = hir_analysis::ty::ty_check::binding_def_span_in_func(db, func, bkey)?; + let tm = span.top_mod(db)?; + Some((tm, span)) + } + SymbolKey::Method(fd) => { + if let Some(span) = fd.scope(db).name_span(db) { + let tm = span.top_mod(db)?; + Some((tm, span)) + } else if let Some(item) = fd.scope(db).to_item() { + let lazy = DynLazySpan::from(item.span()); + let tm = lazy.top_mod(db)?; + Some((tm, lazy)) + } else { + None + } + } + SymbolKey::EnumVariant(v) => { + let sc = v.scope(); + let span = sc.name_span(db)?; + let tm = span.top_mod(db)?; + Some((tm, span)) + } + SymbolKey::Scope(sc) => { + let span = sc.name_span(db)?; + let tm = span.top_mod(db)?; + Some((tm, span)) + } + SymbolKey::FuncParam(item, idx) => { + let sc = ScopeId::FuncParam(item, idx); + let span = sc.name_span(db)?; + let tm = span.top_mod(db)?; + Some((tm, span)) + } + } +} + +// References - needed by find_references +fn find_refs_for_symbol<'db>( + db: &'db dyn SpannedHirAnalysisDb, + top_mod: TopLevelMod<'db>, + key: SymbolKey<'db>, +) -> Vec> { + find_refs_for_symbol_with_filter(db, top_mod, key, false) +} + +fn find_refs_for_symbol_with_filter<'db>( + db: &'db dyn SpannedHirAnalysisDb, + top_mod: TopLevelMod<'db>, + key: SymbolKey<'db>, + only_rename_allowed: bool, +) -> Vec> { + let mut out: Vec> = Vec::new(); + let mut seen: FxHashSet<(common::file::File, parser::TextSize, parser::TextSize)> = + FxHashSet::default(); + + // 1) Always include def-site first when available. + if let Some((tm, def_span)) = def_span_for_symbol(db, key) { + if let Some(sp) = def_span.resolve(db) { + seen.insert((sp.file, sp.range.start(), sp.range.end())); + } + out.push(Reference { + top_mod: tm, + span: def_span, + }); + } + + // 2) Search across all modules in the ingot for references. + for &module in top_mod.ingot(db).all_modules(db) { + for occ in unified_occurrence_rangemap_for_top_mod(db, module).iter() { + // Skip header-name occurrences; def-site is already injected above. + if let OccurrencePayload::ItemHeaderName { .. } = &occ.payload { + continue; + } + + // Resolve occurrence to a symbol identity and anchor appropriately. + let Some(target) = occurrence_symbol_target(db, module, &occ.payload) else { + continue; + }; + // Custom matcher to allow associated functions (scopes) to match method occurrences + let matches = match (key, target) { + (SymbolKey::Scope(sc), SymbolKey::Scope(sc2)) => sc == sc2, + (SymbolKey::Scope(sc), SymbolKey::Method(fd)) => fd.scope(db) == sc, + (SymbolKey::EnumVariant(v), SymbolKey::EnumVariant(v2)) => v == v2, + (SymbolKey::FuncParam(it, idx), SymbolKey::FuncParam(it2, idx2)) => { + it == it2 && idx == idx2 + } + (SymbolKey::Method(fd), SymbolKey::Method(fd2)) => fd == fd2, + (SymbolKey::Local(func, bkey), SymbolKey::Local(func2, bkey2)) => { + func == func2 && bkey == bkey2 + } + _ => false, + }; + if !matches { + continue; + } + + // If filtering for rename operations, check if this occurrence allows renaming + if only_rename_allowed && !occ.payload.rename_allowed(db) { + continue; + } + + let span = compute_reference_span(db, &occ.payload, target, module); + + if let Some(sp) = span.resolve(db) { + let k = (sp.file, sp.range.start(), sp.range.end()); + if !seen.insert(k) { + continue; + } + } + out.push(Reference { + top_mod: module, + span, + }); + } + } + + // 3) Method extras: include implementing method def headers in this module for trait methods. + if let SymbolKey::Method(fd) = key { + for m in crate::refs::implementing_methods_for_trait_method(db, top_mod, fd) { + if let Some(span) = m.scope(db).name_span(db) { + if let Some(sp) = span.resolve(db) { + let k = (sp.file, sp.range.start(), sp.range.end()); + if !seen.insert(k) { + continue; + } + } + if let Some(tm) = span.top_mod(db) { + out.push(Reference { top_mod: tm, span }); + } + } + } + } + + out +} + +fn compute_reference_span<'db>( + db: &'db dyn SpannedHirAnalysisDb, + occ: &OccurrencePayload<'db>, + target: SymbolKey<'db>, + _m: TopLevelMod<'db>, +) -> DynLazySpan<'db> { + match occ { + // For PathSeg, use smart anchoring based on the target + OccurrencePayload::PathSeg { + path, + scope, + path_lazy, + .. + } => { + let view = hir::path_view::HirPathAdapter::new(db, *path); + match target { + SymbolKey::Scope(sc) => crate::anchor::anchor_for_scope_match( + db, + &view, + path_lazy.clone(), + *path, + *scope, + sc, + ), + _ => { + let anchor = hir::path_anchor::AnchorPicker::pick_unresolved_tail(&view); + hir::path_anchor::map_path_anchor_to_dyn_lazy(path_lazy.clone(), anchor) + } + } + } + // For all other occurrence types, use the occurrence's own span + _ => crate::hover::get_span_from_occurrence(occ), + } +} diff --git a/crates/semantic-query/src/refs.rs b/crates/semantic-query/src/refs.rs new file mode 100644 index 0000000000..c47f1f57b4 --- /dev/null +++ b/crates/semantic-query/src/refs.rs @@ -0,0 +1,56 @@ +use hir_analysis::diagnostics::SpannedHirAnalysisDb; +use hir_analysis::ty::{func_def::FuncDef, trait_resolution::PredicateListId}; + +use hir::hir_def::{scope_graph::ScopeId, IdentId, ItemKind, TopLevelMod}; + +pub(crate) fn implementing_methods_for_trait_method<'db>( + db: &'db dyn SpannedHirAnalysisDb, + top_mod: TopLevelMod<'db>, + fd: FuncDef<'db>, +) -> Vec> { + let Some(func) = fd.hir_func_def(db) else { + return Vec::new(); + }; + let Some(parent) = func.scope().parent(db) else { + return Vec::new(); + }; + let trait_item = match parent { + ScopeId::Item(ItemKind::Trait(t)) => t, + _ => return Vec::new(), + }; + let name: IdentId<'db> = fd.name(db); + let assumptions = PredicateListId::empty_list(db); + let mut out = Vec::new(); + for it in top_mod.all_impl_traits(db) { + let Some(tr_ref) = it.trait_ref(db).to_opt() else { + continue; + }; + let hir::hir_def::Partial::Present(path) = tr_ref.path(db) else { + continue; + }; + let Ok(hir_analysis::name_resolution::PathRes::Trait(tr_inst)) = + hir_analysis::name_resolution::resolve_with_policy( + db, + path, + it.scope(), + assumptions, + hir_analysis::name_resolution::DomainPreference::Type, + ) + else { + continue; + }; + if tr_inst.def(db).trait_(db) != trait_item { + continue; + } + for child in it.children_non_nested(db) { + if let ItemKind::Func(impl_fn) = child { + if impl_fn.name(db).to_opt() == Some(name) { + if let Some(fd2) = hir_analysis::ty::func_def::lower_func(db, impl_fn) { + out.push(fd2); + } + } + } + } + } + out +} diff --git a/crates/semantic-query/test-fixtures/local_param_boundary.fe b/crates/semantic-query/test-fixtures/local_param_boundary.fe new file mode 100644 index 0000000000..46a2152caa --- /dev/null +++ b/crates/semantic-query/test-fixtures/local_param_boundary.fe @@ -0,0 +1 @@ +fn main(x: i32) -> i32 { let y = x; return y } \ No newline at end of file diff --git a/crates/semantic-query/test-fixtures/shadow_local.fe b/crates/semantic-query/test-fixtures/shadow_local.fe new file mode 100644 index 0000000000..e451d1ecd0 --- /dev/null +++ b/crates/semantic-query/test-fixtures/shadow_local.fe @@ -0,0 +1 @@ +fn f(x: i32) -> i32 { let x = 1; return x } \ No newline at end of file diff --git a/crates/semantic-query/test_files/ambiguous_last_segment.fe b/crates/semantic-query/test_files/ambiguous_last_segment.fe new file mode 100644 index 0000000000..10754c5225 --- /dev/null +++ b/crates/semantic-query/test_files/ambiguous_last_segment.fe @@ -0,0 +1,6 @@ +mod m { + pub fn ambiguous() {} + pub mod ambiguous {} +} + +use m::ambiguous diff --git a/crates/semantic-query/test_files/ambiguous_last_segment.snap b/crates/semantic-query/test_files/ambiguous_last_segment.snap new file mode 100644 index 0000000000..6d3f9abde4 --- /dev/null +++ b/crates/semantic-query/test_files/ambiguous_last_segment.snap @@ -0,0 +1,32 @@ +--- +source: crates/semantic-query/tests/symbol_keys_snap.rs +expression: out +input_file: test_files/ambiguous_last_segment.snap +--- +Symbol: ambiguous_last_segment::m +help: definitions + references + ┌─ ambiguous_last_segment.fe:1:5 + │ +1 │ mod m { + │ ^ + │ │ + │ def: defined here @ 1:5 (2 refs) + │ ref: 1:5 + · +6 │ use m::ambiguous + │ ^ ref: 6:5 + + + +Symbol: ambiguous_last_segment::m::ambiguous +help: definitions + references + ┌─ ambiguous_last_segment.fe:2:12 + │ +2 │ pub fn ambiguous() {} + │ ^^^^^^^^^ + │ │ + │ def: defined here @ 2:12 (2 refs) + │ ref: 2:12 + · +6 │ use m::ambiguous + │ ^^^^^^^^^ ref: 6:8 diff --git a/crates/semantic-query/test_files/ambiguous_methods.fe b/crates/semantic-query/test_files/ambiguous_methods.fe new file mode 100644 index 0000000000..d442735abf --- /dev/null +++ b/crates/semantic-query/test_files/ambiguous_methods.fe @@ -0,0 +1,22 @@ +struct Container { value: i32 } + +trait TraitA { + fn get(self) -> i32 +} + +trait TraitB { + fn get(self) -> i32 +} + +impl TraitA for Container { + fn get(self) -> i32 { self.value } +} + +impl TraitB for Container { + fn get(self) -> i32 { self.value + 1 } +} + +fn test() { + let c = Container { value: 42 } + let r = c.get() +} diff --git a/crates/semantic-query/test_files/ambiguous_methods.snap b/crates/semantic-query/test_files/ambiguous_methods.snap new file mode 100644 index 0000000000..aec38abfd9 --- /dev/null +++ b/crates/semantic-query/test_files/ambiguous_methods.snap @@ -0,0 +1,161 @@ +--- +source: crates/semantic-query/tests/symbol_keys_snap.rs +expression: out +--- +Symbol: ambiguous_methods::Container +help: definitions + references + ┌─ ambiguous_methods.fe:1:8 + │ + 1 │ struct Container { value: i32 } + │ ^^^^^^^^^ + │ │ + │ def: defined here @ 1:8 (6 refs) + │ ref: 1:8 + · +11 │ impl TraitA for Container { + │ ^^^^^^^^^ ref: 11:17 +12 │ fn get(self) -> i32 { self.value } + │ ^^^^ ref: 12:12 + · +15 │ impl TraitB for Container { + │ ^^^^^^^^^ ref: 15:17 +16 │ fn get(self) -> i32 { self.value + 1 } + │ ^^^^ ref: 16:12 + · +20 │ let c = Container { value: 42 } + │ ^^^^^^^^^ ref: 20:13 + + + +Symbol: ambiguous_methods::Container::value +help: definitions + references + ┌─ ambiguous_methods.fe:1:20 + │ + 1 │ struct Container { value: i32 } + │ ^^^^^ + │ │ + │ def: defined here @ 1:20 (3 refs) + │ ref: 1:20 + · +12 │ fn get(self) -> i32 { self.value } + │ ^^^^^ ref: 12:32 + · +16 │ fn get(self) -> i32 { self.value + 1 } + │ ^^^^^ ref: 16:32 + + + +Symbol: ambiguous_methods::TraitA +help: definitions + references + ┌─ ambiguous_methods.fe:3:7 + │ + 3 │ trait TraitA { + │ ^^^^^^ + │ │ + │ def: defined here @ 3:7 (3 refs) + │ ref: 3:7 + 4 │ fn get(self) -> i32 + │ ^^^^ ref: 4:12 + · +11 │ impl TraitA for Container { + │ ^^^^^^ ref: 11:6 + + + +Symbol: ambiguous_methods::TraitA::get::get +help: definitions + references + ┌─ ambiguous_methods.fe:4:8 + │ + 4 │ fn get(self) -> i32 + │ ^^^ + │ │ + │ def: defined here @ 4:8 (3 refs) + │ ref: 4:8 + · +12 │ fn get(self) -> i32 { self.value } + │ ^^^ ref: 12:8 + · +21 │ let r = c.get() + │ ^^^ ref: 21:15 + + + +Symbol: ambiguous_methods::TraitB +help: definitions + references + ┌─ ambiguous_methods.fe:7:7 + │ + 7 │ trait TraitB { + │ ^^^^^^ + │ │ + │ def: defined here @ 7:7 (3 refs) + │ ref: 7:7 + 8 │ fn get(self) -> i32 + │ ^^^^ ref: 8:12 + · +15 │ impl TraitB for Container { + │ ^^^^^^ ref: 15:6 + + + +Symbol: ambiguous_methods::TraitB::get::get +help: definitions + references + ┌─ ambiguous_methods.fe:8:8 + │ + 8 │ fn get(self) -> i32 + │ ^^^ + │ │ + │ def: defined here @ 8:8 (2 refs) + │ ref: 8:8 + · +16 │ fn get(self) -> i32 { self.value + 1 } + │ ^^^ ref: 16:8 + + + +Symbol: local in ambiguous_methods::test +help: definitions + references + ┌─ ambiguous_methods.fe:20:9 + │ +20 │ let c = Container { value: 42 } + │ ^ + │ │ + │ def: defined here @ 20:9 (2 refs) + │ ref: 20:9 +21 │ let r = c.get() + │ ^ ref: 21:13 + + + +Symbol: local in ambiguous_methods::test +help: definitions + references + ┌─ ambiguous_methods.fe:21:9 + │ +21 │ let r = c.get() + │ ^ + │ │ + │ def: defined here @ 21:9 (1 refs) + │ ref: 21:9 + + + +Symbol: param#0 of +help: definitions + references + ┌─ ambiguous_methods.fe:16:12 + │ +16 │ fn get(self) -> i32 { self.value + 1 } + │ ^^^^ ^^^^ ref: 16:27 + │ │ + │ def: defined here @ 16:12 (2 refs) + │ ref: 16:12 + + + +Symbol: param#0 of +help: definitions + references + ┌─ ambiguous_methods.fe:12:12 + │ +12 │ fn get(self) -> i32 { self.value } + │ ^^^^ ^^^^ ref: 12:27 + │ │ + │ def: defined here @ 12:12 (2 refs) + │ ref: 12:12 diff --git a/crates/semantic-query/test_files/enum_variants.fe b/crates/semantic-query/test_files/enum_variants.fe new file mode 100644 index 0000000000..cebecef95b --- /dev/null +++ b/crates/semantic-query/test_files/enum_variants.fe @@ -0,0 +1,8 @@ +enum Color { Red, Green { intensity: i32 }, Blue(i32) } + +fn main() { + let r = Color::Red + let g = Color::Green { intensity: 5 } + let b = Color::Blue(3) +} + diff --git a/crates/semantic-query/test_files/enum_variants.snap b/crates/semantic-query/test_files/enum_variants.snap new file mode 100644 index 0000000000..8b7c81b202 --- /dev/null +++ b/crates/semantic-query/test_files/enum_variants.snap @@ -0,0 +1,102 @@ +--- +source: crates/semantic-query/tests/symbol_keys_snap.rs +expression: out +input_file: test_files/enum_variants.snap +--- +Symbol: enum_variants::Color +help: definitions + references + ┌─ enum_variants.fe:1:6 + │ +1 │ enum Color { Red, Green { intensity: i32 }, Blue(i32) } + │ ^^^^^ + │ │ + │ def: defined here @ 1:6 (4 refs) + │ ref: 1:6 + · +4 │ let r = Color::Red + │ ^^^^^ ref: 4:11 +5 │ let g = Color::Green { intensity: 5 } + │ ^^^^^ ref: 5:11 +6 │ let b = Color::Blue(3) + │ ^^^^^ ref: 6:11 + + + +Symbol: enum_variants::Color::Blue +help: definitions + references + ┌─ enum_variants.fe:1:45 + │ +1 │ enum Color { Red, Green { intensity: i32 }, Blue(i32) } + │ ^^^^ + │ │ + │ def: defined here @ 1:45 (2 refs) + │ ref: 1:45 + · +6 │ let b = Color::Blue(3) + │ ^^^^ ref: 6:18 + + + +Symbol: enum_variants::Color::Green +help: definitions + references + ┌─ enum_variants.fe:1:19 + │ +1 │ enum Color { Red, Green { intensity: i32 }, Blue(i32) } + │ ^^^^^ + │ │ + │ def: defined here @ 1:19 (2 refs) + │ ref: 1:19 + · +5 │ let g = Color::Green { intensity: 5 } + │ ^^^^^ ref: 5:18 + + + +Symbol: enum_variants::Color::Red +help: definitions + references + ┌─ enum_variants.fe:1:14 + │ +1 │ enum Color { Red, Green { intensity: i32 }, Blue(i32) } + │ ^^^ + │ │ + │ def: defined here @ 1:14 (2 refs) + │ ref: 1:14 + · +4 │ let r = Color::Red + │ ^^^ ref: 4:18 + + + +Symbol: local in enum_variants::main +help: definitions + references + ┌─ enum_variants.fe:4:7 + │ +4 │ let r = Color::Red + │ ^ + │ │ + │ def: defined here @ 4:7 (1 refs) + │ ref: 4:7 + + + +Symbol: local in enum_variants::main +help: definitions + references + ┌─ enum_variants.fe:6:7 + │ +6 │ let b = Color::Blue(3) + │ ^ + │ │ + │ def: defined here @ 6:7 (1 refs) + │ ref: 6:7 + + + +Symbol: local in enum_variants::main +help: definitions + references + ┌─ enum_variants.fe:5:7 + │ +5 │ let g = Color::Green { intensity: 5 } + │ ^ + │ │ + │ def: defined here @ 5:7 (1 refs) + │ ref: 5:7 diff --git a/crates/semantic-query/test_files/fields.fe b/crates/semantic-query/test_files/fields.fe new file mode 100644 index 0000000000..322c65ce99 --- /dev/null +++ b/crates/semantic-query/test_files/fields.fe @@ -0,0 +1,8 @@ +struct Point { x: i32, y: i32 } + +fn main() { + let p = Point { x: 1, y: 2 } + let a = p.x + let b = p.y +} + diff --git a/crates/semantic-query/test_files/fields.snap b/crates/semantic-query/test_files/fields.snap new file mode 100644 index 0000000000..2a0d4c7e3e --- /dev/null +++ b/crates/semantic-query/test_files/fields.snap @@ -0,0 +1,87 @@ +--- +source: crates/semantic-query/tests/symbol_keys_snap.rs +expression: out +input_file: test_files/fields.snap +--- +Symbol: fields::Point +help: definitions + references + ┌─ fields.fe:1:8 + │ +1 │ struct Point { x: i32, y: i32 } + │ ^^^^^ + │ │ + │ def: defined here @ 1:8 (2 refs) + │ ref: 1:8 + · +4 │ let p = Point { x: 1, y: 2 } + │ ^^^^^ ref: 4:11 + + + +Symbol: fields::Point::x +help: definitions + references + ┌─ fields.fe:1:16 + │ +1 │ struct Point { x: i32, y: i32 } + │ ^ + │ │ + │ def: defined here @ 1:16 (2 refs) + │ ref: 1:16 + · +5 │ let a = p.x + │ ^ ref: 5:13 + + + +Symbol: fields::Point::y +help: definitions + references + ┌─ fields.fe:1:24 + │ +1 │ struct Point { x: i32, y: i32 } + │ ^ + │ │ + │ def: defined here @ 1:24 (2 refs) + │ ref: 1:24 + · +6 │ let b = p.y + │ ^ ref: 6:13 + + + +Symbol: local in fields::main +help: definitions + references + ┌─ fields.fe:5:7 + │ +5 │ let a = p.x + │ ^ + │ │ + │ def: defined here @ 5:7 (1 refs) + │ ref: 5:7 + + + +Symbol: local in fields::main +help: definitions + references + ┌─ fields.fe:6:7 + │ +6 │ let b = p.y + │ ^ + │ │ + │ def: defined here @ 6:7 (1 refs) + │ ref: 6:7 + + + +Symbol: local in fields::main +help: definitions + references + ┌─ fields.fe:4:7 + │ +4 │ let p = Point { x: 1, y: 2 } + │ ^ + │ │ + │ def: defined here @ 4:7 (3 refs) + │ ref: 4:7 +5 │ let a = p.x + │ ^ ref: 5:11 +6 │ let b = p.y + │ ^ ref: 6:11 diff --git a/crates/language-server/test_files/hoverable/fe.toml b/crates/semantic-query/test_files/hoverable/fe.toml similarity index 100% rename from crates/language-server/test_files/hoverable/fe.toml rename to crates/semantic-query/test_files/hoverable/fe.toml diff --git a/crates/language-server/test_files/hoverable/src/lib.fe b/crates/semantic-query/test_files/hoverable/src/lib.fe similarity index 94% rename from crates/language-server/test_files/hoverable/src/lib.fe rename to crates/semantic-query/test_files/hoverable/src/lib.fe index a91ad35b84..8e4d5efb60 100644 --- a/crates/language-server/test_files/hoverable/src/lib.fe +++ b/crates/semantic-query/test_files/hoverable/src/lib.fe @@ -26,6 +26,6 @@ struct Numbers { impl Calculatable for Numbers { fn calculate(self) { - self.x + self.y + self.x + self.y; self.y } -} \ No newline at end of file +} diff --git a/crates/language-server/test_files/hoverable/src/stuff.fe b/crates/semantic-query/test_files/hoverable/src/stuff.fe similarity index 98% rename from crates/language-server/test_files/hoverable/src/stuff.fe rename to crates/semantic-query/test_files/hoverable/src/stuff.fe index b97ffe7660..612981d9a0 100644 --- a/crates/language-server/test_files/hoverable/src/stuff.fe +++ b/crates/semantic-query/test_files/hoverable/src/stuff.fe @@ -12,8 +12,8 @@ pub mod calculations { /// which one is it? pub mod ambiguous { - + } /// is it this one? pub fn ambiguous() {} -} \ No newline at end of file +} diff --git a/crates/semantic-query/test_files/leftmost_and_use.fe b/crates/semantic-query/test_files/leftmost_and_use.fe new file mode 100644 index 0000000000..5a822b64e8 --- /dev/null +++ b/crates/semantic-query/test_files/leftmost_and_use.fe @@ -0,0 +1,12 @@ +mod things { pub struct Why {} } +mod stuff { + pub mod calculations { + pub fn ambiguous() {} + pub mod ambiguous {} + } +} + +fn f() { + let _u: things::Why + let _a: stuff::calculations::ambiguous +} diff --git a/crates/semantic-query/test_files/leftmost_and_use.snap b/crates/semantic-query/test_files/leftmost_and_use.snap new file mode 100644 index 0000000000..235266babd --- /dev/null +++ b/crates/semantic-query/test_files/leftmost_and_use.snap @@ -0,0 +1,101 @@ +--- +source: crates/semantic-query/tests/symbol_keys_snap.rs +expression: out +input_file: test_files/leftmost_and_use.snap +--- +Symbol: leftmost_and_use::stuff +help: definitions + references + ┌─ leftmost_and_use.fe:2:5 + │ + 2 │ mod stuff { + │ ^^^^^ + │ │ + │ def: defined here @ 2:5 (2 refs) + │ ref: 2:5 + · +11 │ let _a: stuff::calculations::ambiguous + │ ^^^^^ ref: 11:11 + + + +Symbol: leftmost_and_use::stuff::calculations +help: definitions + references + ┌─ leftmost_and_use.fe:3:13 + │ + 3 │ pub mod calculations { + │ ^^^^^^^^^^^^ + │ │ + │ def: defined here @ 3:13 (2 refs) + │ ref: 3:13 + · +11 │ let _a: stuff::calculations::ambiguous + │ ^^^^^^^^^^^^ ref: 11:18 + + + +Symbol: leftmost_and_use::stuff::calculations::ambiguous +help: definitions + references + ┌─ leftmost_and_use.fe:4:16 + │ + 4 │ pub fn ambiguous() {} + │ ^^^^^^^^^ + │ │ + │ def: defined here @ 4:16 (2 refs) + │ ref: 4:16 + · +11 │ let _a: stuff::calculations::ambiguous + │ ^^^^^^^^^ ref: 11:32 + + + +Symbol: leftmost_and_use::things +help: definitions + references + ┌─ leftmost_and_use.fe:1:5 + │ + 1 │ mod things { pub struct Why {} } + │ ^^^^^^ + │ │ + │ def: defined here @ 1:5 (2 refs) + │ ref: 1:5 + · +10 │ let _u: things::Why + │ ^^^^^^ ref: 10:11 + + + +Symbol: leftmost_and_use::things::Why +help: definitions + references + ┌─ leftmost_and_use.fe:1:25 + │ + 1 │ mod things { pub struct Why {} } + │ ^^^ + │ │ + │ def: defined here @ 1:25 (2 refs) + │ ref: 1:25 + · +10 │ let _u: things::Why + │ ^^^ ref: 10:19 + + + +Symbol: local in leftmost_and_use::f +help: definitions + references + ┌─ leftmost_and_use.fe:10:7 + │ +10 │ let _u: things::Why + │ ^^ + │ │ + │ def: defined here @ 10:7 (1 refs) + │ ref: 10:7 + + + +Symbol: local in leftmost_and_use::f +help: definitions + references + ┌─ leftmost_and_use.fe:11:7 + │ +11 │ let _a: stuff::calculations::ambiguous + │ ^^ + │ │ + │ def: defined here @ 11:7 (1 refs) + │ ref: 11:7 diff --git a/crates/semantic-query/test_files/locals.fe b/crates/semantic-query/test_files/locals.fe new file mode 100644 index 0000000000..f941532b20 --- /dev/null +++ b/crates/semantic-query/test_files/locals.fe @@ -0,0 +1,6 @@ +fn test_locals(x: i32, y: i32) -> i32 { + let a = x + let x = a + y + x +} + diff --git a/crates/semantic-query/test_files/locals.snap b/crates/semantic-query/test_files/locals.snap new file mode 100644 index 0000000000..c2919d386f --- /dev/null +++ b/crates/semantic-query/test_files/locals.snap @@ -0,0 +1,59 @@ +--- +source: crates/semantic-query/tests/symbol_keys_snap.rs +expression: out +input_file: test_files/locals.snap +--- +Symbol: local in locals::test_locals +help: definitions + references + ┌─ locals.fe:2:7 + │ +2 │ let a = x + │ ^ + │ │ + │ def: defined here @ 2:7 (2 refs) + │ ref: 2:7 +3 │ let x = a + y + │ ^ ref: 3:11 + + + +Symbol: local in locals::test_locals +help: definitions + references + ┌─ locals.fe:3:7 + │ +3 │ let x = a + y + │ ^ + │ │ + │ def: defined here @ 3:7 (2 refs) + │ ref: 3:7 +4 │ x + │ ^ ref: 4:3 + + + +Symbol: param#0 of locals::test_locals +help: definitions + references + ┌─ locals.fe:1:16 + │ +1 │ fn test_locals(x: i32, y: i32) -> i32 { + │ ^ + │ │ + │ def: defined here @ 1:16 (2 refs) + │ ref: 1:16 +2 │ let a = x + │ ^ ref: 2:11 + + + +Symbol: param#1 of locals::test_locals +help: definitions + references + ┌─ locals.fe:1:24 + │ +1 │ fn test_locals(x: i32, y: i32) -> i32 { + │ ^ + │ │ + │ def: defined here @ 1:24 (2 refs) + │ ref: 1:24 +2 │ let a = x +3 │ let x = a + y + │ ^ ref: 3:15 diff --git a/crates/semantic-query/test_files/methods_call.fe b/crates/semantic-query/test_files/methods_call.fe new file mode 100644 index 0000000000..f3eaaa25ff --- /dev/null +++ b/crates/semantic-query/test_files/methods_call.fe @@ -0,0 +1,18 @@ +struct Container { value: i32 } + +trait ContainerTrait { + fn get(self) -> i32 +} + +impl ContainerTrait for Container { + fn get(self) -> i32 { self.value } +} + +impl Container { + pub fn get(self) -> i32 { self.value } +} + +fn test() { + let c = Container { value: 42 } + let r = c.get() +} diff --git a/crates/semantic-query/test_files/methods_call.snap b/crates/semantic-query/test_files/methods_call.snap new file mode 100644 index 0000000000..b937ec27f1 --- /dev/null +++ b/crates/semantic-query/test_files/methods_call.snap @@ -0,0 +1,127 @@ +--- +source: crates/semantic-query/tests/symbol_keys_snap.rs +expression: out +input_file: test_files/methods_call.snap +--- +Symbol: local in methods_call::test +help: definitions + references + ┌─ methods_call.fe:16:7 + │ +16 │ let c = Container { value: 42 } + │ ^ + │ │ + │ def: defined here @ 16:7 (2 refs) + │ ref: 16:7 +17 │ let r = c.get() + │ ^ ref: 17:11 + + + +Symbol: local in methods_call::test +help: definitions + references + ┌─ methods_call.fe:17:7 + │ +17 │ let r = c.get() + │ ^ + │ │ + │ def: defined here @ 17:7 (1 refs) + │ ref: 17:7 + + + +Symbol: method get +help: definitions + references + ┌─ methods_call.fe:12:10 + │ +12 │ pub fn get(self) -> i32 { self.value } + │ ^^^ + │ │ + │ def: defined here @ 12:10 (2 refs) + │ ref: 12:10 + · +17 │ let r = c.get() + │ ^^^ ref: 17:13 + + + +Symbol: methods_call::Container +help: definitions + references + ┌─ methods_call.fe:1:8 + │ + 1 │ struct Container { value: i32 } + │ ^^^^^^^^^ + │ │ + │ def: defined here @ 1:8 (6 refs) + │ ref: 1:8 + · + 7 │ impl ContainerTrait for Container { + │ ^^^^^^^^^ ref: 7:25 + 8 │ fn get(self) -> i32 { self.value } + │ ^^^^ ref: 8:12 + · +11 │ impl Container { + │ ^^^^^^^^^ ref: 11:6 +12 │ pub fn get(self) -> i32 { self.value } + │ ^^^^ ref: 12:14 + · +16 │ let c = Container { value: 42 } + │ ^^^^^^^^^ ref: 16:11 + + + +Symbol: methods_call::Container::value +help: definitions + references + ┌─ methods_call.fe:1:20 + │ + 1 │ struct Container { value: i32 } + │ ^^^^^ + │ │ + │ def: defined here @ 1:20 (3 refs) + │ ref: 1:20 + · + 8 │ fn get(self) -> i32 { self.value } + │ ^^^^^ ref: 8:32 + · +12 │ pub fn get(self) -> i32 { self.value } + │ ^^^^^ ref: 12:34 + + + +Symbol: methods_call::ContainerTrait +help: definitions + references + ┌─ methods_call.fe:3:7 + │ +3 │ trait ContainerTrait { + │ ^^^^^^^^^^^^^^ + │ │ + │ def: defined here @ 3:7 (3 refs) + │ ref: 3:7 +4 │ fn get(self) -> i32 + │ ^^^^ ref: 4:12 + · +7 │ impl ContainerTrait for Container { + │ ^^^^^^^^^^^^^^ ref: 7:6 + + + +Symbol: param#0 of +help: definitions + references + ┌─ methods_call.fe:12:14 + │ +12 │ pub fn get(self) -> i32 { self.value } + │ ^^^^ ^^^^ ref: 12:29 + │ │ + │ def: defined here @ 12:14 (2 refs) + │ ref: 12:14 + + + +Symbol: param#0 of +help: definitions + references + ┌─ methods_call.fe:8:12 + │ +8 │ fn get(self) -> i32 { self.value } + │ ^^^^ ^^^^ ref: 8:27 + │ │ + │ def: defined here @ 8:12 (2 refs) + │ ref: 8:12 diff --git a/crates/semantic-query/test_files/methods_ufcs.fe b/crates/semantic-query/test_files/methods_ufcs.fe new file mode 100644 index 0000000000..11634e9b32 --- /dev/null +++ b/crates/semantic-query/test_files/methods_ufcs.fe @@ -0,0 +1,12 @@ +struct Wrapper {} + +impl Wrapper { + pub fn new() -> Wrapper { Wrapper {} } + pub fn from_val() -> Wrapper { Wrapper::new() } +} + +fn main() { + let w1 = Wrapper::new() + let w2 = Wrapper::from_val() +} + diff --git a/crates/semantic-query/test_files/methods_ufcs.snap b/crates/semantic-query/test_files/methods_ufcs.snap new file mode 100644 index 0000000000..636f133380 --- /dev/null +++ b/crates/semantic-query/test_files/methods_ufcs.snap @@ -0,0 +1,86 @@ +--- +source: crates/semantic-query/tests/symbol_keys_snap.rs +expression: out +input_file: test_files/methods_ufcs.snap +--- +Symbol: local in methods_ufcs::main +help: definitions + references + ┌─ methods_ufcs.fe:9:7 + │ +9 │ let w1 = Wrapper::new() + │ ^^ + │ │ + │ def: defined here @ 9:7 (1 refs) + │ ref: 9:7 + + + +Symbol: local in methods_ufcs::main +help: definitions + references + ┌─ methods_ufcs.fe:10:7 + │ +10 │ let w2 = Wrapper::from_val() + │ ^^ + │ │ + │ def: defined here @ 10:7 (1 refs) + │ ref: 10:7 + + + +Symbol: method from_val +help: definitions + references + ┌─ methods_ufcs.fe:5:10 + │ + 5 │ pub fn from_val() -> Wrapper { Wrapper::new() } + │ ^^^^^^^^ + │ │ + │ def: defined here @ 5:10 (2 refs) + │ ref: 5:10 + · +10 │ let w2 = Wrapper::from_val() + │ ^^^^^^^^ ref: 10:21 + + + +Symbol: method new +help: definitions + references + ┌─ methods_ufcs.fe:4:10 + │ +4 │ pub fn new() -> Wrapper { Wrapper {} } + │ ^^^ + │ │ + │ def: defined here @ 4:10 (3 refs) + │ ref: 4:10 +5 │ pub fn from_val() -> Wrapper { Wrapper::new() } + │ ^^^ ref: 5:43 + · +9 │ let w1 = Wrapper::new() + │ ^^^ ref: 9:21 + + + +Symbol: methods_ufcs::Wrapper +help: definitions + references + ┌─ methods_ufcs.fe:1:8 + │ + 1 │ struct Wrapper {} + │ ^^^^^^^ + │ │ + │ def: defined here @ 1:8 (8 refs) + │ ref: 1:8 + 2 │ + 3 │ impl Wrapper { + │ ^^^^^^^ ref: 3:6 + 4 │ pub fn new() -> Wrapper { Wrapper {} } + │ ^^^^^^^ ^^^^^^^ ref: 4:29 + │ │ + │ ref: 4:19 + 5 │ pub fn from_val() -> Wrapper { Wrapper::new() } + │ ^^^^^^^ ^^^^^^^ ref: 5:34 + │ │ + │ ref: 5:24 + · + 9 │ let w1 = Wrapper::new() + │ ^^^^^^^ ref: 9:12 +10 │ let w2 = Wrapper::from_val() + │ ^^^^^^^ ref: 10:12 diff --git a/crates/semantic-query/test_files/pattern_labels.fe b/crates/semantic-query/test_files/pattern_labels.fe new file mode 100644 index 0000000000..5df0dc0c92 --- /dev/null +++ b/crates/semantic-query/test_files/pattern_labels.fe @@ -0,0 +1,15 @@ +enum Color { + Red, + Green { intensity: i32 }, + Blue, +} + +struct Point { x: i32, y: i32 } + +fn test(p: Point, c: Color) -> i32 { + let Point { x, y } = p + match c { + Color::Green { intensity } => intensity + _ => 0 + } +} diff --git a/crates/semantic-query/test_files/pattern_labels.snap b/crates/semantic-query/test_files/pattern_labels.snap new file mode 100644 index 0000000000..1be5bae62d --- /dev/null +++ b/crates/semantic-query/test_files/pattern_labels.snap @@ -0,0 +1,117 @@ +--- +source: crates/semantic-query/tests/symbol_keys_snap.rs +expression: out +input_file: test_files/pattern_labels.snap +--- +Symbol: local in pattern_labels::test +help: definitions + references + ┌─ pattern_labels.fe:10:15 + │ +10 │ let Point { x, y } = p + │ ^ + │ │ + │ def: defined here @ 10:15 (1 refs) + │ ref: 10:15 + + + +Symbol: local in pattern_labels::test +help: definitions + references + ┌─ pattern_labels.fe:12:20 + │ +12 │ Color::Green { intensity } => intensity + │ ^^^^^^^^^ ^^^^^^^^^ ref: 12:35 + │ │ + │ def: defined here @ 12:20 (2 refs) + │ ref: 12:20 + + + +Symbol: local in pattern_labels::test +help: definitions + references + ┌─ pattern_labels.fe:10:18 + │ +10 │ let Point { x, y } = p + │ ^ + │ │ + │ def: defined here @ 10:18 (1 refs) + │ ref: 10:18 + + + +Symbol: param#0 of pattern_labels::test +help: definitions + references + ┌─ pattern_labels.fe:9:9 + │ + 9 │ fn test(p: Point, c: Color) -> i32 { + │ ^ + │ │ + │ def: defined here @ 9:9 (2 refs) + │ ref: 9:9 +10 │ let Point { x, y } = p + │ ^ ref: 10:24 + + + +Symbol: param#1 of pattern_labels::test +help: definitions + references + ┌─ pattern_labels.fe:9:19 + │ + 9 │ fn test(p: Point, c: Color) -> i32 { + │ ^ + │ │ + │ def: defined here @ 9:19 (2 refs) + │ ref: 9:19 +10 │ let Point { x, y } = p +11 │ match c { + │ ^ ref: 11:9 + + + +Symbol: pattern_labels::Color +help: definitions + references + ┌─ pattern_labels.fe:1:6 + │ + 1 │ enum Color { + │ ^^^^^ + │ │ + │ def: defined here @ 1:6 (3 refs) + │ ref: 1:6 + · + 9 │ fn test(p: Point, c: Color) -> i32 { + │ ^^^^^ ref: 9:22 + · +12 │ Color::Green { intensity } => intensity + │ ^^^^^ ref: 12:5 + + + +Symbol: pattern_labels::Color::Green +help: definitions + references + ┌─ pattern_labels.fe:3:3 + │ + 3 │ Green { intensity: i32 }, + │ ^^^^^ + │ │ + │ def: defined here @ 3:3 (2 refs) + │ ref: 3:3 + · +12 │ Color::Green { intensity } => intensity + │ ^^^^^ ref: 12:12 + + + +Symbol: pattern_labels::Point +help: definitions + references + ┌─ pattern_labels.fe:7:8 + │ + 7 │ struct Point { x: i32, y: i32 } + │ ^^^^^ + │ │ + │ def: defined here @ 7:8 (3 refs) + │ ref: 7:8 + 8 │ + 9 │ fn test(p: Point, c: Color) -> i32 { + │ ^^^^^ ref: 9:12 +10 │ let Point { x, y } = p + │ ^^^^^ ref: 10:7 diff --git a/crates/semantic-query/test_files/use_alias_and_glob.fe b/crates/semantic-query/test_files/use_alias_and_glob.fe new file mode 100644 index 0000000000..85d5d229b8 --- /dev/null +++ b/crates/semantic-query/test_files/use_alias_and_glob.fe @@ -0,0 +1,16 @@ +mod root { + pub mod sub { + pub struct Name {} + pub struct Alt {} + } +} + +use root::sub::Name as N +use root::sub::* + +fn f() { + let _a: N + let _b: Name + let _c: sub::Name + let _d: Alt +} diff --git a/crates/semantic-query/test_files/use_alias_and_glob.snap b/crates/semantic-query/test_files/use_alias_and_glob.snap new file mode 100644 index 0000000000..79bbfab273 --- /dev/null +++ b/crates/semantic-query/test_files/use_alias_and_glob.snap @@ -0,0 +1,119 @@ +--- +source: crates/semantic-query/tests/symbol_keys_snap.rs +expression: out +input_file: test_files/use_alias_and_glob.snap +--- +Symbol: local in use_alias_and_glob::f +help: definitions + references + ┌─ use_alias_and_glob.fe:12:7 + │ +12 │ let _a: N + │ ^^ + │ │ + │ def: defined here @ 12:7 (1 refs) + │ ref: 12:7 + + + +Symbol: local in use_alias_and_glob::f +help: definitions + references + ┌─ use_alias_and_glob.fe:15:7 + │ +15 │ let _d: Alt + │ ^^ + │ │ + │ def: defined here @ 15:7 (1 refs) + │ ref: 15:7 + + + +Symbol: local in use_alias_and_glob::f +help: definitions + references + ┌─ use_alias_and_glob.fe:14:7 + │ +14 │ let _c: sub::Name + │ ^^ + │ │ + │ def: defined here @ 14:7 (1 refs) + │ ref: 14:7 + + + +Symbol: local in use_alias_and_glob::f +help: definitions + references + ┌─ use_alias_and_glob.fe:13:7 + │ +13 │ let _b: Name + │ ^^ + │ │ + │ def: defined here @ 13:7 (1 refs) + │ ref: 13:7 + + + +Symbol: use_alias_and_glob::root +help: definitions + references + ┌─ use_alias_and_glob.fe:1:5 + │ +1 │ mod root { + │ ^^^^ + │ │ + │ def: defined here @ 1:5 (3 refs) + │ ref: 1:5 + · +8 │ use root::sub::Name as N + │ ^^^^ ref: 8:5 +9 │ use root::sub::* + │ ^^^^ ref: 9:5 + + + +Symbol: use_alias_and_glob::root::sub +help: definitions + references + ┌─ use_alias_and_glob.fe:2:13 + │ +2 │ pub mod sub { + │ ^^^ + │ │ + │ def: defined here @ 2:13 (3 refs) + │ ref: 2:13 + · +8 │ use root::sub::Name as N + │ ^^^ ref: 8:11 +9 │ use root::sub::* + │ ^^^ ref: 9:11 + + + +Symbol: use_alias_and_glob::root::sub::Alt +help: definitions + references + ┌─ use_alias_and_glob.fe:4:20 + │ + 4 │ pub struct Alt {} + │ ^^^ + │ │ + │ def: defined here @ 4:20 (2 refs) + │ ref: 4:20 + · +15 │ let _d: Alt + │ ^^^ ref: 15:11 + + + +Symbol: use_alias_and_glob::root::sub::Name +help: definitions + references + ┌─ use_alias_and_glob.fe:3:20 + │ + 3 │ pub struct Name {} + │ ^^^^ + │ │ + │ def: defined here @ 3:20 (4 refs) + │ ref: 3:20 + · + 8 │ use root::sub::Name as N + │ ^^^^ ref: 8:16 + · +12 │ let _a: N + │ ^ ref: 12:11 +13 │ let _b: Name + │ ^^^^ ref: 13:11 diff --git a/crates/semantic-query/test_files/use_braces.fe b/crates/semantic-query/test_files/use_braces.fe new file mode 100644 index 0000000000..99f144d4b3 --- /dev/null +++ b/crates/semantic-query/test_files/use_braces.fe @@ -0,0 +1,15 @@ +mod stuff { + pub mod calculations { + pub fn return_three() -> i32 { 3 } + pub fn return_four() -> i32 { 4 } + pub fn return_five() -> i32 { 5 } + } +} + +use stuff::calculations::return_three +use stuff::calculations::return_four + +pub fn test_use_braces() { + let x = return_three() + let y = return_four() +} \ No newline at end of file diff --git a/crates/semantic-query/test_files/use_braces.snap b/crates/semantic-query/test_files/use_braces.snap new file mode 100644 index 0000000000..2d47be8923 --- /dev/null +++ b/crates/semantic-query/test_files/use_braces.snap @@ -0,0 +1,96 @@ +--- +source: crates/semantic-query/tests/symbol_keys_snap.rs +expression: out +input_file: test_files/use_braces.snap +--- +Symbol: local in use_braces::test_use_braces +help: definitions + references + ┌─ use_braces.fe:14:9 + │ +14 │ let y = return_four() + │ ^ + │ │ + │ def: defined here @ 14:9 (1 refs) + │ ref: 14:9 + + + +Symbol: local in use_braces::test_use_braces +help: definitions + references + ┌─ use_braces.fe:13:9 + │ +13 │ let x = return_three() + │ ^ + │ │ + │ def: defined here @ 13:9 (1 refs) + │ ref: 13:9 + + + +Symbol: use_braces::stuff +help: definitions + references + ┌─ use_braces.fe:1:5 + │ + 1 │ mod stuff { + │ ^^^^^ + │ │ + │ def: defined here @ 1:5 (3 refs) + │ ref: 1:5 + · + 9 │ use stuff::calculations::return_three + │ ^^^^^ ref: 9:5 +10 │ use stuff::calculations::return_four + │ ^^^^^ ref: 10:5 + + + +Symbol: use_braces::stuff::calculations +help: definitions + references + ┌─ use_braces.fe:2:13 + │ + 2 │ pub mod calculations { + │ ^^^^^^^^^^^^ + │ │ + │ def: defined here @ 2:13 (3 refs) + │ ref: 2:13 + · + 9 │ use stuff::calculations::return_three + │ ^^^^^^^^^^^^ ref: 9:12 +10 │ use stuff::calculations::return_four + │ ^^^^^^^^^^^^ ref: 10:12 + + + +Symbol: use_braces::stuff::calculations::return_four +help: definitions + references + ┌─ use_braces.fe:4:16 + │ + 4 │ pub fn return_four() -> i32 { 4 } + │ ^^^^^^^^^^^ + │ │ + │ def: defined here @ 4:16 (3 refs) + │ ref: 4:16 + · +10 │ use stuff::calculations::return_four + │ ^^^^^^^^^^^ ref: 10:26 + · +14 │ let y = return_four() + │ ^^^^^^^^^^^ ref: 14:13 + + + +Symbol: use_braces::stuff::calculations::return_three +help: definitions + references + ┌─ use_braces.fe:3:16 + │ + 3 │ pub fn return_three() -> i32 { 3 } + │ ^^^^^^^^^^^^ + │ │ + │ def: defined here @ 3:16 (3 refs) + │ ref: 3:16 + · + 9 │ use stuff::calculations::return_three + │ ^^^^^^^^^^^^ ref: 9:26 + · +13 │ let x = return_three() + │ ^^^^^^^^^^^^ ref: 13:13 diff --git a/crates/semantic-query/test_files/use_paths.fe b/crates/semantic-query/test_files/use_paths.fe new file mode 100644 index 0000000000..5b6df12f8a --- /dev/null +++ b/crates/semantic-query/test_files/use_paths.fe @@ -0,0 +1,6 @@ +mod root { pub mod sub { pub struct Name {} } } + +use root::sub::Name +use root::sub +use root + diff --git a/crates/semantic-query/test_files/use_paths.snap b/crates/semantic-query/test_files/use_paths.snap new file mode 100644 index 0000000000..620fb6b7fa --- /dev/null +++ b/crates/semantic-query/test_files/use_paths.snap @@ -0,0 +1,53 @@ +--- +source: crates/semantic-query/tests/symbol_keys_snap.rs +expression: out +input_file: test_files/use_paths.snap +--- +Symbol: use_paths::root +help: definitions + references + ┌─ use_paths.fe:1:5 + │ +1 │ mod root { pub mod sub { pub struct Name {} } } + │ ^^^^ + │ │ + │ def: defined here @ 1:5 (4 refs) + │ ref: 1:5 +2 │ +3 │ use root::sub::Name + │ ^^^^ ref: 3:5 +4 │ use root::sub + │ ^^^^ ref: 4:5 +5 │ use root + │ ^^^^ ref: 5:5 + + + +Symbol: use_paths::root::sub +help: definitions + references + ┌─ use_paths.fe:1:20 + │ +1 │ mod root { pub mod sub { pub struct Name {} } } + │ ^^^ + │ │ + │ def: defined here @ 1:20 (3 refs) + │ ref: 1:20 +2 │ +3 │ use root::sub::Name + │ ^^^ ref: 3:11 +4 │ use root::sub + │ ^^^ ref: 4:11 + + + +Symbol: use_paths::root::sub::Name +help: definitions + references + ┌─ use_paths.fe:1:37 + │ +1 │ mod root { pub mod sub { pub struct Name {} } } + │ ^^^^ + │ │ + │ def: defined here @ 1:37 (2 refs) + │ ref: 1:37 +2 │ +3 │ use root::sub::Name + │ ^^^^ ref: 3:16 diff --git a/crates/semantic-query/tests/boundary_cases.rs b/crates/semantic-query/tests/boundary_cases.rs new file mode 100644 index 0000000000..953ef5b8fc --- /dev/null +++ b/crates/semantic-query/tests/boundary_cases.rs @@ -0,0 +1,102 @@ +use common::InputDb; +use driver::DriverDataBase; +use fe_semantic_query::SemanticQuery; +use hir::lower::map_file_to_mod; +use hir::span::LazySpan as _; +use url::Url; + +fn offset_of(text: &str, needle: &str) -> parser::TextSize { + parser::TextSize::from(text.find(needle).expect("needle present") as u32) +} + +// Boundary semantics are being finalized alongside half-open spans and selection policy. +// Ignored for now to keep the suite green while we land the analysis bridge. +#[test] +fn local_param_boundaries() { + let fixture_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("test-fixtures") + .join("local_param_boundary.fe"); + let content = std::fs::read_to_string(&fixture_path).unwrap(); + let tmp = std::env::temp_dir().join("boundary_local_param.fe"); + std::fs::write(&tmp, &content).unwrap(); + let mut db = DriverDataBase::default(); + let file = db.workspace().touch( + &mut db, + Url::from_file_path(&tmp).unwrap(), + Some(content.clone()), + ); + let top = map_file_to_mod(&db, file); + + // First character of local 'y' usage (the 'y' in 'return y') + let start_y = offset_of(&content, "return y") + parser::TextSize::from(7u32); // 7 = length of "return " + let key_start = SemanticQuery::at_cursor(&db, top, start_y) + .symbol_key() + .expect("symbol at start of y"); + + // Last character of 'y' usage is same as start here (single-char ident) + let last_y = start_y; // single char + let key_last = SemanticQuery::at_cursor(&db, top, last_y) + .symbol_key() + .expect("symbol at last char of y"); + assert_eq!( + key_start, key_last, + "identity should be stable across y span" + ); + + // Immediately after local 'y' (half-open end): should not select + let after_y = last_y + parser::TextSize::from(1u32); + + let symbol_after = SemanticQuery::at_cursor(&db, top, after_y).symbol_key(); + + assert!(symbol_after.is_none(), "no symbol immediately after y"); + + // Parameter usage 'x' resolves to parameter identity + let x_use = offset_of(&content, " x") + parser::TextSize::from(1u32); + let key_param = SemanticQuery::at_cursor(&db, top, x_use) + .symbol_key() + .expect("symbol for param x usage"); + // Def span should match a param header in the function + let (_tm, def_span) = + SemanticQuery::definition_for_symbol(&db, key_param).expect("def for param"); + let def_res = def_span.resolve(&db).expect("resolve def span"); + let name_text = &content.as_str() + [(Into::::into(def_res.range.start()))..(Into::::into(def_res.range.end()))]; + assert_eq!(name_text, "x"); +} + +#[test] +fn shadowing_param_by_local() { + let fixture_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("test-fixtures") + .join("shadow_local.fe"); + let content = std::fs::read_to_string(&fixture_path).unwrap(); + let tmp = std::env::temp_dir().join("boundary_shadow_local.fe"); + std::fs::write(&tmp, &content).unwrap(); + let mut db = DriverDataBase::default(); + let file = db.workspace().touch( + &mut db, + Url::from_file_path(&tmp).unwrap(), + Some(content.clone()), + ); + let top = map_file_to_mod(&db, file); + + // Cursor at the final 'x' usage should resolve to the local, not the param + let use_x = offset_of(&content, "return x") + parser::TextSize::from(7u32); // 7 = length of "return " + let key_use = SemanticQuery::at_cursor(&db, top, use_x) + .symbol_key() + .expect("symbol at x usage"); + + // Def for resolved key should be the local 'x' binding + let (_tm, def_span) = SemanticQuery::definition_for_symbol(&db, key_use).expect("def for x"); + let def_res = def_span.resolve(&db).expect("resolve def"); + let def_text = &content.as_str() + [(Into::::into(def_res.range.start()))..(Into::::into(def_res.range.end()))]; + assert_eq!(def_text, "x"); + + // Ensure that the key does not equal the param identity + let param_pos = offset_of(&content, "(x:") + parser::TextSize::from(1u32); + let param_key = SemanticQuery::at_cursor(&db, top, param_pos) + .symbol_key() + .expect("param key"); + assert_ne!(format!("{:?}", key_use), format!("{:?}", param_key)); +} diff --git a/crates/semantic-query/tests/refs_def_site.rs b/crates/semantic-query/tests/refs_def_site.rs new file mode 100644 index 0000000000..e41135b49c --- /dev/null +++ b/crates/semantic-query/tests/refs_def_site.rs @@ -0,0 +1,130 @@ +use common::InputDb; +use driver::DriverDataBase; +use fe_semantic_query::SemanticQuery; +use hir::lower::map_file_to_mod; +use hir::span::LazySpan as _; +use url::Url; + +fn line_col_from_offset(text: &str, offset: parser::TextSize) -> (usize, usize) { + let mut line = 0usize; + let mut col = 0usize; + for (i, ch) in text.chars().enumerate() { + if i == Into::::into(offset) { + return (line, col); + } + if ch == '\n' { + line += 1; + col = 0; + } else { + col += 1; + } + } + (line, col) +} + +#[test] +fn def_site_method_refs_include_ufcs() { + // Load the existing fixture used by snapshots + let fixture_path = + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("test_files/methods_ufcs.fe"); + let content = std::fs::read_to_string(&fixture_path).expect("fixture present"); + + let mut db = DriverDataBase::default(); + let file = db.workspace().touch( + &mut db, + Url::from_file_path(&fixture_path).unwrap(), + Some(content.clone()), + ); + let top = map_file_to_mod(&db, file); + + // Cursor at def-site method name: resolve exactly from HIR + let mut cursor: Option = None; + for it in top.all_items(&db).iter() { + if let hir::hir_def::ItemKind::Func(f) = *it { + if let Some(name) = f.name(&db).to_opt() { + if name.data(&db) == "new" { + if let Some(sp) = f.span().name().resolve(&db) { + // place cursor inside the ident + cursor = Some((Into::::into(sp.range.start()) + 1).into()); + break; + } + } + } + } + } + let cursor = cursor.expect("found def-site method name"); + let refs = SemanticQuery::at_cursor(&db, top, cursor).find_references(); + assert!( + refs.len() >= 3, + "expected at least 3 refs, got {}", + refs.len() + ); + + // Collect (line,col) pairs for readability + let mut pairs: Vec<(usize, usize)> = refs + .iter() + .filter_map(|r| r.span.resolve(&db)) + .map(|sp| line_col_from_offset(&content, sp.range.start())) + .collect(); + pairs.sort(); + pairs.dedup(); + + // Expect exact presence of def (3,9) and both UFCS call sites: (4,42) and (8,20) + let expected = [(3, 9), (4, 42), (8, 20)]; + for p in expected.iter() { + assert!( + pairs.contains(p), + "missing expected reference at {:?}, got {:?}", + p, + pairs + ); + } +} + +#[test] +fn round_trip_invariant_param_and_local() { + let content = r#" +fn main(x: i32) -> i32 { let y = x; return y } +"#; + let tmp = std::env::temp_dir().join("round_trip_param_local.fe"); + std::fs::write(&tmp, content).unwrap(); + let mut db = DriverDataBase::default(); + let file = db.workspace().touch( + &mut db, + Url::from_file_path(&tmp).unwrap(), + Some(content.to_string()), + ); + let top = map_file_to_mod(&db, file); + + // Cursor on parameter usage 'x' + let cursor_x = parser::TextSize::from(content.find(" x; ").unwrap() as u32 + 1); + if let Some(key) = SemanticQuery::at_cursor(&db, top, cursor_x).symbol_key() { + if let Some((_tm, def_span)) = SemanticQuery::definition_for_symbol(&db, key) { + let refs = SemanticQuery::references_for_symbol(&db, top, key); + let def_resolved = def_span.resolve(&db).expect("def span resolve"); + assert!( + refs.iter() + .any(|r| r.span.resolve(&db) == Some(def_resolved.clone())), + "param def-site missing from refs" + ); + } + } else { + panic!("failed to resolve symbol at cursor_x"); + } + + // Cursor on local 'y' usage (in return statement) + let cursor_y = parser::TextSize::from(content.rfind("return y").unwrap() as u32 + 7); + if let Some(key) = SemanticQuery::at_cursor(&db, top, cursor_y).symbol_key() { + if let Some((_tm, def_span)) = SemanticQuery::definition_for_symbol(&db, key) { + let refs = SemanticQuery::references_for_symbol(&db, top, key); + let def_resolved = def_span.resolve(&db).expect("def span resolve"); + assert!( + refs.iter() + .any(|r| r.span.resolve(&db) == Some(def_resolved.clone())), + "local def-site missing from refs" + ); + } + } else { + panic!("failed to resolve symbol at cursor_y"); + } +} diff --git a/crates/semantic-query/tests/symbol_keys_snap.rs b/crates/semantic-query/tests/symbol_keys_snap.rs new file mode 100644 index 0000000000..c700ff4ee2 --- /dev/null +++ b/crates/semantic-query/tests/symbol_keys_snap.rs @@ -0,0 +1,160 @@ +use common::InputDb; +use dir_test::{dir_test, Fixture}; +use driver::DriverDataBase; +use fe_semantic_query::SemanticQuery; +use hir::{lower::map_file_to_mod, span::LazySpan as _, SpannedHirDb}; +use hir_analysis::lookup::SymbolKey; +use hir_analysis::HirAnalysisDb; +use test_utils::snap::{codespan_render_defs_refs, line_col_from_cursor}; +use test_utils::snap_test; +use url::Url; + +fn symbol_label<'db>( + db: &'db dyn SpannedHirDb, + adb: &'db dyn HirAnalysisDb, + key: &hir_analysis::lookup::SymbolKey<'db>, +) -> String { + match key { + SymbolKey::Scope(sc) => sc.pretty_path(db).unwrap_or("".into()), + SymbolKey::EnumVariant(v) => v.scope().pretty_path(db).unwrap_or("".into()), + SymbolKey::Method(fd) => { + // Show container scope path + method name + let name = fd.name(adb).data(db); + let path = fd.scope(adb).pretty_path(db).unwrap_or_default(); + if path.is_empty() { + format!("method {}", name) + } else { + format!("{}::{}", path, name) + } + } + SymbolKey::FuncParam(item, idx) => { + let path = hir::hir_def::scope_graph::ScopeId::from_item(*item) + .pretty_path(db) + .unwrap_or_default(); + format!("param#{} of {}", idx, path) + } + SymbolKey::Local(func, _bkey) => { + let path = func.scope().pretty_path(db).unwrap_or_default(); + format!("local in {}", path) + } + } +} + +#[dir_test(dir: "$CARGO_MANIFEST_DIR/test_files", glob: "*.fe")] +fn symbol_keys_snapshot(fx: Fixture<&str>) { + let mut db = DriverDataBase::default(); + let file = db.workspace().touch( + &mut db, + Url::from_file_path(fx.path()).unwrap(), + Some(fx.content().to_string()), + ); + let top = map_file_to_mod(&db, file); + + // Modules in this ingot + let ing = top.ingot(&db); + let view = ing.files(&db); + let mut modules: Vec = Vec::new(); + for (_u, f) in view.iter() { + if f.kind(&db) == Some(common::file::IngotFileKind::Source) { + modules.push(map_file_to_mod(&db, f)); + } + } + if modules.is_empty() { + modules.push(top); + } + + // Build symbol index across modules + let map = SemanticQuery::build_symbol_index_for_modules(&db, &modules); + + // Stable ordering of symbol keys via labels + let mut entries: Vec<(String, hir_analysis::lookup::SymbolKey)> = map + .keys() + .map(|k| (symbol_label(&db, &db, k), *k)) + .collect(); + entries.sort_by(|a, b| a.0.cmp(&b.0)); + + let mut out = String::new(); + for (label, key) in entries { + // Gather def + let def_opt = SemanticQuery::definition_for_symbol(&db, key) + .and_then(|(_tm, span)| span.resolve(&db)); + // Gather refs across modules + let refs = SemanticQuery::references_for_symbol(&db, top, key); + let mut refs_by_file: std::collections::BTreeMap< + common::file::File, + Vec, + > = Default::default(); + for r in refs { + if let Some(sp) = r.span.resolve(&db) { + refs_by_file.entry(sp.file).or_default().push(sp); + } + } + + out.push_str(&format!("Symbol: {}\n", label)); + + // Group by files that have def or refs + let mut files: Vec = refs_by_file.keys().cloned().collect(); + if let Some(d) = def_opt.as_ref() { + if !files.contains(&d.file) { + files.push(d.file); + } + } + // Stable order by file URL path + files.sort_by_key(|f| f.url(&db).map(|u| u.path().to_string()).unwrap_or_default()); + + for f in files { + let content = f.text(&db); + let name = f + .url(&db) + .and_then(|u| { + u.path_segments() + .and_then(|mut s| s.next_back()) + .map(|s| s.to_string()) + }) + .unwrap_or_else(|| "".into()); + let mut defs_same: Vec<(std::ops::Range, String)> = Vec::new(); + let mut refs_same: Vec<(std::ops::Range, String)> = Vec::new(); + + if let Some(def) = def_opt.as_ref().filter(|d| d.file == f) { + let s: usize = Into::::into(def.range.start()); + let e: usize = Into::::into(def.range.end()); + let (l0, c0) = line_col_from_cursor(def.range.start(), content); + let (l, c) = (l0 + 1, c0 + 1); + // total refs count across all files + let total_refs = refs_by_file.values().map(|v| v.len()).sum::(); + defs_same.push(( + s..e, + format!("defined here @ {}:{} ({} refs)", l, c, total_refs), + )); + } + + if let Some(v) = refs_by_file.get(&f) { + let mut spans = v.clone(); + spans.sort_by_key(|sp| (sp.range.start(), sp.range.end())); + for sp in spans { + let s: usize = Into::::into(sp.range.start()); + let e: usize = Into::::into(sp.range.end()); + let (l0, c0) = line_col_from_cursor(sp.range.start(), content); + let (l, c) = (l0 + 1, c0 + 1); + refs_same.push((s..e, format!("{}:{}", l, c))); + } + } + + // Render codespan for this file for this symbol + let block = codespan_render_defs_refs(&name, content, &defs_same, &refs_same); + out.push_str(&block); + out.push('\n'); + } + + out.push('\n'); + } + + let orig = std::path::Path::new(fx.path()); + let stem = orig + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("snapshot"); + let combined_name = format!("{}.snap", stem); + let combined_path = orig.with_file_name(combined_name); + snap_test!(out, combined_path.to_str().unwrap()); +} diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index b63c99ad39..5425bf5687 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -2,13 +2,23 @@ name = "fe-test-utils" version = "0.1.0" edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/ethereum/fe" +description = "Fe test utilities" [lib] doctest = false [dependencies] -insta = { default-features = false, version = "1.42" } -tracing.workspace = true -tracing-subscriber.workspace = true -tracing-tree.workspace = true -url.workspace = true +insta = "1.34.0" +rstest = "0.18.2" +rstest_reuse = "0.6.0" +tracing = { workspace = true, features = ["log"] } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +tracing-tree = "0.3.0" +hir = { workspace = true } +parser = { workspace = true } +url = { workspace = true } +common = { workspace = true } +codespan-reporting = { workspace = true } +termcolor = "1" diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index b62238db8c..a8a3902f04 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -1,5 +1,6 @@ #[doc(hidden)] pub mod _macro_support; +pub mod snap; pub mod url_utils; pub use tracing::Level; use tracing::{ diff --git a/crates/test-utils/src/snap.rs b/crates/test-utils/src/snap.rs new file mode 100644 index 0000000000..7fb3a26c55 --- /dev/null +++ b/crates/test-utils/src/snap.rs @@ -0,0 +1,47 @@ +use std::ops::Range; + +pub fn line_col_from_cursor(cursor: parser::TextSize, s: &str) -> (usize, usize) { + let mut line = 0usize; + let mut col = 0usize; + for (i, ch) in s.chars().enumerate() { + if i == Into::::into(cursor) { + return (line, col); + } + if ch == '\n' { + line += 1; + col = 0; + } else { + col += 1; + } + } + (line, col) +} + +/// Render a codespan-reporting snippet with primary carets for defs and refs only (no cursor). +pub fn codespan_render_defs_refs( + file_name: &str, + content: &str, + defs: &[(Range, String)], + refs: &[(Range, String)], +) -> String { + use codespan_reporting::diagnostic::{Diagnostic, Label, Severity}; + use codespan_reporting::term::{emit, Config}; + use termcolor::Buffer; + + let mut out = Buffer::no_color(); + let cfg = Config::default(); + let mut files = codespan_reporting::files::SimpleFiles::new(); + let file_id = files.add(file_name.to_string(), content.to_string()); + let mut labels: Vec> = Vec::new(); + for (r, msg) in defs.iter() { + labels.push(Label::primary(file_id, r.clone()).with_message(format!("def: {}", msg))); + } + for (r, msg) in refs.iter() { + labels.push(Label::primary(file_id, r.clone()).with_message(format!("ref: {}", msg))); + } + let diag = Diagnostic::new(Severity::Help) + .with_message("definitions + references") + .with_labels(labels); + let _ = emit(&mut out, &cfg, &files, &diag); + String::from_utf8_lossy(out.as_slice()).into_owned() +}