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,11 @@ 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 = set (), strict = True ) -> int :
145187 error_nodes = _find_node_type (filename , lines , node , "ERROR" )
146188 if error_nodes :
147189 for node in error_nodes :
@@ -156,13 +198,41 @@ def _walk(filename, lines, node) -> int:
156198
157199 errors = 0
158200 for node in _find_nodes (filename , lines , node ):
159- errors += _single_node_checks (filename , lines , node )
201+ errors += _single_node_checks (
202+ filename , lines , node , custom_promise_types , strict
203+ )
160204
161205 return errors
162206
163207
208+ def _parse_custom_types (filename , lines , root_node ):
209+ ret = set ()
210+ promise_blocks = _find_node_type (filename , lines , root_node , "promise_block_name" )
211+ ret .update (_text (x ) for x in promise_blocks )
212+ return ret
213+
214+
215+ def _parse_policy_file (filename ):
216+ assert os .path .isfile (filename )
217+ PY_LANGUAGE = Language (tscfengine .language ())
218+ parser = Parser (PY_LANGUAGE )
219+
220+ with open (filename , "rb" ) as f :
221+ original_data = f .read ()
222+ tree = parser .parse (original_data )
223+ lines = original_data .decode ().split ("\n " )
224+
225+ return tree , lines , original_data
226+
227+
164228def lint_policy_file (
165- filename , original_filename = None , original_line = None , snippet = None , prefix = None
229+ filename ,
230+ original_filename = None ,
231+ original_line = None ,
232+ snippet = None ,
233+ prefix = None ,
234+ custom_promise_types = set (),
235+ strict = True ,
166236):
167237 assert original_filename is None or type (original_filename ) is str
168238 assert original_line is None or type (original_line ) is int
@@ -177,14 +247,8 @@ def lint_policy_file(
177247 assert snippet and snippet > 0
178248 assert os .path .isfile (filename )
179249 assert filename .endswith ((".cf" , ".cfengine3" , ".cf3" , ".cf.sub" ))
180- PY_LANGUAGE = Language (tscfengine .language ())
181- parser = Parser (PY_LANGUAGE )
182-
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 " )
187250
251+ tree , lines , original_data = _parse_policy_file (filename )
188252 root_node = tree .root_node
189253 if root_node .type != "source_file" :
190254 if snippet :
@@ -214,7 +278,7 @@ def lint_policy_file(
214278 else :
215279 print (f"Error: Empty policy file '{ filename } '" )
216280 errors += 1
217- errors += _walk (filename , lines , root_node )
281+ errors += _walk (filename , lines , root_node , custom_promise_types , strict )
218282 if prefix :
219283 print (prefix , end = "" )
220284 if errors == 0 :
@@ -235,8 +299,9 @@ def lint_policy_file(
235299 return errors
236300
237301
238- def lint_folder (folder ):
302+ def lint_folder (folder , strict = True ):
239303 errors = 0
304+ policy_files = []
240305 while folder .endswith (("/." , "/" )):
241306 folder = folder [0 :- 1 ]
242307 for filename in itertools .chain (
@@ -246,22 +311,45 @@ def lint_folder(folder):
246311 continue
247312 if filename .startswith ("." ) and not filename .startswith ("./" ):
248313 continue
249- errors += lint_single_file (filename )
314+
315+ if filename .endswith ((".cf" , ".cfengine3" , ".cf3" , ".cf.sub" )):
316+ policy_files .append (filename )
317+ else :
318+ errors += lint_single_file (filename )
319+
320+ custom_promise_types = set ()
321+
322+ # First pass: Gather custom types
323+ for filename in policy_files if strict else []:
324+ tree , lines , _ = _parse_policy_file (filename )
325+ if tree .root_node .type == "source_file" :
326+ custom_promise_types .update (
327+ _parse_custom_types (filename , lines , tree .root_node )
328+ )
329+
330+ # Second pass: lint all policy files
331+ for filename in policy_files :
332+ errors += lint_policy_file (
333+ filename , custom_promise_types = custom_promise_types , strict = strict
334+ )
250335 return errors
251336
252337
253- def lint_single_file (file ):
338+ def lint_single_file (file , custom_promise_types = set (), strict = True ):
254339 assert os .path .isfile (file )
255340 if file .endswith ("/cfbs.json" ):
256341 return lint_cfbs_json (file )
257342 if file .endswith (".json" ):
258343 return lint_json (file )
259344 assert file .endswith (".cf" )
260- return lint_policy_file (file )
345+ return lint_policy_file (
346+ file , custom_promise_types = custom_promise_types , strict = strict
347+ )
261348
262349
263- def lint_single_arg (arg ):
350+ def lint_single_arg (arg , strict = True ):
264351 if os .path .isdir (arg ):
265- return lint_folder (arg )
352+ return lint_folder (arg , strict )
266353 assert os .path .isfile (arg )
267- return lint_single_file (arg )
354+
355+ return lint_single_file (arg , strict )
0 commit comments