codehelion_core/ir.rs
1//! The Syntax IR: a language-neutral structural view of one source file.
2//!
3//! Structural mode compares code by shape, not just by token content. Each
4//! frontend parses a file with its real parser and maps the resulting tree
5//! onto this IR: a token stream (the same [`Token`] representation Fast mode
6//! uses) plus a tree of [`IrNode`]s whose [`Shape`]s come from a small
7//! cross-language vocabulary. Nodes that have no cross-language equivalent
8//! keep their native grammar kind instead of being forced into the nearest
9//! common shape — a C++ `template_declaration` stays distinguishable from a
10//! Rust generic function.
11//!
12//! Error tolerance follows the frontend contract: a malformed region becomes
13//! an [`Shape::Error`] node covering its source range and parsing continues.
14//! Consumers that segment the tree into units must search recursively and
15//! judge each found node by its own subtree, never by the mere presence of an
16//! error ancestor: real parsers wrap large healthy regions in error nodes
17//! whose ranges are the union of individually intact children.
18//!
19//! Macros and templates are not expanded in Fast or Structural mode. The IR
20//! records definition sites ([`Shape::MacroDef`]) and invocation sites
21//! ([`Shape::MacroCall`]) as ordinary nodes so later phases can attach
22//! expansion information without changing this schema.
23//!
24//! Byte ranges are the only positions stored on nodes; line/column rendering
25//! is a reporting concern served by the token stream. No position feeds any
26//! stable identifier.
27//!
28//! # Schema versioning
29//!
30//! [`IR_SCHEMA_VERSION`] is a fingerprint input, and fingerprints built from
31//! different IR schema versions are never considered equal. It stays at 1
32//! until the first release tag: nothing has shipped, so a change that alters a
33//! comparison result — adding or removing a [`Shape`], changing a shape tag,
34//! changing how frontends map native kinds — invalidates the databases holding
35//! the old results rather than being versioned away from them, and re-running
36//! the scan is the whole of the recovery.
37
38use crate::discovery::Language;
39use crate::frontend::{Diagnostic, Lexeme, Token};
40
41/// Version of the Syntax IR schema, recorded per file and hashed into every
42/// structural fingerprint.
43pub const IR_SCHEMA_VERSION: u32 = 1;
44
45/// Maximum number of IR nodes on one root-to-leaf path emitted by the bundled
46/// structural frontends.
47///
48/// Frontends stop descending before this budget can be exceeded and preserve
49/// the omitted source region as an [`Shape::Error`] leaf. This keeps
50/// structural recovery bounded for mechanically generated and adversarial
51/// source files.
52pub const MAX_IR_DEPTH: usize = 500;
53
54/// A half-open byte range into the source text.
55///
56/// Ordering is by start then end, which is source order: it exists so ranges
57/// can be sorted into a deterministic reporting order, and carries no meaning
58/// beyond that.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
60pub struct ByteRange {
61 /// Byte offset of the range start.
62 pub start: usize,
63 /// Byte offset one past the range end.
64 pub end: usize,
65}
66
67impl ByteRange {
68 /// Length of the range in bytes; `0` for a malformed range.
69 #[must_use]
70 pub const fn len(&self) -> usize {
71 self.end.saturating_sub(self.start)
72 }
73
74 /// Whether the range covers no bytes.
75 #[must_use]
76 pub const fn is_empty(&self) -> bool {
77 self.len() == 0
78 }
79
80 /// Whether `other` lies entirely within this range.
81 #[must_use]
82 pub const fn contains(&self, other: &Self) -> bool {
83 self.start <= other.start && other.end <= self.end
84 }
85}
86
87/// The cross-language shape vocabulary.
88///
89/// Every variant except [`Shape::Native`] means the same thing in all three
90/// languages, so structural comparison across files (and, in later phases,
91/// across languages) can work on shapes alone. A frontend maps its grammar
92/// onto these; whatever does not fit is carried as [`Shape::Native`] with the
93/// grammar's own kind name preserved.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum Shape {
96 /// A free function definition.
97 Function,
98 /// A method definition (function inside an `impl`/class/struct body).
99 Method,
100 /// A closure or lambda.
101 Closure,
102 /// A record definition: `struct`, `class`, `union` or `enum`.
103 Record,
104 /// An implementation or member-definition container (`impl`, class body).
105 Impl,
106 /// A braced statement block.
107 Block,
108 /// A loop of any flavour (`for`, `while`, `loop`, do-while).
109 Loop,
110 /// A two-way conditional (`if`/`else` chain member).
111 Branch,
112 /// A multi-way conditional (`match`, `switch`).
113 Match,
114 /// One arm of a multi-way conditional.
115 MatchArm,
116 /// A function, method or macro-like call expression.
117 Call,
118 /// An assignment or compound assignment.
119 Assign,
120 /// A local variable declaration (`let`, C/C++ declaration statement).
121 VarDecl,
122 /// A `return` (or expression-position tail return).
123 Return,
124 /// An early loop exit (`break`).
125 Break,
126 /// A loop continuation (`continue`).
127 Continue,
128 /// Error propagation or handling (`?` operator, `try`/`catch`).
129 Try,
130 /// An expression used as a statement, not covered by a finer shape.
131 ExprStmt,
132 /// A macro or preprocessor definition site.
133 MacroDef,
134 /// A macro invocation site (not expanded).
135 MacroCall,
136 /// A region the parser could not interpret. Children may still be intact.
137 Error,
138 /// A node kept under its native grammar kind because no common shape
139 /// applies. The kind name takes part in structural comparison, so two
140 /// native nodes match only when their grammars call them the same thing.
141 Native(Lexeme),
142}
143
144impl Shape {
145 /// A stable one-byte tag for this shape, for use as fingerprint input.
146 ///
147 /// For [`Shape::Native`] the tag alone is not sufficient input: the
148 /// native kind name must be hashed alongside it, or all native nodes
149 /// would collapse into one shape.
150 #[must_use]
151 pub const fn tag(&self) -> u8 {
152 match self {
153 Self::Function => 1,
154 Self::Method => 2,
155 Self::Closure => 3,
156 Self::Record => 4,
157 Self::Impl => 5,
158 Self::Block => 6,
159 Self::Loop => 7,
160 Self::Branch => 8,
161 Self::Match => 9,
162 Self::MatchArm => 10,
163 Self::Call => 11,
164 Self::Assign => 12,
165 Self::VarDecl => 13,
166 Self::Return => 14,
167 Self::Break => 15,
168 Self::Continue => 16,
169 Self::Try => 17,
170 Self::ExprStmt => 18,
171 Self::MacroDef => 19,
172 Self::MacroCall => 20,
173 Self::Error => 21,
174 Self::Native(_) => 22,
175 }
176 }
177
178 /// Whether this shape opens a lexical scope for alpha renaming.
179 ///
180 /// This is the structural basis normalization uses to rename identifiers
181 /// consistently within — and only within — one scope. The judgement is
182 /// syntactic and shared by all three languages: bodies bind, containers
183 /// and single statements do not.
184 #[must_use]
185 pub const fn introduces_scope(&self) -> bool {
186 matches!(
187 self,
188 Self::Function
189 | Self::Method
190 | Self::Closure
191 | Self::Block
192 | Self::Loop
193 | Self::Branch
194 | Self::Match
195 | Self::MatchArm
196 | Self::Try
197 )
198 }
199
200 /// Whether nodes of this shape are statements for the purposes of
201 /// statement sequences and statement-window fragments.
202 #[must_use]
203 pub const fn is_statement(&self) -> bool {
204 matches!(
205 self,
206 Self::Loop
207 | Self::Branch
208 | Self::Match
209 | Self::Assign
210 | Self::VarDecl
211 | Self::Return
212 | Self::Break
213 | Self::Continue
214 | Self::Try
215 | Self::ExprStmt
216 | Self::MacroCall
217 )
218 }
219}
220
221/// One node of the Syntax IR tree.
222///
223/// A node covers a contiguous token range (`token_start..token_end` indices
224/// into [`SyntaxIrFile::tokens`]) and a contiguous byte range of the source.
225/// Children are in source order and lie within their parent's ranges. Nodes
226/// destroy their descendants iteratively so dropping a recovered file does not
227/// consume stack space proportional to its depth.
228#[derive(Debug, Clone, PartialEq, Eq)]
229pub struct IrNode {
230 /// The node's shape.
231 pub shape: Shape,
232 /// The declared name, when the frontend can recover one (functions,
233 /// methods, records, macro definitions).
234 pub name: Option<Lexeme>,
235 /// Index of the node's first token in the file's token stream.
236 pub token_start: usize,
237 /// Index one past the node's last token.
238 pub token_end: usize,
239 /// Source bytes the node covers.
240 pub range: ByteRange,
241 /// Child nodes in source order.
242 pub children: Vec<Self>,
243}
244
245impl IrNode {
246 /// Number of tokens the node covers; `0` for a malformed range.
247 #[must_use]
248 pub const fn token_len(&self) -> usize {
249 self.token_end.saturating_sub(self.token_start)
250 }
251
252 /// Depth-first pre-order traversal over this node and its descendants.
253 ///
254 /// The traversal is iterative so callers can safely inspect externally
255 /// constructed IR too, even when it did not come from a bounded frontend.
256 pub fn walk(&self, visit: &mut impl FnMut(&Self)) {
257 let mut pending = vec![self];
258 while let Some(node) = pending.pop() {
259 visit(node);
260 pending.extend(node.children.iter().rev());
261 }
262 }
263
264 /// The sequence of statement children, summarised.
265 ///
266 /// This is the per-block statement sequence view of the IR: direct
267 /// children whose shapes are statements, in source order, each reduced
268 /// to its [`StatementSummary`]. Non-statement children (nested items,
269 /// blocks acting as expressions) are skipped, matching how statement
270 /// windows are cut.
271 ///
272 /// [`Shape::Native`] children are included: a native node that is a
273 /// direct child of the node being summarised sits in statement position
274 /// by construction (C `goto`, a preprocessor conditional inside a
275 /// function body), and dropping it would silently shorten the sequence.
276 #[must_use]
277 pub fn statement_summaries(&self, tokens: &[Token]) -> Vec<StatementSummary> {
278 self.children
279 .iter()
280 .filter(|child| child.shape.is_statement() || matches!(child.shape, Shape::Native(_)))
281 .map(|child| StatementSummary::of(child, tokens))
282 .collect()
283 }
284}
285
286impl Drop for IrNode {
287 fn drop(&mut self) {
288 let mut worklist = std::mem::take(&mut self.children);
289 while let Some(mut node) = worklist.pop() {
290 worklist.append(&mut node.children);
291 }
292 }
293}
294
295/// How many leading tokens a [`StatementSummary`] keeps.
296pub const SUMMARY_HEAD_TOKENS: usize = 4;
297
298/// A statement reduced to its shape and the span of its tokens.
299///
300/// The shape carries the rename-invariant signal that aligns two statement
301/// sequences; the span is how the text itself is recovered, for the lexical
302/// comparison that decides whether aligned statements are actually copies.
303///
304/// The span is kept rather than the token texts because a compound statement
305/// covers its whole body: cloning the texts would cost a copy of the token
306/// stream once per level of nesting, while an index pair costs the same
307/// whatever the statement contains. It is a position into one file's stream
308/// and nothing more — identity in this tool is content-derived, and no
309/// fingerprint reads this type.
310#[derive(Debug, Clone, PartialEq, Eq)]
311pub struct StatementSummary {
312 /// Shape tag of the statement (see [`Shape::tag`]).
313 pub shape_tag: u8,
314 /// Native kind name when the statement is a [`Shape::Native`] node.
315 pub native_kind: Option<Lexeme>,
316 /// Index of the statement's first token in its file's stream.
317 pub token_start: usize,
318 /// Index one past the statement's last token in its file's stream.
319 pub token_end: usize,
320}
321
322impl StatementSummary {
323 /// Summarise one node against its file's token stream.
324 #[must_use]
325 pub fn of(node: &IrNode, tokens: &[Token]) -> Self {
326 let native_kind = match &node.shape {
327 Shape::Native(kind) => Some(kind.clone()),
328 _ => None,
329 };
330 let token_end = node.token_end.min(tokens.len());
331 Self {
332 shape_tag: node.shape.tag(),
333 native_kind,
334 token_start: node.token_start.min(token_end),
335 token_end,
336 }
337 }
338
339 /// The statement's tokens, resolved against the stream it was summarised
340 /// from. Empty for any other stream, since the span would not be its own.
341 #[must_use]
342 pub fn tokens<'a>(&self, tokens: &'a [Token]) -> &'a [Token] {
343 tokens.get(self.token_start..self.token_end).unwrap_or(&[])
344 }
345}
346
347/// The Syntax IR of one source file.
348#[derive(Debug, Clone)]
349pub struct SyntaxIrFile {
350 /// Language the file was parsed as.
351 pub language: Language,
352 /// Version tag of the structural frontend that produced this IR; a
353 /// fingerprint input alongside [`IR_SCHEMA_VERSION`].
354 pub frontend_version: &'static str,
355 /// IR schema version this file conforms to.
356 pub ir_schema_version: u32,
357 /// Tokens in source order, comments and whitespace removed. Same
358 /// representation as Fast mode, so token-level normalization is shared.
359 pub tokens: Vec<Token>,
360 /// Top-level IR nodes in source order.
361 pub roots: Vec<IrNode>,
362 /// Recoverable lexical problems, as in Fast mode.
363 pub diagnostics: Vec<Diagnostic>,
364 /// Source regions the parser marked as errors. Overlapping nodes are
365 /// still emitted; these ranges only lower confidence downstream.
366 pub error_ranges: Vec<ByteRange>,
367 /// Whether the frontend stopped descending after reaching its structural
368 /// depth budget.
369 ///
370 /// This is deliberately separate from [`Self::error_ranges`]: malformed
371 /// source is ordinary recovery, while a depth ceiling means the scan was
372 /// intentionally incomplete and must be reported as such.
373 pub depth_truncated: bool,
374 /// Whether the file is the body of a module its tree declares test-only.
375 ///
376 /// Not something a parse can answer: the declaration carrying the marker
377 /// is in another file, so this is settled once the whole set is known and
378 /// left here for the walk that reads a unit's markers to start from. A
379 /// frontend leaves it false. See
380 /// [`declared_test_modules`](crate::test_code::declared_test_modules).
381 pub test_module: bool,
382}
383
384impl SyntaxIrFile {
385 /// Depth-first pre-order traversal over every node in the file.
386 pub fn walk(&self, visit: &mut impl FnMut(&IrNode)) {
387 for root in &self.roots {
388 root.walk(visit);
389 }
390 }
391
392 /// Total number of nodes in the file.
393 #[must_use]
394 pub fn node_count(&self) -> usize {
395 let mut count = 0;
396 self.walk(&mut |_| count += 1);
397 count
398 }
399
400 /// Tokens the parser could not attach to any structure: those inside a
401 /// [`Shape::Error`] node that none of its children recovered.
402 ///
403 /// This is the honest measure of what a parse lost, and
404 /// [`error_ranges`](Self::error_ranges) is not. An error-tolerant parser
405 /// recovering from one bad construct routinely wraps everything around it
406 /// in a single error node: a header whose include guard encloses the file
407 /// gets one error region covering every byte of it, with the whole file's
408 /// declarations intact inside. Measured over one project's C++ sources,
409 /// the error regions covered 13.3% of the bytes while the tokens that
410 /// actually failed to parse were 1.82% — the difference is entirely code
411 /// the parser did read, sitting inside a region it had to open.
412 ///
413 /// Nesting is not double-counted: an error node inside another is covered
414 /// by its parent's children, so the parent contributes only the gaps
415 /// around it and the child contributes its own.
416 #[must_use]
417 pub fn unaccounted_tokens(&self) -> usize {
418 let mut lost = 0;
419 self.walk(&mut |node| {
420 if matches!(node.shape, Shape::Error) {
421 let recovered: usize = node.children.iter().map(IrNode::token_len).sum();
422 lost += node.token_len().saturating_sub(recovered);
423 }
424 });
425 lost
426 }
427}
428
429/// A Structural-mode parser for one language.
430///
431/// Like the Fast [`Frontend`](crate::frontend::Frontend), a structural
432/// frontend never executes, expands or resolves anything in the target code:
433/// it parses text and maps the tree. Malformed input degrades to
434/// [`Shape::Error`] nodes plus [`SyntaxIrFile::error_ranges`]. A frontend
435/// that reaches its depth budget records the remaining source region in the
436/// same way instead of continuing recursive descent.
437pub trait StructuralFrontend {
438 /// The language this frontend parses.
439 fn language(&self) -> Language;
440
441 /// The frontend's version tag, used as a fingerprint input.
442 fn frontend_version(&self) -> &'static str;
443
444 /// Parse `source` into a Syntax IR file.
445 fn parse(&self, source: &str) -> SyntaxIrFile;
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451 use crate::frontend::{SourceSpan, TokenKind};
452
453 fn token(text: &str, start_byte: usize) -> Token {
454 Token {
455 kind: TokenKind::Identifier,
456 text: Lexeme::from(text),
457 span: SourceSpan {
458 start_byte,
459 end_byte: start_byte + text.len(),
460 start_line: 1,
461 start_column: 1,
462 },
463 }
464 }
465
466 fn node(shape: Shape, token_start: usize, token_end: usize) -> IrNode {
467 IrNode {
468 shape,
469 name: None,
470 token_start,
471 token_end,
472 range: ByteRange {
473 start: token_start,
474 end: token_end,
475 },
476 children: Vec::new(),
477 }
478 }
479
480 #[test]
481 fn dropping_a_deep_tree_is_iterative() {
482 let mut tree = node(Shape::Block, 0, 0);
483 for _ in 0..10_000 {
484 tree = IrNode {
485 shape: Shape::Block,
486 name: None,
487 token_start: 0,
488 token_end: 0,
489 range: ByteRange { start: 0, end: 0 },
490 children: vec![tree],
491 };
492 }
493
494 drop(tree);
495 }
496
497 #[test]
498 fn shape_tags_are_distinct_and_stable() {
499 let shapes = [
500 Shape::Function,
501 Shape::Method,
502 Shape::Closure,
503 Shape::Record,
504 Shape::Impl,
505 Shape::Block,
506 Shape::Loop,
507 Shape::Branch,
508 Shape::Match,
509 Shape::MatchArm,
510 Shape::Call,
511 Shape::Assign,
512 Shape::VarDecl,
513 Shape::Return,
514 Shape::Break,
515 Shape::Continue,
516 Shape::Try,
517 Shape::ExprStmt,
518 Shape::MacroDef,
519 Shape::MacroCall,
520 Shape::Error,
521 Shape::Native(Lexeme::from("preproc_ifdef")),
522 ];
523 let mut tags: Vec<u8> = shapes.iter().map(Shape::tag).collect();
524 tags.sort_unstable();
525 tags.dedup();
526 assert_eq!(tags.len(), shapes.len(), "shape tags must be distinct");
527 // Tag values are part of the fingerprint schema: spot-pin endpoints.
528 assert_eq!(Shape::Function.tag(), 1);
529 assert_eq!(Shape::Native(Lexeme::from("x")).tag(), 22);
530 }
531
532 #[test]
533 fn native_nodes_share_a_tag_but_keep_their_kind() {
534 let a = Shape::Native(Lexeme::from("preproc_ifdef"));
535 let b = Shape::Native(Lexeme::from("using_declaration"));
536 assert_eq!(a.tag(), b.tag());
537 assert_ne!(a, b, "the kind name still distinguishes native shapes");
538 }
539
540 #[test]
541 fn scope_and_statement_tables_are_consistent() {
542 assert!(Shape::Function.introduces_scope());
543 assert!(Shape::Block.introduces_scope());
544 assert!(!Shape::Call.introduces_scope());
545 assert!(!Shape::Record.introduces_scope());
546
547 assert!(Shape::Return.is_statement());
548 assert!(Shape::MacroCall.is_statement());
549 assert!(!Shape::Function.is_statement(), "items are not statements");
550 assert!(!Shape::Block.is_statement());
551 }
552
553 #[test]
554 fn byte_range_arithmetic_guards_malformed_input() {
555 let range = ByteRange { start: 10, end: 20 };
556 assert_eq!(range.len(), 10);
557 assert!(!range.is_empty());
558 assert!(range.contains(&ByteRange { start: 12, end: 18 }));
559 assert!(!range.contains(&ByteRange { start: 5, end: 18 }));
560
561 let malformed = ByteRange { start: 20, end: 10 };
562 assert_eq!(malformed.len(), 0);
563 assert!(malformed.is_empty());
564 }
565
566 #[test]
567 fn statement_summaries_take_statement_children_in_order() {
568 let tokens: Vec<Token> = ["let", "x", "=", "f", "(", ")", "return", "x"]
569 .iter()
570 .enumerate()
571 .map(|(i, text)| token(text, i * 8))
572 .collect();
573
574 let mut block = node(Shape::Block, 0, 8);
575 block.children = vec![
576 node(Shape::VarDecl, 0, 6),
577 node(Shape::Function, 0, 0), // nested item: not a statement
578 node(Shape::Return, 6, 8),
579 ];
580
581 let summaries = block.statement_summaries(&tokens);
582 assert_eq!(summaries.len(), 2);
583 assert!(
584 summaries.iter().all(|s| s.native_kind.is_none()),
585 "no native statements in this block"
586 );
587 assert_eq!(summaries[0].shape_tag, Shape::VarDecl.tag());
588 let text = |summary: &StatementSummary| -> Vec<String> {
589 summary
590 .tokens(&tokens)
591 .iter()
592 .map(|token| token.text.as_str().to_string())
593 .collect()
594 };
595 assert_eq!(
596 text(&summaries[0]),
597 vec!["let", "x", "=", "f", "(", ")"],
598 "the span covers the whole statement, not just its head"
599 );
600 assert_eq!(summaries[1].shape_tag, Shape::Return.tag());
601 assert_eq!(text(&summaries[1]), vec!["return", "x"]);
602 }
603
604 #[test]
605 fn native_children_count_as_statements_in_position() {
606 let tokens = vec![token("goto", 0), token("fail", 8)];
607 let mut block = node(Shape::Block, 0, 2);
608 block.children = vec![IrNode {
609 shape: Shape::Native(Lexeme::from("goto_statement")),
610 name: None,
611 token_start: 0,
612 token_end: 2,
613 range: ByteRange { start: 0, end: 12 },
614 children: Vec::new(),
615 }];
616
617 let summaries = block.statement_summaries(&tokens);
618 assert_eq!(summaries.len(), 1);
619 assert_eq!(
620 summaries[0].native_kind,
621 Some(Lexeme::from("goto_statement"))
622 );
623 }
624
625 #[test]
626 fn summary_of_out_of_bounds_token_range_is_empty_not_panicking() {
627 let tokens = vec![token("x", 0)];
628 let stray = node(Shape::ExprStmt, 5, 9);
629 let summary = StatementSummary::of(&stray, &tokens);
630 assert!(summary.tokens(&tokens).is_empty());
631 }
632
633 #[test]
634 fn walk_visits_every_node_pre_order() {
635 let mut root = node(Shape::Function, 0, 10);
636 let mut block = node(Shape::Block, 1, 9);
637 block.children = vec![node(Shape::Return, 2, 4)];
638 root.children = vec![block];
639
640 let file = SyntaxIrFile {
641 language: Language::Rust,
642 frontend_version: "test-v1",
643 ir_schema_version: IR_SCHEMA_VERSION,
644 tokens: Vec::new(),
645 roots: vec![root],
646 diagnostics: Vec::new(),
647 error_ranges: Vec::new(),
648 depth_truncated: false,
649 test_module: false,
650 };
651
652 let mut seen = Vec::new();
653 file.walk(&mut |n| seen.push(n.shape.tag()));
654 assert_eq!(
655 seen,
656 vec![
657 Shape::Function.tag(),
658 Shape::Block.tag(),
659 Shape::Return.tag()
660 ]
661 );
662 assert_eq!(file.node_count(), 3);
663 }
664
665 /// A file whose roots are `roots`, for the traversal tests.
666 fn file_of(roots: Vec<IrNode>) -> SyntaxIrFile {
667 SyntaxIrFile {
668 language: Language::Rust,
669 frontend_version: "test-v1",
670 ir_schema_version: IR_SCHEMA_VERSION,
671 tokens: Vec::new(),
672 roots,
673 diagnostics: Vec::new(),
674 error_ranges: Vec::new(),
675 depth_truncated: false,
676 test_module: false,
677 }
678 }
679
680 #[test]
681 fn code_recovered_inside_an_error_node_is_not_counted_as_lost() {
682 // The shape an error-tolerant parser actually produces: one error
683 // node opened by a construct it could not read, holding everything
684 // that followed and parsed cleanly. Counting the node's own extent
685 // would call the whole file unreadable.
686 let mut wrapper = node(Shape::Error, 0, 100);
687 wrapper.children = vec![node(Shape::Function, 3, 60), node(Shape::Function, 60, 100)];
688 assert_eq!(
689 file_of(vec![wrapper]).unaccounted_tokens(),
690 3,
691 "only the tokens no child accounts for"
692 );
693 }
694
695 #[test]
696 fn an_error_node_that_recovered_nothing_loses_all_of_it() {
697 assert_eq!(
698 file_of(vec![node(Shape::Error, 0, 40)]).unaccounted_tokens(),
699 40
700 );
701 }
702
703 #[test]
704 fn a_file_the_parser_followed_loses_nothing() {
705 let mut function = node(Shape::Function, 0, 20);
706 function.children = vec![node(Shape::Block, 4, 20)];
707 assert_eq!(file_of(vec![function]).unaccounted_tokens(), 0);
708 }
709
710 #[test]
711 fn nested_error_nodes_count_their_own_gaps_once() {
712 // The inner error is one of the outer's children, so the outer counts
713 // only what surrounds it and the inner counts what it failed to
714 // recover. Adding both extents would report more than the file holds.
715 let mut inner = node(Shape::Error, 40, 60);
716 inner.children = vec![node(Shape::Return, 45, 55)];
717 let mut outer = node(Shape::Error, 0, 100);
718 outer.children = vec![node(Shape::Function, 0, 40), inner];
719 assert_eq!(
720 file_of(vec![outer]).unaccounted_tokens(),
721 40 + 10,
722 "the outer's trailing gap plus the inner's own"
723 );
724 }
725}