11use std:: fmt:: Write ;
22
3- use crate :: { Ast , Node , NodeContent , CHILD_FIELD } ;
3+ use crate :: { schema :: Schema , Ast , Node , NodeContent , CHILD_FIELD } ;
44
55/// Options for controlling AST dump output.
66pub struct DumpOptions {
@@ -45,16 +45,143 @@ pub fn dump_ast_with_options(
4545 options : & DumpOptions ,
4646) -> String {
4747 let mut out = String :: new ( ) ;
48- dump_node ( ast, root, source, options, 0 , & mut out) ;
48+ dump_node ( ast, root, source, options, 0 , None , & mut out) ;
4949 out
5050}
5151
52+ /// Dump an AST and annotate type mismatches against a schema inline.
53+ ///
54+ /// Any node that does not match the expected type set for its parent field is
55+ /// rendered with a trailing `" <-- ERROR: ..."` annotation on the same line.
56+ pub fn dump_ast_with_type_errors (
57+ ast : & Ast ,
58+ root : usize ,
59+ source : & str ,
60+ schema : & Schema ,
61+ ) -> String {
62+ dump_ast_with_type_errors_and_options ( ast, root, source, schema, & DumpOptions :: default ( ) )
63+ }
64+
65+ /// Dump an AST and annotate type mismatches against a schema inline.
66+ ///
67+ /// Any node that does not match the expected type set for its parent field is
68+ /// rendered with a trailing `" <-- ERROR: ..."` annotation on the same line.
69+ pub fn dump_ast_with_type_errors_and_options (
70+ ast : & Ast ,
71+ root : usize ,
72+ source : & str ,
73+ schema : & Schema ,
74+ options : & DumpOptions ,
75+ ) -> String {
76+ let mut out = String :: new ( ) ;
77+ dump_node ( ast, root, source, options, 0 , Some ( ( schema, None , None ) ) , & mut out) ;
78+ out
79+ }
80+
81+ fn format_node_types ( node_types : & [ crate :: schema:: NodeType ] ) -> String {
82+ node_types
83+ . iter ( )
84+ . map ( |t| {
85+ if t. named {
86+ t. kind . clone ( )
87+ } else {
88+ format ! ( "\" {}\" " , t. kind)
89+ }
90+ } )
91+ . collect :: < Vec < _ > > ( )
92+ . join ( " | " )
93+ }
94+
95+ const EMPTY_NODE_TYPES : & [ crate :: schema:: NodeType ] = & [ ] ;
96+
97+ /// Generate a type-checking error message for a node if it doesn't match expected types.
98+ ///
99+ /// # Arguments
100+ /// - `schema`: The AST schema to validate against.
101+ /// - `node`: The node being checked.
102+ /// - `expected`: The set of allowed types for this node, or `None` if type-checking is disabled.
103+ /// - `parent_field`: Optional tuple of (parent_kind, field_name) for context in error messages.
104+ ///
105+ /// # Returns
106+ /// `Some(error_message)` if the node violates the schema (e.g., wrong kind, missing field declaration).
107+ /// `None` if the node matches the expected types or if type-checking is disabled.
108+ fn type_error_for_node (
109+ schema : & Schema ,
110+ node : & Node ,
111+ expected : Option < & [ crate :: schema:: NodeType ] > ,
112+ parent_field : Option < ( & str , & str ) > ,
113+ ) -> Option < String > {
114+ if schema. id_for_node_kind ( node. kind_name ( ) ) . is_none ( )
115+ && schema. id_for_unnamed_node_kind ( node. kind_name ( ) ) . is_none ( )
116+ {
117+ return Some ( format ! ( "node kind '{}' not in schema" , node. kind_name( ) ) ) ;
118+ }
119+
120+ let expected = expected?;
121+ if expected. is_empty ( ) {
122+ if let Some ( ( kind, field) ) = parent_field {
123+ return Some ( format ! ( "the node '{kind}' has no field '{field}'" ) ) ;
124+ }
125+ return Some ( "field not declared in schema for this parent node" . to_string ( ) ) ;
126+ }
127+ if schema. node_matches_types ( node. kind_name ( ) , node. is_named ( ) , expected) {
128+ None
129+ } else {
130+ let actual = if node. is_named ( ) {
131+ node. kind_name ( ) . to_string ( )
132+ } else {
133+ format ! ( "\" {}\" " , node. kind_name( ) )
134+ } ;
135+
136+ if let Some ( ( kind, field) ) = parent_field {
137+ Some ( format ! (
138+ "The field {}.{} should contain {}, but got {}" ,
139+ kind,
140+ field,
141+ format_node_types( expected) ,
142+ actual
143+ ) )
144+ } else {
145+ Some ( format ! (
146+ "expected {}, got {}" ,
147+ format_node_types( expected) ,
148+ actual
149+ ) )
150+ }
151+ }
152+ }
153+
154+ /// Look up the allowed types for a field in the schema.
155+ ///
156+ /// # Arguments
157+ /// - `schema`: The AST schema to query.
158+ /// - `parent_kind`: The node kind of the parent that contains this field.
159+ /// - `field_id`: The field ID within that parent node.
160+ ///
161+ /// # Returns
162+ /// `Some(&[NodeType])` if the field is declared in the schema and has type constraints.
163+ /// `None` if the field is not declared or has no constraints (undeclared field).
164+ fn expected_for_field < ' a > (
165+ schema : & ' a Schema ,
166+ parent_kind : & str ,
167+ field_id : u16 ,
168+ ) -> Option < & ' a [ crate :: schema:: NodeType ] > {
169+ schema
170+ . field_types ( parent_kind, field_id)
171+ . map ( |v| v. as_slice ( ) )
172+ }
173+
52174fn dump_node (
53175 ast : & Ast ,
54176 id : usize ,
55177 source : & str ,
56178 options : & DumpOptions ,
57179 indent : usize ,
180+ type_check : Option < (
181+ & Schema ,
182+ Option < & [ crate :: schema:: NodeType ] > ,
183+ Option < ( & str , & str ) > ,
184+ ) > ,
58185 out : & mut String ,
59186) {
60187 let node = match ast. get_node ( id) {
@@ -90,6 +217,12 @@ fn dump_node(
90217 }
91218 }
92219
220+ if let Some ( ( schema, expected, parent_field) ) = type_check {
221+ if let Some ( err) = type_error_for_node ( schema, node, expected, parent_field) {
222+ write ! ( out, " <-- ERROR: {err}" ) . unwrap ( ) ;
223+ }
224+ }
225+
93226 writeln ! ( out) . unwrap ( ) ;
94227
95228 // Named fields first
@@ -98,39 +231,87 @@ fn dump_node(
98231 continue ; // Handle unnamed children last
99232 }
100233 let field_name = ast. field_name_for_id ( field_id) . unwrap_or ( "?" ) ;
234+ let child_type_check = type_check. map ( |( schema, _, _) | {
235+ let expected = expected_for_field ( schema, node. kind_name ( ) , field_id)
236+ . or ( Some ( EMPTY_NODE_TYPES ) ) ;
237+ let parent_field = Some ( ( node. kind_name ( ) , field_name) ) ;
238+ ( schema, expected, parent_field)
239+ } ) ;
240+
101241 if children. len ( ) == 1 {
102242 write ! ( out, "{prefix} {field_name}:" ) . unwrap ( ) ;
103243 // Inline single child
104244 let child = ast. get_node ( children[ 0 ] ) ;
105245 if child. is_some_and ( is_leaf) {
106246 write ! ( out, " " ) . unwrap ( ) ;
107- dump_node_inline ( ast, children[ 0 ] , source, options, out) ;
247+ dump_node_inline ( ast, children[ 0 ] , source, options, child_type_check , out) ;
108248 } else {
109249 writeln ! ( out) . unwrap ( ) ;
110- dump_node ( ast, children[ 0 ] , source, options, indent + 2 , out) ;
250+ dump_node (
251+ ast,
252+ children[ 0 ] ,
253+ source,
254+ options,
255+ indent + 2 ,
256+ child_type_check,
257+ out,
258+ ) ;
111259 }
112260 } else {
113261 writeln ! ( out, "{prefix} {field_name}:" ) . unwrap ( ) ;
114262 for & child_id in children {
115- dump_node ( ast, child_id, source, options, indent + 2 , out) ;
263+ dump_node (
264+ ast,
265+ child_id,
266+ source,
267+ options,
268+ indent + 2 ,
269+ child_type_check,
270+ out,
271+ ) ;
116272 }
117273 }
118274 }
119275
120276 // Unnamed children — skip unnamed tokens (keywords, punctuation)
121277 if let Some ( children) = node. fields . get ( & CHILD_FIELD ) {
278+ let child_type_check = type_check. map ( |( schema, _, _) | {
279+ let expected = expected_for_field ( schema, node. kind_name ( ) , CHILD_FIELD )
280+ . or ( Some ( EMPTY_NODE_TYPES ) ) ;
281+ let parent_field = Some ( ( node. kind_name ( ) , "children" ) ) ;
282+ ( schema, expected, parent_field)
283+ } ) ;
122284 for & child_id in children {
123285 if let Some ( child) = ast. get_node ( child_id) {
124286 if child. is_named ( ) {
125- dump_node ( ast, child_id, source, options, indent + 1 , out) ;
287+ dump_node (
288+ ast,
289+ child_id,
290+ source,
291+ options,
292+ indent + 1 ,
293+ child_type_check,
294+ out,
295+ ) ;
126296 }
127297 }
128298 }
129299 }
130300}
131301
132302/// Dump a leaf node inline (no newline prefix, caller provides context).
133- fn dump_node_inline ( ast : & Ast , id : usize , source : & str , options : & DumpOptions , out : & mut String ) {
303+ fn dump_node_inline (
304+ ast : & Ast ,
305+ id : usize ,
306+ source : & str ,
307+ options : & DumpOptions ,
308+ type_check : Option < (
309+ & Schema ,
310+ Option < & [ crate :: schema:: NodeType ] > ,
311+ Option < ( & str , & str ) > ,
312+ ) > ,
313+ out : & mut String ,
314+ ) {
134315 let node = match ast. get_node ( id) {
135316 Some ( n) => n,
136317 None => return ,
@@ -159,6 +340,12 @@ fn dump_node_inline(ast: &Ast, id: usize, source: &str, options: &DumpOptions, o
159340 }
160341 }
161342
343+ if let Some ( ( schema, expected, parent_field) ) = type_check {
344+ if let Some ( err) = type_error_for_node ( schema, node, expected, parent_field) {
345+ write ! ( out, " <-- ERROR: {err}" ) . unwrap ( ) ;
346+ }
347+ }
348+
162349 writeln ! ( out) . unwrap ( ) ;
163350}
164351
0 commit comments