@@ -51,7 +51,7 @@ pub fn dump_ast(ast: &Ast, root: Id, source: &str) -> String {
5151
5252pub fn dump_ast_with_options ( ast : & Ast , root : Id , source : & str , options : & DumpOptions ) -> String {
5353 let mut out = String :: new ( ) ;
54- dump_node ( ast, root, source, options, 0 , None , & mut out) ;
54+ dump_node ( ast, root, source, options, 0 , None , false , & mut out) ;
5555 out
5656}
5757
@@ -86,6 +86,7 @@ pub fn dump_ast_with_type_errors_and_options(
8686 expected : None ,
8787 parent_field : None ,
8888 } ) ,
89+ false ,
8990 & mut out,
9091 ) ;
9192 out
@@ -195,6 +196,7 @@ fn dump_node(
195196 options : & DumpOptions ,
196197 indent : usize ,
197198 type_check : Option < TypeCheckContext < ' _ > > ,
199+ external_to_parent : bool ,
198200 out : & mut String ,
199201) {
200202 let node = match ast. get_node ( id) {
@@ -232,6 +234,9 @@ fn dump_node(
232234
233235 if options. show_abridged_source {
234236 write_source_skeleton ( ast, node, source, out) ;
237+ if external_to_parent {
238+ write ! ( out, " (external)" ) . unwrap ( ) ;
239+ }
235240 }
236241
237242 if let Some ( context) = type_check {
@@ -300,9 +305,18 @@ fn dump_node(
300305 write ! ( out, "{prefix} {field_name}:" ) . unwrap ( ) ;
301306 // Inline single child
302307 let child = ast. get_node ( children[ 0 ] ) ;
308+ let external = child. is_some_and ( |child| is_external_child ( node, child) ) ;
303309 if child. is_some_and ( is_leaf) {
304310 write ! ( out, " " ) . unwrap ( ) ;
305- dump_node_inline ( ast, children[ 0 ] , source, options, child_type_check, out) ;
311+ dump_node_inline (
312+ ast,
313+ children[ 0 ] ,
314+ source,
315+ options,
316+ child_type_check,
317+ external,
318+ out,
319+ ) ;
306320 } else {
307321 writeln ! ( out) . unwrap ( ) ;
308322 dump_node (
@@ -312,19 +326,24 @@ fn dump_node(
312326 options,
313327 indent + 2 ,
314328 child_type_check,
329+ external,
315330 out,
316331 ) ;
317332 }
318333 } else {
319334 writeln ! ( out, "{prefix} {field_name}:" ) . unwrap ( ) ;
320335 for & child_id in children {
336+ let external = ast
337+ . get_node ( child_id)
338+ . is_some_and ( |child| is_external_child ( node, child) ) ;
321339 dump_node (
322340 ast,
323341 child_id,
324342 source,
325343 options,
326344 indent + 2 ,
327345 child_type_check,
346+ external,
328347 out,
329348 ) ;
330349 }
@@ -365,13 +384,15 @@ fn dump_node(
365384 for & child_id in children {
366385 if let Some ( child) = ast. get_node ( child_id) {
367386 if child. is_named ( ) {
387+ let external = is_external_child ( node, child) ;
368388 dump_node (
369389 ast,
370390 child_id,
371391 source,
372392 options,
373393 indent + 1 ,
374394 child_type_check,
395+ external,
375396 out,
376397 ) ;
377398 }
@@ -387,6 +408,7 @@ fn dump_node_inline(
387408 source : & str ,
388409 options : & DumpOptions ,
389410 type_check : Option < TypeCheckContext < ' _ > > ,
411+ external_to_parent : bool ,
390412 out : & mut String ,
391413) {
392414 let node = match ast. get_node ( id) {
@@ -419,6 +441,9 @@ fn dump_node_inline(
419441
420442 if options. show_abridged_source {
421443 write_source_skeleton ( ast, node, source, out) ;
444+ if external_to_parent {
445+ write ! ( out, " (external)" ) . unwrap ( ) ;
446+ }
422447 }
423448
424449 if let Some ( context) = type_check {
@@ -457,6 +482,13 @@ fn node_source_range(node: &Node) -> Option<crate::Range> {
457482 }
458483}
459484
485+ fn is_external_child ( parent : & Node , child : & Node ) -> bool {
486+ let ( Some ( parent) , Some ( child) ) = ( node_source_range ( parent) , node_source_range ( child) ) else {
487+ return false ;
488+ } ;
489+ child. start_byte < parent. start_byte || child. end_byte > parent. end_byte
490+ }
491+
460492fn source_skeleton ( ast : & Ast , node : & Node , source : & str ) -> SourceSkeleton {
461493 let Some ( parent) = node_source_range ( node) else {
462494 return SourceSkeleton :: Missing ;
@@ -494,10 +526,7 @@ fn source_skeleton(ast: &Ast, node: &Node, source: &str) -> SourceSkeleton {
494526 ) ) ;
495527 }
496528 if child. start < parent. start || child. end > parent. end {
497- return SourceSkeleton :: Invalid ( format ! (
498- "child range {}..{} is outside node range {}..{}" ,
499- child. start, child. end, parent. start, parent. end
500- ) ) ;
529+ continue ;
501530 }
502531 if child. start == child. end {
503532 continue ;
@@ -613,12 +642,6 @@ mod tests {
613642 #[ test]
614643 fn source_skeleton_validates_empty_child_ranges ( ) {
615644 let cases = [
616- (
617- "abcdef" ,
618- range ( 0 , 3 ) ,
619- range ( 4 , 4 ) ,
620- "child range 4..4 is outside node range 0..3" ,
621- ) ,
622645 (
623646 "abcdef" ,
624647 range ( 0 , 6 ) ,
@@ -640,6 +663,13 @@ mod tests {
640663 "unexpected dump: {dump}"
641664 ) ;
642665 }
666+
667+ let dump = dump_with_children ( "abcdef" , range ( 0 , 3 ) , & [ ( "marker" , range ( 4 , 4 ) ) ] ) ;
668+ assert ! ( dump. starts_with( "parent source=\" abc\" \n " ) ) ;
669+ assert ! (
670+ dump. contains( "marker: child source=\" \" (external)\n " ) ,
671+ "unexpected dump: {dump}"
672+ ) ;
643673 }
644674
645675 #[ test]
@@ -667,12 +697,15 @@ mod tests {
667697 }
668698
669699 #[ test]
670- fn source_skeleton_reports_children_outside_the_parent ( ) {
700+ fn source_skeleton_marks_children_outside_the_parent ( ) {
671701 let source = "abcdefghi" ;
672702 let dump = dump_with_children ( source, range ( 0 , 6 ) , & [ ( "child" , range ( 7 , 9 ) ) ] ) ;
673703
674- assert ! ( dump
675- . starts_with( "parent source=<invalid: child range 7..9 is outside node range 0..6>\n " ) ) ;
704+ assert ! ( dump. starts_with( "parent source=\" abcdef\" \n " ) ) ;
705+ assert ! (
706+ dump. contains( " child source=\" hi\" (external)\n " ) ,
707+ "unexpected dump: {dump}"
708+ ) ;
676709 }
677710}
678711
0 commit comments