From 15e2bbba992bc827aa6ed3f1869ee6ec2e7546b3 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Fri, 6 Mar 2026 14:23:09 +0100 Subject: [PATCH 1/4] lint: Enabled specifying files/folders and refactored Signed-off-by: Ole Herman Schumacher Elgesem --- src/cfengine_cli/commands.py | 49 +++++++++++++++++++++++------ src/cfengine_cli/lint.py | 60 ++++++++++++++++++++++++++---------- src/cfengine_cli/main.py | 5 +-- 3 files changed, 86 insertions(+), 28 deletions(-) diff --git a/src/cfengine_cli/commands.py b/src/cfengine_cli/commands.py index 311ddf7..1f52abf 100644 --- a/src/cfengine_cli/commands.py +++ b/src/cfengine_cli/commands.py @@ -1,6 +1,7 @@ import sys import os import re +import itertools import json from cfengine_cli.profile import profile_cfengine, generate_callstack from cfengine_cli.dev import dispatch_dev_subcommand @@ -94,20 +95,48 @@ def format(names, line_length) -> int: return 0 -def lint() -> int: +def _lint_folder(folder): errors = 0 - for filename in find(".", extension=".json"): - if filename.startswith(("./.", "./out/")): + while folder.endswith(("/.", "/")): + folder = folder[0:-1] + for filename in itertools.chain( + find(folder, extension=".json"), find(folder, extension=".cf") + ): + if filename.startswith(("./.", "./out/", folder + "/.", folder + "/out/")): continue - if filename.endswith("/cfbs.json"): - lint_cfbs_json(filename) + if filename.startswith(".") and not filename.startswith("./"): continue - errors += lint_json(filename) + errors += _lint_single_file(filename) + return errors - for filename in find(".", extension=".cf"): - if filename.startswith(("./.", "./out/")): - continue - errors += lint_policy_file(filename) + +def _lint_single_file(file): + assert os.path.isfile(file) + if file.endswith("/cfbs.json"): + return lint_cfbs_json(file) + if file.endswith(".json"): + return lint_json(file) + assert file.endswith(".cf") + return lint_policy_file(file) + + +def _lint_single_arg(arg): + if os.path.isdir(arg): + return _lint_folder(arg) + assert os.path.isfile(arg) + _lint_single_file(arg) + return 0 + + +def lint(files) -> int: + + if not files: + return _lint_folder(".") + + errors = 0 + + for file in files: + errors += _lint_single_arg(file) if errors == 0: return 0 diff --git a/src/cfengine_cli/lint.py b/src/cfengine_cli/lint.py index 0bfedb8..8eed49c 100644 --- a/src/cfengine_cli/lint.py +++ b/src/cfengine_cli/lint.py @@ -72,27 +72,54 @@ def _text(node): return node.text.decode() +def _walk_generic(filename, lines, node, visitor): + visitor(node) + for node in node.children: + _walk_generic(filename, lines, node, visitor) + + +def _find_node_type(filename, lines, node, node_type): + matches = [] + visitor = lambda x: matches.extend([x] if x.type == node_type else []) + _walk_generic(filename, lines, node, visitor) + return matches + + +def _find_nodes(filename, lines, node): + matches = [] + visitor = lambda x: matches.append(x) + _walk_generic(filename, lines, node, visitor) + return matches + + +def _single_node_checks(filename, lines, node): + """Things which can be checked by only looking at one node, + not needing to recurse into children.""" + line = node.range.start_point[0] + 1 + column = node.range.start_point[1] + 1 + if node.type == "attribute_name" and _text(node) == "ifvarclass": + _highlight_range(node, lines) + print( + f"Deprecation: Use 'if' instead of 'ifvarclass' at {filename}:{line}:{column}" + ) + return 1 + # TODO add more rules here + return 0 + + def _walk(filename, lines, node) -> int: line = node.range.start_point[0] + 1 - column = node.range.start_point[1] - errors = 0 - # Checking for syntax errors (already detected by parser / grammar). - # These are represented in the syntax tree as special ERROR nodes. - if node.type == "ERROR": + column = node.range.start_point[1] + 1 + error_nodes = _find_node_type(filename, lines, node, "ERROR") + for node in error_nodes: _highlight_range(node, lines) print(f"Error: Syntax error at {filename}:{line}:{column}") - errors += 1 + if error_nodes: + return len(error_nodes) - if node.type == "attribute_name": - if _text(node) == "ifvarclass": - _highlight_range(node, lines) - print( - f"Error: Use 'if' instead of 'ifvarclass' (deprecated) at {filename}:{line}:{column}" - ) - errors += 1 - - for node in node.children: - errors += _walk(filename, lines, node) + errors = 0 + for node in _find_nodes(filename, lines, node): + errors += _single_node_checks(filename, lines, node) return errors @@ -100,6 +127,7 @@ def _walk(filename, lines, node) -> int: def lint_policy_file( filename, original_filename=None, original_line=None, snippet=None, prefix=None ): + print(f"Linting: {filename}") assert original_filename is None or type(original_filename) is str assert original_line is None or type(original_line) is int assert snippet is None or type(snippet) is int diff --git a/src/cfengine_cli/main.py b/src/cfengine_cli/main.py index ed62ba3..bf739f8 100644 --- a/src/cfengine_cli/main.py +++ b/src/cfengine_cli/main.py @@ -48,10 +48,11 @@ def _get_arg_parser(): fmt = subp.add_parser("format", help="Autoformat .json and .cf files") fmt.add_argument("files", nargs="*", help="Files to format") fmt.add_argument("--line-length", default=80, type=int, help="Maximum line length") - subp.add_parser( + lnt = subp.add_parser( "lint", help="Look for syntax errors and other simple mistakes", ) + lnt.add_argument("files", nargs="*", help="Files to format") subp.add_parser( "report", help="Run the agent and hub commands necessary to get new reporting data", @@ -131,7 +132,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() + return commands.lint(args.files) if args.command == "report": return commands.report() if args.command == "run": From 70772796121a8e027c3a635a0d5b6dcf114519a5 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Fri, 6 Mar 2026 14:27:09 +0100 Subject: [PATCH 2/4] lint: Added more linting rules Signed-off-by: Ole Herman Schumacher Elgesem --- src/cfengine_cli/lint.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/cfengine_cli/lint.py b/src/cfengine_cli/lint.py index 8eed49c..2e4ea24 100644 --- a/src/cfengine_cli/lint.py +++ b/src/cfengine_cli/lint.py @@ -17,6 +17,9 @@ from cfbs.validate import validate_config from cfbs.cfbs_config import CFBSConfig +DEPRECATED_PROMISE_TYPES = ["defaults", "guest_environments"] +ALLOWED_BUNDLE_TYPES = ["agent", "common", "monitor", "server", "edit_line"] + def lint_cfbs_json(filename) -> int: assert os.path.isfile(filename) @@ -103,7 +106,36 @@ def _single_node_checks(filename, lines, node): f"Deprecation: Use 'if' instead of 'ifvarclass' at {filename}:{line}:{column}" ) return 1 - # TODO add more rules here + if node.type == "promise_guard": + assert _text(node) and len(_text(node)) > 1 and _text(node)[-1] == ":" + promise_type = _text(node)[0:-1] + if promise_type in DEPRECATED_PROMISE_TYPES: + _highlight_range(node, lines) + print( + f"Deprecation: Promise type '{promise_type}' is deprecated at {filename}:{line}:{column}" + ) + return 1 + if node.type == "bundle_block_name": + if _text(node) != _text(node).lower(): + _highlight_range(node, lines) + print( + f"Convention: Bundle name should be lowercase at {filename}:{line}:{column}" + ) + return 1 + if node.type == "promise_block_name": + if _text(node) != _text(node).lower(): + _highlight_range(node, lines) + print( + f"Convention: Promise type should be lowercase at {filename}:{line}:{column}" + ) + return 1 + if node.type == "bundle_block_type": + if _text(node) not in ALLOWED_BUNDLE_TYPES: + _highlight_range(node, lines) + print( + f"Error: Bundle type must be one of ({', '.join(ALLOWED_BUNDLE_TYPES)}), not '{_text(node)}' at {filename}:{line}:{column}" + ) + return 1 return 0 From c65777fe9481d7413273687c5cf4d1d970a3d021 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Fri, 6 Mar 2026 15:20:28 +0100 Subject: [PATCH 3/4] lint: Added a printout of success / failure at the end of the command Signed-off-by: Ole Herman Schumacher Elgesem --- src/cfengine_cli/commands.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/cfengine_cli/commands.py b/src/cfengine_cli/commands.py index 1f52abf..735feaf 100644 --- a/src/cfengine_cli/commands.py +++ b/src/cfengine_cli/commands.py @@ -128,7 +128,7 @@ def _lint_single_arg(arg): return 0 -def lint(files) -> int: +def _lint(files) -> int: if not files: return _lint_folder(".") @@ -143,6 +143,15 @@ def lint(files) -> int: return 1 +def lint(files) -> int: + errors = _lint(files) + if errors == 0: + print("Success, no errors found.") + else: + print(f"Failure, {errors} errors in total.") + return errors + + def report() -> int: _require_cfhub() _require_cfagent() From b119edd217042df04d0631633cfdd478589544c5 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Fri, 6 Mar 2026 15:27:14 +0100 Subject: [PATCH 4/4] lint: Fixed bug in line / column count of printed syntax errors Signed-off-by: Ole Herman Schumacher Elgesem --- src/cfengine_cli/lint.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/cfengine_cli/lint.py b/src/cfengine_cli/lint.py index 2e4ea24..62f427c 100644 --- a/src/cfengine_cli/lint.py +++ b/src/cfengine_cli/lint.py @@ -140,15 +140,18 @@ def _single_node_checks(filename, lines, node): def _walk(filename, lines, node) -> int: - line = node.range.start_point[0] + 1 - column = node.range.start_point[1] + 1 error_nodes = _find_node_type(filename, lines, node, "ERROR") - for node in error_nodes: - _highlight_range(node, lines) - print(f"Error: Syntax error at {filename}:{line}:{column}") if error_nodes: + for node in error_nodes: + line = node.range.start_point[0] + 1 + column = node.range.start_point[1] + 1 + _highlight_range(node, lines) + print(f"Error: Syntax error at {filename}:{line}:{column}") return len(error_nodes) + line = node.range.start_point[0] + 1 + column = node.range.start_point[1] + 1 + errors = 0 for node in _find_nodes(filename, lines, node): errors += _single_node_checks(filename, lines, node)