From 46811865074adfd9422f8d4bccaef6a5366debb3 Mon Sep 17 00:00:00 2001 From: can1357 Date: Tue, 7 Apr 2026 02:23:38 +0200 Subject: [PATCH] fix(chunk): corrected namespace and module member addressability with recursion handlers - Fixed namespace and module member addressability by adding dedicated recursion handlers for internal modules and namespace definitions. - Fixed indent detection to scan source content as fallback when chunk metadata unavailable, improving robustness. - Changed default indent character from tab to space for consistency with modern conventions. - Added comprehensive test coverage for namespace member addressability, internal module recursion, and indent detection fallback logic. --- crates/pi-natives/src/chunk/ast_js_ts.rs | 12 +++- crates/pi-natives/src/chunk/ast_misc.rs | 17 +++++- crates/pi-natives/src/chunk/edit.rs | 70 ++++++++++++++++++++---- crates/pi-natives/src/chunk/indent.rs | 59 ++++++++++++++++++-- crates/pi-natives/src/chunk/mod.rs | 55 +++++++++++++++++++ 5 files changed, 191 insertions(+), 22 deletions(-) diff --git a/crates/pi-natives/src/chunk/ast_js_ts.rs b/crates/pi-natives/src/chunk/ast_js_ts.rs index ce76165d3..32ab4f996 100644 --- a/crates/pi-natives/src/chunk/ast_js_ts.rs +++ b/crates/pi-natives/src/chunk/ast_js_ts.rs @@ -6,6 +6,10 @@ use super::{classify::LangClassifier, common::*, defaults::promote_assigned_expr pub struct JsTsClassifier; +fn recurse_internal_module(node: Node<'_>) -> Option> { + recurse_into(node, ChunkContext::ClassBody, &["body"], &["statement_block"]) +} + impl LangClassifier for JsTsClassifier { fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option> { match node.kind() { @@ -42,7 +46,9 @@ impl LangClassifier for JsTsClassifier { Some(container_candidate(node, "iface", source, recurse_interface(node))) }, "enum_declaration" => Some(container_candidate(node, "enum", source, recurse_enum(node))), - "internal_module" => Some(container_candidate(node, "mod", source, recurse_class(node))), + "internal_module" => { + Some(container_candidate(node, "mod", source, recurse_internal_module(node))) + }, // ── Types ── "type_alias_declaration" => Some(named_candidate(node, "type", source, None)), @@ -59,7 +65,7 @@ impl LangClassifier for JsTsClassifier { .into_iter() .find(|c| c.kind() == "internal_module"); if let Some(ns) = inner { - Some(container_candidate(ns, "mod", source, recurse_class(ns))) + Some(container_candidate(ns, "mod", source, recurse_internal_module(ns))) } else { Some(group_candidate(node, "stmts", source)) } @@ -303,7 +309,7 @@ fn classify_export_statement<'t>(node: Node<'t>, source: &str) -> RawChunkCandid } }, "internal_module" => { - let recurse = recurse_class(child); + let recurse = recurse_internal_module(child); if is_default { make_container_chunk_from(node, child, "default_export".to_string(), source, recurse) } else { diff --git a/crates/pi-natives/src/chunk/ast_misc.rs b/crates/pi-natives/src/chunk/ast_misc.rs index 32d24a42e..2ab68a426 100644 --- a/crates/pi-natives/src/chunk/ast_misc.rs +++ b/crates/pi-natives/src/chunk/ast_misc.rs @@ -16,6 +16,16 @@ impl LangClassifier for MiscClassifier { recurse_body(node, ChunkContext::FunctionBody) .or_else(|| recurse_into(node, ChunkContext::FunctionBody, &["body"], &["block"])) }; + let module_recurse = || { + recurse_class(node).or_else(|| { + recurse_into(node, ChunkContext::ClassBody, &["body"], &[ + "compound_statement", + "statement_block", + "declaration_list", + "block", + ]) + }) + }; Some(match node.kind() { // ── Imports / package headers ── "import_statement" @@ -83,9 +93,10 @@ impl LangClassifier for MiscClassifier { "contract_declaration" | "library_declaration" | "trait_declaration" => { container_candidate(node, "contract", source, recurse_class(node)) }, - "namespace_declaration" | "module_definition" | "extension_definition" => { - container_candidate(node, "mod", source, recurse_class(node)) - }, + "namespace_declaration" + | "namespace_definition" + | "module_definition" + | "extension_definition" => container_candidate(node, "mod", source, module_recurse()), // ── Types / aliases ── "type_alias_declaration" | "const_type_declaration" | "opaque_declaration" => { diff --git a/crates/pi-natives/src/chunk/edit.rs b/crates/pi-natives/src/chunk/edit.rs index 874227e56..e11603bb3 100644 --- a/crates/pi-natives/src/chunk/edit.rs +++ b/crates/pi-natives/src/chunk/edit.rs @@ -47,7 +47,7 @@ pub fn apply_edits(state: &ChunkState, params: &EditParams) -> Result Vec ChunkState { + let tree = build_chunk_tree(source, language).expect("tree should build"); + ChunkState::from_inner(ChunkStateInner::new(source.to_owned(), language.to_owned(), tree)) + } + + #[test] + fn root_level_replace_preserves_space_indentation() { + let source = "fn main() {\n println!(\"old\");\n}\n"; + let state = state_for(source, "rust"); + let chunk = state.inner().chunk("fn_main").expect("fn_main"); + + let result = apply_edits(&state, &EditParams { + operations: vec![EditOperation { + op: ChunkEditOp::Replace, + sel: Some("fn_main".to_owned()), + crc: Some(chunk.checksum.clone()), + content: Some("fn main() {\n println!(\"new\");\n}".to_owned()), + line: None, + end_line: None, + }], + default_selector: None, + default_crc: None, + anchor_style: None, + cwd: ".".to_owned(), + file_path: "test.rs".to_owned(), + }) + .expect("edit should apply"); + + assert!( + result.diff_after.contains("println!(\"new\");"), + "expected updated body text, got {:?}", + result.diff_after + ); + assert!( + !result.diff_after.contains("\n\tprintln!(\"new\");\n"), + "expected no tab-indented body, got {:?}", + result.diff_after + ); + } +} diff --git a/crates/pi-natives/src/chunk/indent.rs b/crates/pi-natives/src/chunk/indent.rs index ef956cfe6..44c3996fb 100644 --- a/crates/pi-natives/src/chunk/indent.rs +++ b/crates/pi-natives/src/chunk/indent.rs @@ -232,19 +232,33 @@ pub fn reindent_inserted_block( indent_non_empty_lines(&dedented, &normalized_target_indent) } -/// Detect the file's indent character by looking at non-root chunks. -/// Returns `'\t'` or `' '`, defaulting to `'\t'` if no chunks have indentation. -pub fn detect_file_indent_char(tree: &ChunkTree) -> char { +/// Detect the file's indent character. +/// Prefer chunk metadata, then fall back to scanning source lines. +/// Returns `' '` when the file provides no indentation signal. +pub fn detect_file_indent_char(source: &str, tree: &ChunkTree) -> char { for chunk in &tree.chunks { if chunk.indent > 0 && !chunk.indent_char.is_empty() { - return chunk.indent_char.chars().next().unwrap_or('\t'); + return chunk.indent_char.chars().next().unwrap_or(' '); } } - '\t' + + for line in source.split('\n') { + if line.trim().is_empty() { + continue; + } + if let Some(ch) = leading_whitespace(line).chars().next() + && matches!(ch, ' ' | '\t') + { + return ch; + } + } + + ' ' } /// Detect spaces-per-indent-level from parent→child indent differences. -/// Only meaningful for space-indented files; returns `DEFAULT_SPACE_INDENT_STEP` for tab files. +/// Only meaningful for space-indented files; returns +/// `DEFAULT_SPACE_INDENT_STEP` for tab files. pub fn detect_file_indent_step(tree: &ChunkTree) -> u32 { for chunk in &tree.chunks { if chunk.children.is_empty() { @@ -552,4 +566,37 @@ mod tests { }; assert_eq!(detect_file_indent_step(&tree), 2); } + + #[test] + fn detect_file_indent_char_falls_back_to_source_lines() { + let tree = ChunkTree { + language: "rust".to_owned(), + checksum: "ABCD".to_owned(), + line_count: 3, + parse_errors: 0, + fallback: false, + root_path: String::new(), + root_children: vec!["fn_main".to_owned()], + chunks: vec![chunk("fn_main", Some(""), &[], 0, "")], + }; + + let source = "fn main() {\n println!(\"hi\");\n}\n"; + assert_eq!(detect_file_indent_char(source, &tree), ' '); + } + + #[test] + fn detect_file_indent_char_defaults_to_spaces_without_signal() { + let tree = ChunkTree { + language: "rust".to_owned(), + checksum: "ABCD".to_owned(), + line_count: 0, + parse_errors: 0, + fallback: false, + root_path: String::new(), + root_children: Vec::new(), + chunks: Vec::new(), + }; + + assert_eq!(detect_file_indent_char("", &tree), ' '); + } } diff --git a/crates/pi-natives/src/chunk/mod.rs b/crates/pi-natives/src/chunk/mod.rs index 924407b8c..98c0106e9 100644 --- a/crates/pi-natives/src/chunk/mod.rs +++ b/crates/pi-natives/src/chunk/mod.rs @@ -1824,4 +1824,59 @@ end assert!(!enum_chunk.leaf); assert!(!enum_chunk.children.is_empty(), "mixed-size variants should stay addressable"); } + + #[test] + fn typescript_namespace_members_stay_addressable() { + let source = r"namespace Foo { + export function bar() { + return 1; + } + } + "; + let tree = build_chunk_tree(source, "typescript").expect("tree should build"); + + let module = tree + .chunks + .iter() + .find(|c| c.path == "mod_Foo") + .expect("mod_Foo"); + assert!(!module.leaf); + assert!( + module + .children + .iter() + .any(|child| child == "mod_Foo.fn_bar"), + "expected fn_bar inside namespace, got {:?}", + module.children + ); + } + + #[test] + fn php_namespace_definition_keeps_inner_members_addressable() { + let source = "