From 6bedac1ad420050e6e30aea957f759e316da759c Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Mon, 23 Mar 2026 11:20:58 +0100 Subject: [PATCH 1/2] ENT-13073: Added linter errors for unknown promisetypes Signed-off-by: Simon Halvorsen --- src/cfengine_cli/commands.py | 9 ++-- src/cfengine_cli/lint.py | 102 ++++++++++++++++++++++++++++++++--- src/cfengine_cli/main.py | 8 ++- 3 files changed, 106 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..0808db9 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,15 @@ def _single_node_checks(filename, lines, node): f"Deprecation: Promise type '{promise_type}' is deprecated at {filename}:{line}:{column}" ) return 1 + if strict and ( + (promise_type not in BUILTIN_PROMISE_TYPES.union(custom_promise_types)) + ): + _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 +185,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 +209,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 +245,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 +299,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 +309,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 +342,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": From c5e8daf70405023b33f6733a1bf53731fd62d3fa Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Wed, 25 Mar 2026 14:40:53 +0100 Subject: [PATCH 2/2] ENT-13074: Added linter errors for unknown calls (bundles/bodies/functions) Signed-off-by: Simon Halvorsen --- src/cfengine_cli/builtin_types.py | 236 ++++++++++++++++++++++++++++++ src/cfengine_cli/lint.py | 61 +++----- 2 files changed, 259 insertions(+), 38 deletions(-) create mode 100644 src/cfengine_cli/builtin_types.py diff --git a/src/cfengine_cli/builtin_types.py b/src/cfengine_cli/builtin_types.py new file mode 100644 index 0000000..5737729 --- /dev/null +++ b/src/cfengine_cli/builtin_types.py @@ -0,0 +1,236 @@ +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", +} +BUILTIN_FUNCTIONS = { + "accessedbefore", + "accumulated", + "ago", + "and", + "basename", + "bundlesmatching", + "bundlestate", + "callstack_callers", + "callstack_promisers", + "canonify", + "canonifyuniquely", + "cf_version_after", + "cf_version_at", + "cf_version_before", + "cf_version_between", + "cf_version_maximum", + "cf_version_minimum", + "changedbefore", + "classesmatching", + "classfiltercsv", + "classfilterdata", + "classify", + "classmatch", + "concat", + "countclassesmatching", + "countlinesmatching", + "data_expand", + "data_readstringarray", + "data_readstringarrayidx", + "data_regextract", + "data_sysctlvalues", + "datastate", + "difference", + "dirname", + "diskfree", + "escape", + "eval", + "every", + "execresult", + "execresult_as_data", + "expandrange", + "file_hash", + "fileexists", + "filesexist", + "filesize", + "filestat", + "filter", + "findfiles", + "findfiles_up", + "findlocalgroups", + "findlocalusers", + "findprocesses", + "format", + "getacls", + "getbundlemetatags", + "getclassmetatags", + "getenv", + "getfields", + "getgid", + "getgroupinfo", + "getgroups", + "getindices", + "getuid", + "getuserinfo", + "getusers", + "getvalues", + "getvariablemetatags", + "grep", + "groupexists", + "hash", + "hash_to_int", + "hashmatch", + "host2ip", + "hostinnetgroup", + "hostrange", + "hostsseen", + "hostswithclass", + "hostswithgroup", + "hubknowledge", + "ifelse", + "int", + "intersection", + "ip2host", + "iprange", + "irange", + "is_type", + "isconnectable", + "isdir", + "isexecutable", + "isgreaterthan", + "isipinsubnet", + "islessthan", + "islink", + "isnewerthan", + "isnewerthantime", + "isplain", + "isreadable", + "isvariable", + "join", + "lastnode", + "laterthan", + "ldaparray", + "ldaplist", + "ldapvalue", + "length", + "lsdir", + "makerule", + "maparray", + "mapdata", + "maplist", + "max", + "mean", + "mergedata", + "min", + "network_connections", + "none", + "not", + "now", + "nth", + "on", + "or", + "packagesmatching", + "packageupdatesmatching", + "parseintarray", + "parsejson", + "parserealarray", + "parsestringarray", + "parsestringarrayidx", + "parseyaml", + "peerleader", + "peerleaders", + "peers", + "processexists", + "product", + "randomint", + "read_module_protocol", + "readcsv", + "readdata", + "readenvfile", + "readfile", + "readintarray", + "readintlist", + "readjson", + "readrealarray", + "readreallist", + "readstringarray", + "readstringarrayidx", + "readstringlist", + "readtcp", + "readyaml", + "regarray", + "regcmp", + "regex_replace", + "regextract", + "registryvalue", + "regldap", + "regline", + "reglist", + "remoteclassesmatching", + "remotescalar", + "returnszero", + "reverse", + "rrange", + "search_up", + "selectservers", + "shuffle", + "some", + "sort", + "splayclass", + "splitstring", + "storejson", + "strcmp", + "strftime", + "string", + "string_downcase", + "string_head", + "string_length", + "string_mustache", + "string_replace", + "string_reverse", + "string_split", + "string_tail", + "string_trim", + "string_upcase", + "sublist", + "sum", + "sysctlvalue", + "translatepath", + "type", + "unique", + "url_get", + "usemodule", + "userexists", + "useringroup", + "validdata", + "validjson", + "variablesmatching", + "variablesmatching_as_data", + "variance", + "version_compare", +} diff --git a/src/cfengine_cli/lint.py b/src/cfengine_cli/lint.py index 0808db9..f9f341b 100644 --- a/src/cfengine_cli/lint.py +++ b/src/cfengine_cli/lint.py @@ -18,43 +18,15 @@ from cfbs.validate import validate_config from cfbs.cfbs_config import CFBSConfig from cfbs.utils import find - -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", -} +from cfengine_cli.builtin_types import ( + DEPRECATED_PROMISE_TYPES, + ALLOWED_BUNDLE_TYPES, + BUILTIN_PROMISE_TYPES, + BUILTIN_FUNCTIONS, +) custom_promise_types = set() +custom_callables = set() # Globally set as there might be more future cases where we want to # classify rules that only apply in strict cases @@ -185,7 +157,15 @@ 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 - + if node.type == "calling_identifier": + if strict and ( + _text(node) not in BUILTIN_FUNCTIONS.union(custom_callables) + ): + _highlight_range(node, lines) + print( + f"Error: Call to unknown function / bundle / body '{_text(node)}' at at {filename}:{line}:{column}" + ) + return 1 return 0 @@ -211,8 +191,13 @@ def _walk(filename, lines, node) -> int: 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)) + custom_promise_types.update(_text(x) for x in promise_blocks) + + bundle_blocks = _find_node_type(filename, lines, root_node, "bundle_block_name") + custom_callables.update(_text(x) for x in bundle_blocks) + + body_blocks = _find_node_type(filename, lines, root_node, "body_block_name") + custom_callables.update(_text(x) for x in body_blocks) return 0