2727 are a bit awkward. Could make iteration nicer.
2828"""
2929
30+ from copy import deepcopy
3031from enum import Enum
3132import os
3233import json
3334from typing import Callable , Iterable
3435import tree_sitter_cfengine as tscfengine
35- from dataclasses import dataclass
36+ from dataclasses import dataclass , field
3637from tree_sitter import Language , Node , Parser , Tree
3738from cfbs .validate import validate_config
3839from cfbs .cfbs_config import CFBSConfig
@@ -179,6 +180,13 @@ def visit(x):
179180
180181 _walk_callback (tree .root_node , visit )
181182
183+ def __deepcopy__ (self , _ ):
184+ """Overrides deepcopy for state-snapshotting.
185+ treesitter-tree is not pickleable, and PolicyFile
186+ should not change across state snapshots
187+ """
188+ return self
189+
182190
183191@dataclass
184192class State :
@@ -199,6 +207,8 @@ class State:
199207 promise_type : str | None = None # "vars" | "files" | "classes" | ... | None
200208 attribute_name : str | None = None # "if" | "string" | "slist" | ... | None
201209 namespace : str = DEFAULT_NAMESPACE # "ns" | "default" | ... |
210+ macro : str | None = None # "minimum_version()", "else", "between_versions()"
211+ old_state : dict = field (default_factory = dict )
202212 mode : Mode = Mode .NONE
203213 walking : bool = False
204214 strict : bool = True
@@ -307,11 +317,14 @@ def _add_definition(self, name: str, node: Node, definitions: dict) -> None:
307317 parameters = list (filter ("," .__ne__ , iter (_text (x ) for x in args )))
308318 else :
309319 parameters = []
320+
310321 definition = {
311322 "filename" : self .policy_file .filename ,
312323 "line" : node .range .start_point [0 ] + 1 ,
313324 "parameters" : parameters ,
314325 }
326+ if self .macro :
327+ definition ["macro" ] = self .macro
315328 if name not in definitions :
316329 definitions [name ] = []
317330 definitions [name ].append (definition )
@@ -429,6 +442,22 @@ def navigate(self, node) -> None:
429442 self .promise_type = None
430443 return
431444
445+ if node .type == "macro" :
446+ macro_type = _text (node )
447+ if macro_type .startswith ("@if" ):
448+ self .old_state = deepcopy (self .__dict__ )
449+ self .macro = macro_type .split (" " )[- 1 ]
450+ elif macro_type .startswith ("@else" ):
451+ self .macro = "else"
452+ self .old_state = deepcopy (self .__dict__ )
453+ self .__dict__ .update (self .old_state )
454+ elif macro_type .startswith ("@endif" ):
455+ self .macro = None
456+ self .__dict__ .update (self .old_state )
457+ # NOTE: this or that? maybe a dict of states based on the macro-type?
458+ self .old_state = {}
459+ return
460+
432461
433462def _check_syntax (policy_file : PolicyFile , state : State ) -> int :
434463 """Iterate over a syntax tree and print errors if it is empty or has syntax
@@ -746,7 +775,23 @@ def _lint_node(
746775 f"Error: Expected { expected } arguments, received { len (args )} for body '{ call } ' { location } "
747776 )
748777 return 1
749-
778+ if node .type == "half_promise" :
779+ prev_sib = node .prev_named_sibling
780+ while prev_sib and prev_sib .type == "comment" :
781+ prev_sib = prev_sib .prev_named_sibling
782+ prev_type = prev_sib .type if prev_sib else None
783+ if not state .macro :
784+ _highlight_range (node , lines )
785+ print (
786+ f"Error: Found promise attribute with no parent-promiser outside of a macro { location } "
787+ )
788+ return 1
789+ elif prev_type != "macro" :
790+ _highlight_range (node , lines )
791+ print (
792+ f"Error: Multiple promise attributes with ending semicolon found inside macro '{ state .macro } ' { location } "
793+ )
794+ return 1
750795 return 0
751796
752797
0 commit comments