Skip to content

Commit 8335562

Browse files
shuaimuclaude
andcommitted
transpiler: fix absolute path qualification for shadowed module function calls
When code inside `tests::parser` calls functions from the outer `parser` module (e.g., `from_str<TestFlags>(s)` or `to_writer(f, &s)`), C++ name lookup found the inner `tests::parser` namespace instead. Fix: emit absolute paths (`::parser::from_str<T>(...)`) when a single-segment function call matches a function in `module_qualified_functions` and the module prefix is shadowed by a same-named namespace in the current scope. Also handles multi-segment paths (`parser::from_str<T>(...)` → `::parser::from_str<T>(...)`). Bitflags compilation errors: 447 → 363 (84 fewer). All namespace collision errors from architecture gap #7 are now resolved. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5c6eabd commit 8335562

2 files changed

Lines changed: 78 additions & 24 deletions

File tree

TODO.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2592,7 +2592,7 @@ Work on tasks defined in TODO.md. Repeat the following steps, don’t stop until
25922592
- [x] *done* Leaf 1.1: Pre-scan all items to detect inline module names that collide with function names anywhere in the file; rename colliding namespaces by appending `_tests` suffix
25932593
- [x] *done* Leaf 1.2: Apply renames consistently in forward declarations, full namespace body emission, and test wrapper call paths via `escape_and_rename_qualified_name()`
25942594
- [x] *done* Leaf 1.3: Add regression test for namespace/function collision rename pattern
2595-
- [ ] Leaf 1.4: Fix remaining path resolution for function calls inside renamed scopes — `from_str<T>(s)` inside `tests::parser` can't find `::parser::from_str<B>()` by unqualified lookup; needs either `using namespace ::parser;` injection or absolute path qualification
2595+
- [x] *done* Leaf 1.4: Fix path resolution for function calls inside renamed scopes — emit absolute `::parser::from_str<T>(...)` paths when calling functions from a parent module whose name is shadowed by a same-named namespace in the current scope. Handles both turbofish and non-turbofish calls. Bitflags errors: 447 → 363 (84 fewer).
25962596
- [ ] Leaf 2: Fix structured binding type deduction failures (fixes 6 semver errors)
25972597
- [ ] Leaf 2.1: Detect `let (a, b) = expr` where expr returns void or incomplete type and emit explicit typed bindings or skip destructuring
25982598
- [ ] Leaf 2.2: Add regression tests for structured bindings from functions returning tuples vs void
@@ -2630,7 +2630,7 @@ Work on tasks defined in TODO.md. Repeat the following steps, don’t stop until
26302630
- [x] *done* Leaf 12.1: Detect when expanded test code creates sub-modules with the same name as function templates and apply `_tests` suffix to test sub-module namespaces (done in Leaf 1.1)
26312631
- [x] *done* Leaf 12.2: Update test wrapper call paths to use renamed test namespaces (done in Leaf 1.2)
26322632
- [x] *done* Leaf 12.3: Add regression tests for namespace/function collision patterns (done in Leaf 1.3)
2633-
- [ ] Leaf 12.4: Fix remaining unqualified function calls inside renamed scopes (see Leaf 1.4)
2633+
- [x] *done* Leaf 12.4: Fix remaining unqualified function calls inside renamed scopes (done in Leaf 1.4)
26342634
- [ ] Leaf 13: Re-run parity matrix after all fixes and verify 7/7 crates pass
26352635
- [x] *done* Leaf 5: Verification matrix (required)
26362636
- [x] *done* Add an integration parity matrix test that runs `parity-test --stop-after run` for `either`, `tap`, `cfg-if`, `take_mut`, `arrayvec`, `semver`, and `bitflags`

transpiler/src/codegen.rs

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18107,34 +18107,54 @@ impl CodeGen {
1810718107
}
1810818108
// For single-segment paths that match a function declared in an
1810918109
// inline module, qualify with the module name to avoid name collision
18110-
// with identically-named test namespaces. Only applies when there's
18111-
// a turbofish arg (to distinguish from variable/type references).
18112-
// E.g., `from_str::<TestFlags>(s)` → `parser::from_str<TestFlags>(s)`
18110+
// with identically-named test namespaces.
18111+
// E.g., `from_str::<TestFlags>(s)` → `::parser::from_str<TestFlags>(s)`
18112+
// Also handles non-turbofish calls: `to_writer(f, &s)` → `::parser::to_writer(f, &s)`
1811318113
if path.segments.len() == 1 {
18114-
let seg = &path.segments[0];
18115-
let has_turbofish =
18116-
matches!(seg.arguments, syn::PathArguments::AngleBracketed(_));
18117-
if has_turbofish {
18118-
let fn_name = seg.ident.to_string();
18119-
if let Some(qualified) = self.module_qualified_functions.get(&fn_name) {
18120-
if !qualified.is_empty() {
18121-
let module_prefix = qualified.split("::").next().unwrap_or("");
18122-
// Only skip qualification if the INNERMOST module scope
18123-
// is the target module. Being in a nested sub-module
18124-
// (e.g., `parser::from_str`) still needs qualification
18125-
// because `namespace from_str` shadows `parser::from_str`.
18126-
let inside_module = self.module_stack.last().is_some_and(|m| m == module_prefix);
18127-
if !inside_module {
18128-
let mut emitted = qualified.clone();
18129-
if let Some(template_args) = self.emit_expr_path_template_args(path) {
18130-
emitted.push_str(&template_args);
18131-
}
18132-
return emitted;
18114+
let fn_name = path.segments[0].ident.to_string();
18115+
if let Some(qualified) = self.module_qualified_functions.get(&fn_name) {
18116+
if !qualified.is_empty() {
18117+
let module_prefix = qualified.split("::").next().unwrap_or("");
18118+
// Only skip qualification if the module stack is EXACTLY
18119+
// [module_prefix] — i.e., we're directly inside that module.
18120+
// Being in a nested sub-module with the same name
18121+
// (e.g., `tests::parser` vs `parser`) is NOT the same scope.
18122+
let directly_inside = self.module_stack.len() == 1
18123+
&& self.module_stack.last().is_some_and(|m| m == module_prefix);
18124+
if !directly_inside {
18125+
// Use absolute path (::prefix::fn) to avoid shadowing
18126+
// by same-named inner namespaces
18127+
let needs_root = self.module_stack.iter().any(|m| m == module_prefix);
18128+
let mut emitted = if needs_root {
18129+
format!("::{}", qualified)
18130+
} else {
18131+
qualified.clone()
18132+
};
18133+
if let Some(template_args) = self.emit_expr_path_template_args(path) {
18134+
emitted.push_str(&template_args);
1813318135
}
18136+
return emitted;
1813418137
}
1813518138
}
1813618139
}
1813718140
}
18141+
// For multi-segment paths where the first segment matches a module name
18142+
// that is shadowed by a same-named namespace in the current scope,
18143+
// prefix with `::` to force absolute lookup.
18144+
// E.g., inside `tests::parser`, `parser::from_str<T>(...)` needs
18145+
// `::parser::from_str<T>(...)` to avoid resolving to `tests::parser::from_str`.
18146+
if path.segments.len() >= 2 {
18147+
let first_seg = path.segments[0].ident.to_string();
18148+
let is_shadowed = self.module_stack.iter().any(|m| *m == first_seg)
18149+
&& self.module_stack.first().map(|m| m.as_str()) != Some(&first_seg);
18150+
if is_shadowed {
18151+
let mut emitted = format!("::{}", self.emit_path_to_string(path));
18152+
if let Some(template_args) = self.emit_expr_path_template_args(path) {
18153+
emitted.push_str(&template_args);
18154+
}
18155+
return emitted;
18156+
}
18157+
}
1813818158
let mut emitted = self.emit_path_to_string(path);
1813918159
if let Some(template_args) = self.emit_expr_path_template_args(path) {
1814018160
emitted.push_str(&template_args);
@@ -33709,4 +33729,38 @@ mod tests {
3370933729
"outer from_str function should keep its name\nGot: {out}"
3371033730
);
3371133731
}
33732+
33733+
#[test]
33734+
fn test_namespace_collision_absolute_path_qualification() {
33735+
// When calling a function from a parent module whose name is shadowed
33736+
// by a same-named namespace in the current scope, the transpiler
33737+
// should emit absolute path qualification.
33738+
let out = transpile_str(
33739+
r#"
33740+
mod parser {
33741+
pub fn from_str<B>(input: &str) -> i32 { 0 }
33742+
pub fn to_writer(flags: i32, s: &mut String) {}
33743+
}
33744+
mod tests {
33745+
mod parser {
33746+
fn roundtrip() {
33747+
let s = String::new();
33748+
to_writer(42, &mut s);
33749+
}
33750+
mod from_str {
33751+
fn valid() {
33752+
let r = from_str::<i32>("hello");
33753+
}
33754+
}
33755+
}
33756+
}
33757+
"#,
33758+
);
33759+
// Inside tests::parser, calls to from_str/to_writer should use
33760+
// absolute paths to avoid shadowing by the inner parser namespace
33761+
assert!(
33762+
out.contains("::parser::to_writer(") || out.contains("::parser::to_writer<"),
33763+
"to_writer call inside tests::parser should use absolute path\nGot: {out}"
33764+
);
33765+
}
3371233766
}

0 commit comments

Comments
 (0)