Skip to content

Commit 01fdec6

Browse files
authored
Merge pull request #30 from SimonThalvorsen/ENT-13073
ENT-13073: Added linter errors for unknown promise types
2 parents 7b58304 + 32a7c26 commit 01fdec6

File tree

4 files changed

+126
-27
lines changed

4 files changed

+126
-27
lines changed

src/cfengine_cli/commands.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import json
55
from cfengine_cli.profile import profile_cfengine, generate_callstack
66
from cfengine_cli.dev import dispatch_dev_subcommand
7-
from cfengine_cli.lint import lint_single_arg, lint_folder
7+
from cfengine_cli.lint import lint_folder, lint_single_arg
88
from cfengine_cli.shell import user_command
99
from cfengine_cli.paths import bin
1010
from cfengine_cli.version import cfengine_cli_version_string
@@ -94,21 +94,20 @@ def format(names, line_length) -> int:
9494
return 0
9595

9696

97-
def _lint(files) -> int:
98-
97+
def _lint(files, strict) -> int:
9998
if not files:
100-
return lint_folder(".")
99+
return lint_folder(".", strict)
101100

102101
errors = 0
103102

104103
for file in files:
105-
errors += lint_single_arg(file)
104+
errors += lint_single_arg(file, strict)
106105

107106
return errors
108107

109108

110-
def lint(files) -> int:
111-
errors = _lint(files)
109+
def lint(files, strict) -> int:
110+
errors = _lint(files, strict)
112111
if errors == 0:
113112
print("Success, no errors found.")
114113
else:

src/cfengine_cli/docs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ def check_docs() -> int:
409409
410410
Run by the command:
411411
cfengine dev lint-docs"""
412-
r = lint_folder(".")
412+
r = lint_folder(".", strict=False)
413413
if r != 0:
414414
return r
415415
_process_markdown_code_blocks(

src/cfengine_cli/lint.py

Lines changed: 112 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,38 @@
2121

2222
DEPRECATED_PROMISE_TYPES = ["defaults", "guest_environments"]
2323
ALLOWED_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

2658
def 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+
164231
def 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)

src/cfengine_cli/main.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ def _get_arg_parser():
5252
"lint",
5353
help="Look for syntax errors and other simple mistakes",
5454
)
55+
lnt.add_argument(
56+
"--strict",
57+
type=str,
58+
default="yes",
59+
help="Strict mode. Default=yes, checks for undefined promisetypes",
60+
)
5561
lnt.add_argument("files", nargs="*", help="Files to format")
5662
subp.add_parser(
5763
"report",
@@ -132,7 +138,7 @@ def run_command_with_args(args) -> int:
132138
if args.command == "format":
133139
return commands.format(args.files, args.line_length)
134140
if args.command == "lint":
135-
return commands.lint(args.files)
141+
return commands.lint(args.files, (args.strict.lower() in ("y", "ye", "yes")))
136142
if args.command == "report":
137143
return commands.report()
138144
if args.command == "run":

0 commit comments

Comments
 (0)