4242LINT_EXTENSIONS = (".cf" , ".json" )
4343
4444
45- def _load_syntax_description (path : str | None = None ) -> dict :
46- """Load and return the parsed syntax-description.json file."""
47- if path is None :
48- path = os .path .join (os .path .dirname (__file__ ), "syntax-description.json" )
49- with open (path , "r" ) as f :
50- return json .load (f )
51-
52-
53- def _derive_syntax_sets (data : dict ) -> tuple :
54- """Derive the four sets used for linting from a loaded syntax-description dict.
55-
56- Returns: (ALLOWED_BUNDLE_TYPES, BUILTIN_PROMISE_TYPES, BUILTIN_FUNCTIONS, DEPRECATED_PROMISE_TYPES)
57- """
58- builtin_body_types = set (data .get ("bodyTypes" , {}).keys ())
59-
60- allowed_bundle_types = data .get ("bundleTypes" , {}).keys ()
61-
62- builtin_promise_types = set (data .get ("promiseTypes" , {}).keys ())
45+ @dataclass
46+ class SyntaxData :
47+ BUILTIN_BODY_TYPES = {}
48+ BUILTIN_BUNDLE_TYPES = {}
49+ BUILTIN_PROMISE_TYPES = {}
50+ BUILTIN_FUNCTIONS = {}
51+
52+ def __init__ (self ):
53+ self ._data_dict = self ._load_syntax_description ()
54+ self ._derive_syntax_dicts (self ._data_dict )
55+
56+ assert self .BUILTIN_BODY_TYPES
57+ assert self .BUILTIN_BUNDLE_TYPES
58+ assert self .BUILTIN_PROMISE_TYPES
59+ assert self .BUILTIN_FUNCTIONS
60+
61+ def _load_syntax_description (self , path : str | None = None ) -> dict :
62+ """Load and return the parsed syntax-description.json file."""
63+ if path is None :
64+ path = os .path .join (os .path .dirname (__file__ ), "syntax-description.json" )
65+ with open (path , "r" ) as f :
66+ return json .load (f )
67+
68+ def _derive_syntax_dicts (self , data : dict ):
69+ """Derive the five dictionaries used for linting from a loaded syntax-description json-file.
70+ sets the (BUILTIN_BODY_TYPES, BUILTIN_BUNDLE_TYPES, BUILTIN_PROMISE_TYPES, BUILTIN_FUNCTIONS, DEPRECATED_PROMISE_TYPES) dicts
71+ """
72+ builtin_body_types = data .get ("bodyTypes" , {})
6373
64- builtin_functions = set ( data .get ("functions " , {}). keys () )
74+ builtin_bundle_types = data .get ("bundleTypes " , {})
6575
66- deprecated_promise_types = {
67- "defaults" ,
68- "guest_environments" ,
69- } # Has to be hardcoded, not tagged in syntax-description.json
76+ builtin_promise_types = data .get ("promiseTypes" , {})
7077
71- return (
72- builtin_body_types ,
73- allowed_bundle_types ,
74- builtin_promise_types ,
75- builtin_functions ,
76- deprecated_promise_types ,
77- )
78+ builtin_functions = data .get ("functions" , {})
7879
80+ deprecated_promise_types = {
81+ promise : builtin_promise_types .get (promise , {})
82+ for promise in {
83+ "defaults" ,
84+ "guest_environments" ,
85+ } # Has to be hardcoded, not tagged in syntax-description.json
86+ }
7987
80- _SYNTAX_DATA = _load_syntax_description ()
81- (
82- _ ,
83- ALLOWED_BUNDLE_TYPES ,
84- BUILTIN_PROMISE_TYPES ,
85- BUILTIN_FUNCTIONS ,
86- DEPRECATED_PROMISE_TYPES ,
87- ) = _derive_syntax_sets (_SYNTAX_DATA )
88+ self .BUILTIN_BODY_TYPES = builtin_body_types
89+ self .BUILTIN_BUNDLE_TYPES = builtin_bundle_types
90+ self .BUILTIN_PROMISE_TYPES = builtin_promise_types
91+ self .BUILTIN_FUNCTIONS = builtin_functions
92+ self .DEPRECATED_PROMISE_TYPES = deprecated_promise_types
8893
8994
9095def _qualify (name : str , namespace : str ) -> str :
@@ -487,7 +492,9 @@ def _discover(policy_file: PolicyFile, state: State) -> int:
487492 return 0
488493
489494
490- def _lint_node (node : Node , policy_file : PolicyFile , state : State ) -> int :
495+ def _lint_node (
496+ node : Node , policy_file : PolicyFile , state : State , syntax_data : SyntaxData
497+ ) -> int :
491498 """Checks we run on each node in the syntax tree,
492499 utilizes state for checks which require context."""
493500
@@ -503,15 +510,15 @@ def _lint_node(node: Node, policy_file: PolicyFile, state: State) -> int:
503510 if node .type == "promise_guard" :
504511 assert _text (node ) and len (_text (node )) > 1 and _text (node )[- 1 ] == ":"
505512 promise_type = _text (node )[0 :- 1 ]
506- if promise_type in DEPRECATED_PROMISE_TYPES :
513+ if promise_type in syntax_data . DEPRECATED_PROMISE_TYPES :
507514 _highlight_range (node , lines )
508515 print (
509516 f"Deprecation: Promise type '{ promise_type } ' is deprecated { location } "
510517 )
511518 return 1
512519 if (
513520 state .strict
514- and promise_type not in BUILTIN_PROMISE_TYPES
521+ and promise_type not in syntax_data . BUILTIN_PROMISE_TYPES
515522 and promise_type not in state .custom_promise_types
516523 ):
517524 _highlight_range (node , lines )
@@ -525,21 +532,60 @@ def _lint_node(node: Node, policy_file: PolicyFile, state: State) -> int:
525532 _highlight_range (node , lines )
526533 print (f"Convention: Promise type should be lowercase { location } " )
527534 return 1
528- if node .type == "bundle_block_type" and _text (node ) not in ALLOWED_BUNDLE_TYPES :
535+ if (
536+ node .type == "bundle_block_type"
537+ and _text (node ) not in syntax_data .BUILTIN_BUNDLE_TYPES
538+ ):
529539 _highlight_range (node , lines )
530540 print (
531- f"Error: Bundle type must be one of ({ ', ' .join (ALLOWED_BUNDLE_TYPES )} ), not '{ _text (node )} ' { location } "
541+ f"Error: Bundle type must be one of ({ ', ' .join (syntax_data . BUILTIN_BUNDLE_TYPES )} ), not '{ _text (node )} ' { location } "
532542 )
533543 return 1
534544 if state .strict and (
535545 node .type in ("bundle_block_name" , "body_block_name" )
536- and _text (node ) in BUILTIN_FUNCTIONS
546+ and _text (node ) in syntax_data . BUILTIN_FUNCTIONS
537547 ):
538548 _highlight_range (node , lines )
539549 print (
540550 f"Error: { 'Bundle' if 'bundle' in node .type else 'Body' } '{ _text (node )} ' conflicts with built-in function with the same name { location } "
541551 )
542552 return 1
553+ if state .promise_type == "vars" and node .type == "promise" :
554+ attribute_nodes = [x for x in node .children if x .type == "attribute" ]
555+ if not attribute_nodes :
556+ _highlight_range (node , lines )
557+ print (
558+ f"Error: Missing attribute value for promiser "
559+ f"{ _text (node )[:- 1 ]} inside vars-promise type { location } "
560+ )
561+ return 1
562+
563+ mutually_excl_vars_attrs = {
564+ "data" ,
565+ "ilist" ,
566+ "int" ,
567+ "real" ,
568+ "rlist" ,
569+ "slist" ,
570+ "string" ,
571+ }
572+
573+ promise_type_attrs = {
574+ _text (child ): attr_node
575+ for attr_node in attribute_nodes
576+ for child in attr_node .children
577+ if child .type == "attribute_name"
578+ and _text (child ) in mutually_excl_vars_attrs
579+ }
580+
581+ if len (promise_type_attrs ) > 1 :
582+ for n in promise_type_attrs :
583+ _highlight_range (promise_type_attrs [n ], lines )
584+ print (
585+ f"Error: Mutually exclusive attribute values { tuple (promise_type_attrs )} for a single promiser"
586+ f" inside vars-promise { location } "
587+ )
588+ return 1
543589 if node .type == "calling_identifier" :
544590 name = _text (node )
545591 qualified_name = _qualify (name , state .namespace )
@@ -556,15 +602,15 @@ def _lint_node(node: Node, policy_file: PolicyFile, state: State) -> int:
556602 if state .strict and (
557603 qualified_name not in state .bundles
558604 and qualified_name not in state .bodies
559- and name not in BUILTIN_FUNCTIONS
605+ and name not in syntax_data . BUILTIN_FUNCTIONS
560606 ):
561607 _highlight_range (node , lines )
562608 print (
563609 f"Error: Call to unknown function / bundle / body '{ name } ' { location } "
564610 )
565611 return 1
566612 if (
567- name not in BUILTIN_FUNCTIONS
613+ name not in syntax_data . BUILTIN_FUNCTIONS
568614 and state .promise_type == "vars"
569615 and state .attribute_name not in ("action" , "classes" )
570616 ):
@@ -605,14 +651,14 @@ def _pass_fail_state(state: State, errors: int) -> str:
605651 return _pass_fail_filename (pretty_filename , errors )
606652
607653
608- def _lint (policy_file : PolicyFile , state : State ) -> int :
654+ def _lint (policy_file : PolicyFile , state : State , syntax_data : SyntaxData ) -> int :
609655 """Run linting rules (checks) on nodes in a policy file syntax tree."""
610656 assert state .mode == Mode .LINT
611657 errors = 0
612658 state .start_file (policy_file )
613659 for node in policy_file .nodes :
614660 state .navigate (node )
615- errors += _lint_node (node , policy_file , state )
661+ errors += _lint_node (node , policy_file , state , syntax_data )
616662 message = _pass_fail_state (state , errors )
617663 state .end_file ()
618664 if state .prefix :
@@ -738,7 +784,11 @@ def _args_to_filenames(args: list[str]) -> list[str]:
738784
739785
740786def _lint_main (
741- args : list [str ], strict : bool , state = None , snippet : Snippet | None = None
787+ args : list [str ],
788+ strict : bool ,
789+ state = None ,
790+ snippet : Snippet | None = None ,
791+ syntax_data = None ,
742792) -> int :
743793 """This is the main function used for linting, it does all the steps on all
744794 the arguments (files / folders).
@@ -765,6 +815,9 @@ def _lint_main(
765815 state .strict = strict
766816 state .mode = Mode .SYNTAX
767817
818+ if syntax_data is None :
819+ syntax_data = SyntaxData ()
820+
768821 filenames = _args_to_filenames (args )
769822
770823 if snippet :
@@ -799,7 +852,7 @@ def _lint_main(
799852 state .mode = Mode .LINT
800853
801854 for policy_file in policy_files :
802- errors += _lint (policy_file , state )
855+ errors += _lint (policy_file , state , syntax_data )
803856
804857 return errors
805858
0 commit comments