Skip to content

Commit 905afe4

Browse files
ENT-13073: Added linter errors for unknown promisetypes
Signed-off-by: Simon Halvorsen <simon.halvorsen@northern.tech>
1 parent 7b58304 commit 905afe4

File tree

4 files changed

+121
-28
lines changed

4 files changed

+121
-28
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: 107 additions & 19 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,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+
164228
def 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)

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)