From 776d92483c1085501f78679635a0f430c536dbea Mon Sep 17 00:00:00 2001 From: Kinflou Date: Mon, 31 Aug 2026 20:56:12 +0800 Subject: [PATCH] fix: `use ns::Name as X` binds the alias `X` (closes #43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FrozenUnit::Import(String, (usize,usize))` -> `Import(String, Option, (usize,usize))` — the second field is the local alias from `use ns::Name as X` (`None` for a plain `use`). - `resolve_use_declaration` takes the alias and attaches it to the single-symbol / whole-namespace import unit; glob and multi imports ignore it (meaningless there). - Validation: `SymbolTable` gains a `bare_imports` set — the alias, or else the trailing segment of a plain `use`. Kept out of `symbols` so it never trips duplicate detection and a real local declaration always shadows it. `is_imported_bare` now checks that set. - A plain `use ns::Name` still binds bare `Name` (from #44); an aliased `use ns::Name as X` binds `X` but NOT bare `Name`, Rust-style. ~10 mechanical `Import(x, y)` -> `Import(x, None, y)` construction sites, and `Import(p, _)` -> `Import(p, _, _)` in the tests. Tests: `imports.rs` gains `test_use_binds_the_bare_name`, `test_use_as_binds_the_alias`, `test_use_as_does_not_bind_the_original_bare_name`; `from_sources.rs` covers bare / qualified / alias. Full suite green. --- core/src/autodoc/mod.rs | 2 +- .../ir/compiler/interpreter/incremental.rs | 38 +++++++---- core/src/schema/ir/frozen/unit.rs | 6 +- core/src/schema/ir/validation/symbols.rs | 26 ++++--- core/src/schema/ir/validation/validator.rs | 28 +++++--- core/tests/package/from_sources.rs | 15 +++-- core/tests/schema/ir/examples_compile.rs | 2 +- core/tests/schema/ir/generation.rs | 2 +- core/tests/schema/ir/imports.rs | 67 +++++++++++++++++-- 9 files changed, 137 insertions(+), 49 deletions(-) diff --git a/core/src/autodoc/mod.rs b/core/src/autodoc/mod.rs index 102db32..caef801 100644 --- a/core/src/autodoc/mod.rs +++ b/core/src/autodoc/mod.rs @@ -33,7 +33,7 @@ pub fn node_difference(from: FrozenUnit, to: FrozenUnit) { for node in from { match node { FrozenUnit::Namespace(n) => {} - FrozenUnit::Import(_, _) => {} + FrozenUnit::Import(..) => {} FrozenUnit::Constant { .. } => {} FrozenUnit::Property { .. } => {} FrozenUnit::Parameter { .. } => {} diff --git a/core/src/schema/ir/compiler/interpreter/incremental.rs b/core/src/schema/ir/compiler/interpreter/incremental.rs index a9b074a..d41dab4 100644 --- a/core/src/schema/ir/compiler/interpreter/incremental.rs +++ b/core/src/schema/ir/compiler/interpreter/incremental.rs @@ -56,17 +56,23 @@ impl IncrementalInterpreter { match spanned_decl.value { Declaration::Import(import) => { // Legacy import support - frozen_units.push(FrozenUnit::Import(import.path(), span)); + frozen_units.push(FrozenUnit::Import(import.path(), None, span)); } Declaration::Use(use_stmt) => { + let alias = use_stmt.alias.as_ref().map(|a| a.name.text.clone()); let units = match use_context { Some((current_namespace, project_context)) => resolve_use_declaration( project_context, current_namespace, &use_stmt.path, + alias, span, ), - None => vec![FrozenUnit::Import(extract_use_path(&use_stmt.path), span)], + None => vec![FrozenUnit::Import( + extract_use_path(&use_stmt.path), + alias, + span, + )], }; frozen_units.extend(units); } @@ -450,6 +456,7 @@ fn resolve_use_declaration( project_context: &ProjectContext, current_namespace: &[String], use_path: &UsePath, + alias: Option, span: (usize, usize), ) -> Vec { let resolver = ImportResolver::new(vec![], Default::default(), None); @@ -458,24 +465,24 @@ fn resolve_use_declaration( Ok(target) => target, Err(message) => { tracing::warn!("Failed to resolve use path: {}", message); - return vec![FrozenUnit::Import(format!("", message), span)]; + return vec![FrozenUnit::Import(format!("", message), alias, span)]; } }; let joined_namespace = target.resolved.absolute_namespace.join("::"); - // Glob import: `use ns::*;` + // Glob import: `use ns::*;` — an alias here is meaningless. if target.resolved.symbols == ["*".to_string()] { - let mut units = vec![FrozenUnit::Import(format!("{}::*", joined_namespace), span)]; + let mut units = vec![FrozenUnit::Import(format!("{}::*", joined_namespace), None, span)]; if let Some(schema) = &target.schema { units.extend(declared_symbol_names(&schema.borrow()).into_iter().map(|name| { - FrozenUnit::Import(format!("{}::{}", joined_namespace, name), span) + FrozenUnit::Import(format!("{}::{}", joined_namespace, name), None, span) })); } return units; } - // Item imports: `use ns::{A, B};` + // Item imports: `use ns::{A, B};` — an alias here is meaningless. if !target.resolved.symbols.is_empty() { return target .resolved @@ -486,20 +493,21 @@ fn resolve_use_declaration( { tracing::warn!("Symbol '{}' not found in schema '{}'", item, joined_namespace); } - FrozenUnit::Import(format!("{}::{}", joined_namespace, item), span) + FrozenUnit::Import(format!("{}::{}", joined_namespace, item), None, span) }) .collect(); } - // Whole-namespace or single-symbol import (`use ns;` / `use ns::Symbol;`) + // Whole-namespace or single-symbol import (`use ns;` / `use ns::Symbol;`). + // The alias (`use ... as X`) binds `X` to that one target. match &target.schema { Some(schema) if target.remaining.is_empty() => { let ns = schema.borrow().namespace_joined(); - let mut units = vec![FrozenUnit::Import(ns.clone(), span)]; + let mut units = vec![FrozenUnit::Import(ns.clone(), alias, span)]; units.extend( declared_symbol_names(&schema.borrow()) .into_iter() - .map(|name| FrozenUnit::Import(format!("{}::{}", ns, name), span)), + .map(|name| FrozenUnit::Import(format!("{}::{}", ns, name), None, span)), ); units } @@ -511,10 +519,14 @@ fn resolve_use_declaration( tracing::warn!("Symbol '{}' not found in schema '{}'", symbol, schema_namespace); } - vec![FrozenUnit::Import(format!("{}::{}", schema_namespace, symbol), span)] + vec![FrozenUnit::Import( + format!("{}::{}", schema_namespace, symbol), + alias, + span, + )] } // Not part of this project (external dependency, stdlib, or genuinely // unresolved) - fall back to the raw resolved namespace. - None => vec![FrozenUnit::Import(joined_namespace, span)], + None => vec![FrozenUnit::Import(joined_namespace, alias, span)], } } diff --git a/core/src/schema/ir/frozen/unit.rs b/core/src/schema/ir/frozen/unit.rs index e67457f..c23bbfe 100644 --- a/core/src/schema/ir/frozen/unit.rs +++ b/core/src/schema/ir/frozen/unit.rs @@ -23,7 +23,11 @@ pub enum FrozenUnit { // Span is included in the hash/CAS identity deliberately: two schemas // that only differ in formatting/position should not be considered // content-identical. - Import(String, (usize, usize)), + // + // `(resolved path, optional local alias, span)`. The alias is the `X` in + // `use ns::Name as X`; `None` for a plain `use` (the bare name it binds is + // then the path's trailing segment). + Import(String, Option, (usize, usize)), Constant { docstring: Option, name: String, diff --git a/core/src/schema/ir/validation/symbols.rs b/core/src/schema/ir/validation/symbols.rs index 4910aff..7b658bc 100644 --- a/core/src/schema/ir/validation/symbols.rs +++ b/core/src/schema/ir/validation/symbols.rs @@ -1,5 +1,5 @@ // use crate::schema::ir::frozen::unit::FrozenUnit; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SymbolType { @@ -14,15 +14,26 @@ pub enum SymbolType { pub struct SymbolTable<'a> { pub symbols: HashMap<&'a str, SymbolType>, + /// Bare names a `use` brought into scope: the alias in `use ns::Name as X`, + /// otherwise the trailing segment of `use ns::Name`. Kept apart from + /// `symbols` so it never causes a "duplicate definition" and a real local + /// declaration always shadows it. + bare_imports: HashSet<&'a str>, } impl<'a> SymbolTable<'a> { pub fn new() -> Self { Self { symbols: HashMap::new(), + bare_imports: HashSet::new(), } } + /// Record a bare name made available by a `use`. + pub fn add_bare_import(&mut self, name: &'a str) { + self.bare_imports.insert(name); + } + pub fn insert(&mut self, name: &'a str, kind: SymbolType) -> Result<(), SymbolType> { if let Some(existing) = self.symbols.get(name) { return Err(*existing); @@ -39,15 +50,10 @@ impl<'a> SymbolTable<'a> { self.symbols.contains_key(name) } - /// Whether `name` is the trailing segment of some `use ns::Name` import — - /// i.e. a bare reference that a `use` brought into scope, Rust-style. - /// `contains` is checked first; the qualified `ns::Name` form always - /// resolves through `contains`. + /// Whether `name` is a bare reference a `use` brought into scope, Rust-style + /// (an alias, or the trailing segment of a plain `use ns::Name`). The + /// qualified `ns::Name` form resolves through `contains` instead. pub fn is_imported_bare(&self, name: &str) -> bool { - self.symbols.iter().any(|(key, kind)| { - *kind == SymbolType::Import - && key.contains("::") - && key.rsplit("::").next() == Some(name) - }) + self.bare_imports.contains(name) } } diff --git a/core/src/schema/ir/validation/validator.rs b/core/src/schema/ir/validation/validator.rs index 890472d..1e2a2d7 100644 --- a/core/src/schema/ir/validation/validator.rs +++ b/core/src/schema/ir/validation/validator.rs @@ -14,27 +14,33 @@ pub fn validate(units: &[FrozenUnit]) -> Result<(), Vec> { // Pass 1: Collect Symbols & Check Duplicates for unit in units { + // Imports: the resolved path is a symbol (so a qualified `ns::Name` + // resolves), and the bare name a `use` brought into scope goes in a + // separate set (so it never conflicts and a local declaration shadows + // it). Redundant identical `use`s are fine, hence `let _`. + if let FrozenUnit::Import(path, alias, _span) = unit { + let _ = symbols.insert(path.as_str(), SymbolType::Import); + let bare = match alias { + Some(a) => Some(a.as_str()), + None => path.rsplit("::").next().filter(|s| *s != path.as_str()), + }; + if let Some(bare) = bare { + symbols.add_bare_import(bare); + } + continue; + } + let (name, kind, span) = match unit { FrozenUnit::Struct { name, span, .. } => (name.as_str(), SymbolType::Struct, Some(*span)), FrozenUnit::Enum { name, span, .. } => (name.as_str(), SymbolType::Enum, Some(*span)), FrozenUnit::Protocol { name, span, .. } => (name.as_str(), SymbolType::Protocol, Some(*span)), FrozenUnit::Constant { name, span, .. } => (name.as_str(), SymbolType::Constant, Some(*span)), - FrozenUnit::Import(path, span) => (path.as_str(), SymbolType::Import, Some(*span)), FrozenUnit::Validator { name, .. } => (name.as_str(), SymbolType::Validator, None), // TODO: Function handling if they become top-level _ => continue, }; - if let Err(existing_kind) = symbols.insert(name, kind) { - // Two `use` paths naming the same symbol (e.g. a whole-namespace - // import expanded alongside an explicit named import of one of - // its symbols) is redundant, not a conflict - only a real - // duplicate declaration, or an import colliding with one, is an - // error. - if kind == SymbolType::Import && existing_kind == SymbolType::Import { - continue; - } - + if let Err(_existing_kind) = symbols.insert(name, kind) { errors.push(ValidationError { message: format!("Duplicate definition of '{}'", name), context: format!("Definition of {:?} '{}'", kind, name), diff --git a/core/tests/package/from_sources.rs b/core/tests/package/from_sources.rs index 2d9de78..df13295 100644 --- a/core/tests/package/from_sources.rs +++ b/core/tests/package/from_sources.rs @@ -52,17 +52,22 @@ fn multiple_schemas_are_all_interpreted() { #[test] fn cross_schema_use_resolves_across_the_added_schemas() { - // Both the bare name (`use ns::Name` -> `Name`) and the qualified form - // (`ns::Name`) resolve. - for reference in ["User", "types::User"] { + // The bare name (`use ns::Name` -> `Name`), the qualified form (`ns::Name`), + // and an alias (`use ns::Name as X` -> `X`) all resolve. + let cases = [ + ("use types::User", "User"), + ("use types::User", "types::User"), + ("use types::User as Account", "Account"), + ]; + for (import, reference) in cases { let ctx = PackageSources::new() .schema(["types"], "struct User {\n id: u64\n}\n") .schema( ["api"], - &format!("use types::User\n\nstruct Session {{\n user: {reference}\n}}\n"), + &format!("{import}\n\nstruct Session {{\n user: {reference}\n}}\n"), ) .compile() - .unwrap_or_else(|e| panic!("`user: {reference}` should resolve: {e}")); + .unwrap_or_else(|e| panic!("`{import}` / `{reference}` should resolve: {e}")); assert_eq!(ctx.schema_contexts.len(), 2); for sc in &ctx.schema_contexts { diff --git a/core/tests/schema/ir/examples_compile.rs b/core/tests/schema/ir/examples_compile.rs index 4acdd5b..aaecec0 100644 --- a/core/tests/schema/ir/examples_compile.rs +++ b/core/tests/schema/ir/examples_compile.rs @@ -115,7 +115,7 @@ fn test_imports_package_compiles_with_resolved_cross_file_use() { let import_paths: Vec<&str> = frozen .iter() .filter_map(|unit| match unit { - FrozenUnit::Import(path, _) => Some(path.as_str()), + FrozenUnit::Import(path, _, _) => Some(path.as_str()), _ => None, }) .collect(); diff --git a/core/tests/schema/ir/generation.rs b/core/tests/schema/ir/generation.rs index 58ad53a..e490e43 100644 --- a/core/tests/schema/ir/generation.rs +++ b/core/tests/schema/ir/generation.rs @@ -292,7 +292,7 @@ const MIN_VALUE: s8 = -128 let ir_units = IncrementalInterpreter::from_source(code); assert_eq!(ir_units.len(), 1); match &ir_units[0] { - comline_core::schema::ir::frozen::unit::FrozenUnit::Import(path, _) => { + comline_core::schema::ir::frozen::unit::FrozenUnit::Import(path, _, _) => { assert_eq!(path, "std"); } _ => panic!("Expected Import unit"), diff --git a/core/tests/schema/ir/imports.rs b/core/tests/schema/ir/imports.rs index 171d225..f52349e 100644 --- a/core/tests/schema/ir/imports.rs +++ b/core/tests/schema/ir/imports.rs @@ -60,7 +60,7 @@ fn test_whole_schema_use_resolves_across_files() { assert!( frozen .iter() - .any(|unit| matches!(unit, FrozenUnit::Import(path, _) if path == "types")), + .any(|unit| matches!(unit, FrozenUnit::Import(path, _, _) if path == "types")), "Expected a resolved import of 'types', got {:?}", frozen ); @@ -82,7 +82,7 @@ fn test_symbol_use_resolves_to_declaring_schema() { assert!( frozen .iter() - .any(|unit| matches!(unit, FrozenUnit::Import(path, _) if path == "types::User")), + .any(|unit| matches!(unit, FrozenUnit::Import(path, _, _) if path == "types::User")), "Expected a resolved import of 'types::User', got {:?}", frozen ); @@ -109,7 +109,7 @@ fn test_multi_item_use_resolves_each_symbol() { assert!( frozen .iter() - .any(|unit| matches!(unit, FrozenUnit::Import(path, _) if path == expected)), + .any(|unit| matches!(unit, FrozenUnit::Import(path, _, _) if path == expected)), "Expected a resolved import of '{}', got {:?}", expected, frozen @@ -133,7 +133,7 @@ fn test_glob_use_resolves_namespace() { assert!( frozen .iter() - .any(|unit| matches!(unit, FrozenUnit::Import(path, _) if path == "types::*")), + .any(|unit| matches!(unit, FrozenUnit::Import(path, _, _) if path == "types::*")), "Expected a resolved glob import of 'types::*', got {:?}", frozen ); @@ -166,7 +166,7 @@ fn test_unresolved_use_does_not_panic() { assert!( frozen .iter() - .any(|unit| matches!(unit, FrozenUnit::Import(path, _) if path.starts_with("