From c87c3c98aa5b18beb60db5e1af0ed238c3586f14 Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Mon, 23 Mar 2026 11:20:58 +0100 Subject: [PATCH] ENT-13073: Added linter errors for unknown promisetypes Signed-off-by: Simon Halvorsen --- src/cfengine_cli/commands.py | 9 +-- src/cfengine_cli/lint.py | 104 ++++++++++++++++++++++++++++++++--- src/cfengine_cli/main.py | 8 ++- 3 files changed, 108 insertions(+), 13 deletions(-) diff --git a/src/cfengine_cli/commands.py b/src/cfengine_cli/commands.py index 1f9f3f5..2ca798e 100644 --- a/src/cfengine_cli/commands.py +++ b/src/cfengine_cli/commands.py @@ -4,7 +4,7 @@ import json from cfengine_cli.profile import profile_cfengine, generate_callstack from cfengine_cli.dev import dispatch_dev_subcommand -from cfengine_cli.lint import lint_single_arg, lint_folder +from cfengine_cli.lint import lint_folder, lint_single_arg, set_strict from cfengine_cli.shell import user_command from cfengine_cli.paths import bin from cfengine_cli.version import cfengine_cli_version_string @@ -94,7 +94,8 @@ def format(names, line_length) -> int: return 0 -def _lint(files) -> int: +def _lint(files, strict) -> int: + set_strict(strict) if not files: return lint_folder(".") @@ -107,8 +108,8 @@ def _lint(files) -> int: return errors -def lint(files) -> int: - errors = _lint(files) +def lint(files, strict) -> int: + errors = _lint(files, strict) if errors == 0: print("Success, no errors found.") else: diff --git a/src/cfengine_cli/lint.py b/src/cfengine_cli/lint.py index 9349f8f..adeb518 100644 --- a/src/cfengine_cli/lint.py +++ b/src/cfengine_cli/lint.py @@ -21,6 +21,44 @@ DEPRECATED_PROMISE_TYPES = ["defaults", "guest_environments"] ALLOWED_BUNDLE_TYPES = ["agent", "common", "monitor", "server", "edit_line", "edit_xml"] +BUILTIN_PROMISE_TYPES = { + "access", + "build_xpath", + "classes", + "commands", + "databases", + "defaults", + "delete_attribute", + "delete_lines", + "delete_text", + "delete_tree", + "field_edits", + "files", + "guest_environments", + "insert_lines", + "insert_text", + "insert_tree", + "measurements", + "meta", + "methods", + "packages", + "processes", + "replace_patterns", + "reports", + "roles", + "services", + "set_attribute", + "set_text", + "storage", + "users", + "vars", +} + +custom_promise_types = set() + +# Globally set as there might be more future cases where we want to +# classify rules that only apply in strict cases +strict = True def lint_cfbs_json(filename) -> int: @@ -117,6 +155,17 @@ def _single_node_checks(filename, lines, node): f"Deprecation: Promise type '{promise_type}' is deprecated at {filename}:{line}:{column}" ) return 1 + if ( + (promise_type not in BUILTIN_PROMISE_TYPES) + and (promise_type not in custom_promise_types) + and strict + ): + _highlight_range(node, lines) + print( + f"Error: Undefined promise type '{promise_type}' at {filename}:{line}:{column}" + ) + return 1 + if node.type == "bundle_block_name": if _text(node) != _text(node).lower(): _highlight_range(node, lines) @@ -138,6 +187,7 @@ def _single_node_checks(filename, lines, node): f"Error: Bundle type must be one of ({', '.join(ALLOWED_BUNDLE_TYPES)}), not '{_text(node)}' at {filename}:{line}:{column}" ) return 1 + return 0 @@ -161,6 +211,26 @@ def _walk(filename, lines, node) -> int: return errors +def _parse_custom(filename, lines, root_node): + promise_blocks = _find_node_type(filename, lines, root_node, "promise_block_name") + for node in promise_blocks: + custom_promise_types.add(_text(node)) + return 0 + + +def _parse_policy_file(filename): + assert os.path.isfile(filename) + PY_LANGUAGE = Language(tscfengine.language()) + parser = Parser(PY_LANGUAGE) + + with open(filename, "rb") as f: + original_data = f.read() + tree = parser.parse(original_data) + lines = original_data.decode().split("\n") + + return tree, lines, original_data + + def lint_policy_file( filename, original_filename=None, original_line=None, snippet=None, prefix=None ): @@ -177,14 +247,8 @@ def lint_policy_file( assert snippet and snippet > 0 assert os.path.isfile(filename) assert filename.endswith((".cf", ".cfengine3", ".cf3", ".cf.sub")) - PY_LANGUAGE = Language(tscfengine.language()) - parser = Parser(PY_LANGUAGE) - - with open(filename, "rb") as f: - original_data = f.read() - tree = parser.parse(original_data) - lines = original_data.decode().split("\n") + tree, lines, original_data = _parse_policy_file(filename) root_node = tree.root_node if root_node.type != "source_file": if snippet: @@ -237,6 +301,7 @@ def lint_policy_file( def lint_folder(folder): errors = 0 + policy_files = [] while folder.endswith(("/.", "/")): folder = folder[0:-1] for filename in itertools.chain( @@ -246,7 +311,21 @@ def lint_folder(folder): continue if filename.startswith(".") and not filename.startswith("./"): continue - errors += lint_single_file(filename) + + if filename.endswith((".cf", ".cfengine3", ".cf3", ".cf.sub")): + policy_files.append(filename) + else: + errors += lint_single_file(filename) + + # First pass: Gather custom types/bundles/+++ + for filename in policy_files: + tree, lines, _ = _parse_policy_file(filename) + if tree.root_node.type == "source_file": + _parse_custom(filename, lines, tree.root_node) + + # Second pass: lint all policy files + for filename in policy_files: + errors += lint_policy_file(filename) return errors @@ -265,3 +344,12 @@ def lint_single_arg(arg): return lint_folder(arg) assert os.path.isfile(arg) return lint_single_file(arg) + + +def set_strict(is_strict): + """ + Used to set the global variable 'strict' inside 'lint.py'. + Used for ignoring/handling specific linting rules. + """ + global strict + strict = is_strict diff --git a/src/cfengine_cli/main.py b/src/cfengine_cli/main.py index bf739f8..2cfd3af 100644 --- a/src/cfengine_cli/main.py +++ b/src/cfengine_cli/main.py @@ -52,6 +52,12 @@ def _get_arg_parser(): "lint", help="Look for syntax errors and other simple mistakes", ) + lnt.add_argument( + "--strict", + type=str, + default="yes", + help="Strict mode. Default=yes, checks for custom promisetypes", + ) lnt.add_argument("files", nargs="*", help="Files to format") subp.add_parser( "report", @@ -132,7 +138,7 @@ def run_command_with_args(args) -> int: if args.command == "format": return commands.format(args.files, args.line_length) if args.command == "lint": - return commands.lint(args.files) + return commands.lint(args.files, args.strict.lower() in ("y", "ye", "yes")) if args.command == "report": return commands.report() if args.command == "run":