2121
2222DEPRECATED_PROMISE_TYPES = ["defaults" , "guest_environments" ]
2323ALLOWED_BUNDLE_TYPES = ["agent" , "common" , "monitor" , "server" , "edit_line" , "edit_xml" ]
24+ BUILTIN_PROMISE_TYPES = {
25+ "access" ,
26+ "build_xpath" ,
27+ "classes" ,
28+ "commands" ,
29+ "databases" ,
30+ "defaults" ,
31+ "delete_attribute" ,
32+ "delete_lines" ,
33+ "delete_text" ,
34+ "delete_tree" ,
35+ "field_edits" ,
36+ "files" ,
37+ "guest_environments" ,
38+ "insert_lines" ,
39+ "insert_text" ,
40+ "insert_tree" ,
41+ "measurements" ,
42+ "meta" ,
43+ "methods" ,
44+ "packages" ,
45+ "processes" ,
46+ "replace_patterns" ,
47+ "reports" ,
48+ "roles" ,
49+ "services" ,
50+ "set_attribute" ,
51+ "set_text" ,
52+ "storage" ,
53+ "users" ,
54+ "vars" ,
55+ }
2456
2557
2658def lint_cfbs_json (filename ) -> int :
@@ -97,7 +129,7 @@ def _find_nodes(filename, lines, node):
97129 return matches
98130
99131
100- def _single_node_checks (filename , lines , node ):
132+ def _single_node_checks (filename , lines , node , custom_promise_types , strict ):
101133 """Things which can be checked by only looking at one node,
102134 not needing to recurse into children."""
103135 line = node .range .start_point [0 ] + 1
@@ -117,6 +149,15 @@ def _single_node_checks(filename, lines, node):
117149 f"Deprecation: Promise type '{ promise_type } ' is deprecated at { filename } :{ line } :{ column } "
118150 )
119151 return 1
152+ if strict and (
153+ (promise_type not in BUILTIN_PROMISE_TYPES .union (custom_promise_types ))
154+ ):
155+ _highlight_range (node , lines )
156+ print (
157+ f"Error: Undefined promise type '{ promise_type } ' at { filename } :{ line } :{ column } "
158+ )
159+ return 1
160+
120161 if node .type == "bundle_block_name" :
121162 if _text (node ) != _text (node ).lower ():
122163 _highlight_range (node , lines )
@@ -138,10 +179,14 @@ def _single_node_checks(filename, lines, node):
138179 f"Error: Bundle type must be one of ({ ', ' .join (ALLOWED_BUNDLE_TYPES )} ), not '{ _text (node )} ' at { filename } :{ line } :{ column } "
139180 )
140181 return 1
182+
141183 return 0
142184
143185
144- def _walk (filename , lines , node ) -> int :
186+ def _walk (filename , lines , node , custom_promise_types = None , strict = True ) -> int :
187+ if custom_promise_types is None :
188+ custom_promise_types = set ()
189+
145190 error_nodes = _find_node_type (filename , lines , node , "ERROR" )
146191 if error_nodes :
147192 for node in error_nodes :
@@ -156,13 +201,41 @@ def _walk(filename, lines, node) -> int:
156201
157202 errors = 0
158203 for node in _find_nodes (filename , lines , node ):
159- errors += _single_node_checks (filename , lines , node )
204+ errors += _single_node_checks (
205+ filename , lines , node , custom_promise_types , strict
206+ )
160207
161208 return errors
162209
163210
211+ def _parse_custom_types (filename , lines , root_node ):
212+ ret = set ()
213+ promise_blocks = _find_node_type (filename , lines , root_node , "promise_block_name" )
214+ ret .update (_text (x ) for x in promise_blocks )
215+ return ret
216+
217+
218+ def _parse_policy_file (filename ):
219+ assert os .path .isfile (filename )
220+ PY_LANGUAGE = Language (tscfengine .language ())
221+ parser = Parser (PY_LANGUAGE )
222+
223+ with open (filename , "rb" ) as f :
224+ original_data = f .read ()
225+ tree = parser .parse (original_data )
226+ lines = original_data .decode ().split ("\n " )
227+
228+ return tree , lines , original_data
229+
230+
164231def lint_policy_file (
165- filename , original_filename = None , original_line = None , snippet = None , prefix = None
232+ filename ,
233+ original_filename = None ,
234+ original_line = None ,
235+ snippet = None ,
236+ prefix = None ,
237+ custom_promise_types = None ,
238+ strict = True ,
166239):
167240 assert original_filename is None or type (original_filename ) is str
168241 assert original_line is None or type (original_line ) is int
@@ -177,14 +250,11 @@ def lint_policy_file(
177250 assert snippet and snippet > 0
178251 assert os .path .isfile (filename )
179252 assert filename .endswith ((".cf" , ".cfengine3" , ".cf3" , ".cf.sub" ))
180- PY_LANGUAGE = Language (tscfengine .language ())
181- parser = Parser (PY_LANGUAGE )
182253
183- with open (filename , "rb" ) as f :
184- original_data = f .read ()
185- tree = parser .parse (original_data )
186- lines = original_data .decode ().split ("\n " )
254+ if custom_promise_types is None :
255+ custom_promise_types = set ()
187256
257+ tree , lines , original_data = _parse_policy_file (filename )
188258 root_node = tree .root_node
189259 if root_node .type != "source_file" :
190260 if snippet :
@@ -214,7 +284,7 @@ def lint_policy_file(
214284 else :
215285 print (f"Error: Empty policy file '{ filename } '" )
216286 errors += 1
217- errors += _walk (filename , lines , root_node )
287+ errors += _walk (filename , lines , root_node , custom_promise_types , strict )
218288 if prefix :
219289 print (prefix , end = "" )
220290 if errors == 0 :
@@ -235,8 +305,9 @@ def lint_policy_file(
235305 return errors
236306
237307
238- def lint_folder (folder ):
308+ def lint_folder (folder , strict = True ):
239309 errors = 0
310+ policy_files = []
240311 while folder .endswith (("/." , "/" )):
241312 folder = folder [0 :- 1 ]
242313 for filename in itertools .chain (
@@ -246,22 +317,45 @@ def lint_folder(folder):
246317 continue
247318 if filename .startswith ("." ) and not filename .startswith ("./" ):
248319 continue
249- errors += lint_single_file (filename )
320+
321+ if filename .endswith ((".cf" , ".cfengine3" , ".cf3" , ".cf.sub" )):
322+ policy_files .append (filename )
323+ else :
324+ errors += lint_single_file (filename )
325+
326+ custom_promise_types = set ()
327+
328+ # First pass: Gather custom types
329+ for filename in policy_files if strict else []:
330+ tree , lines , _ = _parse_policy_file (filename )
331+ if tree .root_node .type == "source_file" :
332+ custom_promise_types .update (
333+ _parse_custom_types (filename , lines , tree .root_node )
334+ )
335+
336+ # Second pass: lint all policy files
337+ for filename in policy_files :
338+ errors += lint_policy_file (
339+ filename , custom_promise_types = custom_promise_types , strict = strict
340+ )
250341 return errors
251342
252343
253- def lint_single_file (file ):
344+ def lint_single_file (file , custom_promise_types = None , strict = True ):
254345 assert os .path .isfile (file )
255346 if file .endswith ("/cfbs.json" ):
256347 return lint_cfbs_json (file )
257348 if file .endswith (".json" ):
258349 return lint_json (file )
259350 assert file .endswith (".cf" )
260- return lint_policy_file (file )
351+ return lint_policy_file (
352+ file , custom_promise_types = custom_promise_types , strict = strict
353+ )
261354
262355
263- def lint_single_arg (arg ):
356+ def lint_single_arg (arg , strict = True ):
264357 if os .path .isdir (arg ):
265- return lint_folder (arg )
358+ return lint_folder (arg , strict )
266359 assert os .path .isfile (arg )
267- return lint_single_file (arg )
360+
361+ return lint_single_file (arg , strict )
0 commit comments