Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/src/autodoc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 { .. } => {}
Expand Down
38 changes: 25 additions & 13 deletions core/src/schema/ir/compiler/interpreter/incremental.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -450,6 +456,7 @@ fn resolve_use_declaration(
project_context: &ProjectContext,
current_namespace: &[String],
use_path: &UsePath,
alias: Option<String>,
span: (usize, usize),
) -> Vec<FrozenUnit> {
let resolver = ImportResolver::new(vec![], Default::default(), None);
Expand All @@ -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!("<unresolved: {}>", message), span)];
return vec![FrozenUnit::Import(format!("<unresolved: {}>", 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
Expand All @@ -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
}
Expand All @@ -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)],
}
}
6 changes: 5 additions & 1 deletion core/src/schema/ir/frozen/unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>, (usize, usize)),
Constant {
docstring: Option<String>,
name: String,
Expand Down
26 changes: 16 additions & 10 deletions core/src/schema/ir/validation/symbols.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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);
Expand All @@ -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)
}
}
28 changes: 17 additions & 11 deletions core/src/schema/ir/validation/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,33 @@ pub fn validate(units: &[FrozenUnit]) -> Result<(), Vec<ValidationError>> {

// 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),
Expand Down
15 changes: 10 additions & 5 deletions core/tests/package/from_sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion core/tests/schema/ir/examples_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion core/tests/schema/ir/generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
67 changes: 61 additions & 6 deletions core/tests/schema/ir/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand All @@ -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
);
Expand All @@ -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
Expand All @@ -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
);
Expand Down Expand Up @@ -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("<unresolved:"))),
.any(|unit| matches!(unit, FrozenUnit::Import(path, _, _) if path.starts_with("<unresolved:"))),
"Expected an unresolved-import marker, got {:?}",
frozen
);
Expand All @@ -190,7 +190,7 @@ fn test_same_package_symbol_not_found_still_compiles() {
assert!(
frozen
.iter()
.any(|unit| matches!(unit, FrozenUnit::Import(path, _) if path == "types::Missing")),
.any(|unit| matches!(unit, FrozenUnit::Import(path, _, _) if path == "types::Missing")),
"Expected a best-effort import of 'types::Missing', got {:?}",
frozen
);
Expand Down Expand Up @@ -262,3 +262,58 @@ fn test_duplicate_struct_name_fails_compilation() {
result
);
}

#[test]
fn test_use_binds_the_bare_name() {
let mut project = build_project();
add_schema(&mut project, &["types"], "struct User {\n id: u64\n}\n");
add_schema(
&mut project,
&["api"],
// bare `User`, not `types::User`
"use types::User\n\nstruct Session {\n user: User\n}\n",
);

interpret_context(&project).expect("a bare reference after `use` should resolve");
}

#[test]
fn test_use_as_binds_the_alias() {
let mut project = build_project();
add_schema(&mut project, &["types"], "struct User {\n id: u64\n}\n");
add_schema(
&mut project,
&["api"],
"use types::User as Account\n\nstruct Session {\n who: Account\n}\n",
);

interpret_context(&project).expect("the alias should resolve as a type");

let frozen = frozen_units_for(&project, "api");
assert!(
frozen.iter().any(|unit| matches!(
unit,
FrozenUnit::Import(path, alias, _)
if path == "types::User" && alias.as_deref() == Some("Account")
)),
"import should carry the alias, got {:?}",
frozen
);
}

#[test]
fn test_use_as_does_not_bind_the_original_bare_name() {
let mut project = build_project();
add_schema(&mut project, &["types"], "struct User {\n id: u64\n}\n");
add_schema(
&mut project,
&["api"],
// aliased, so bare `User` is NOT in scope — only `Account` (or `types::User`)
"use types::User as Account\n\nstruct Session {\n who: User\n}\n",
);

assert!(
interpret_context(&project).is_err(),
"a bare `User` after `use ... as Account` should not resolve"
);
}
Loading