@@ -493,14 +493,26 @@ def navigate(self, node) -> None:
493493 return
494494
495495
496+ def _last_real_sibling_before (node : Node ) -> Node | None :
497+ """Walk backwards from node through its preceding siblings, skipping
498+ comments and macros, to find the last real (content) sibling."""
499+ sib = node .prev_sibling
500+ while sib is not None and sib .type in ("comment" , "macro" ):
501+ sib = sib .prev_sibling
502+ return sib
503+
504+
496505def _check_syntax (policy_file : PolicyFile , state : State ) -> int :
497- """Iterate over a syntax tree and print errors if it is empty or has syntax
498- errors.
506+ """Iterate over a syntax tree and print errors if it is empty, has syntax
507+ errors, or is missing expected tokens (e.g. a dropped ';') .
499508
500509 Notably, printing syntax errors _does not happen during parsing_.
501510
502511 Tree sitter has already fully parsed the policy, we iterate over the result
503- and see if it has inserted any "ERROR" nodes.
512+ and see if it has inserted any "ERROR" nodes, or any "MISSING" nodes
513+ (tree-sitter's error recovery can silently insert a synthetic token,
514+ e.g. a missing ';' right before a closing '}', without ever producing
515+ an ERROR node.
504516
505517 Stops at first error. Returns number of errors, always 0 or 1 in this case.
506518 """
@@ -522,15 +534,26 @@ def _check_syntax(policy_file: PolicyFile, state: State) -> int:
522534 state .start_file (policy_file )
523535 for node in policy_file .nodes :
524536 state .navigate (node )
525- if node .type != "ERROR" :
526- continue
527537 line = node .range .start_point [0 ] + 1
528538 column = node .range .start_point [1 ] + 1
529- _highlight_range (node , lines )
530- location = state .get_location_extended (line , column )
531- print (f"Error: Syntax error { location } " )
532- state .end_file ()
533- return 1
539+ if node .type == "ERROR" :
540+ _highlight_range (node , lines )
541+ location = state .get_location_extended (line , column )
542+ print (f"Error: Syntax error { location } " )
543+ state .end_file ()
544+ return 1
545+ if node .is_missing :
546+ # To avoid pointing at #comment/@macro -lines
547+ prev = _last_real_sibling_before (node )
548+ report_node = prev if prev is not None else node
549+ _highlight_range (report_node , lines )
550+ line = report_node .range .end_point [0 ] + 1
551+ column = report_node .range .end_point [1 ] + 1
552+ missing = node .type if not node .is_named else f"token: { node .type } "
553+ location = state .get_location_extended (line , column )
554+ print (f"Error: Missing '{ missing } ' { location } " )
555+ state .end_file ()
556+ return 1
534557 state .end_file ()
535558 return 0
536559
0 commit comments