From 2225ac4446083c806db5a5f28e4bdc445d9ed011 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Tue, 21 Apr 2026 22:43:47 +0200 Subject: [PATCH] Added new dev subcommand for printing syntax trees Co-authored-by: Claude Opus 4.6 (1M context) Signed-off-by: Ole Herman Schumacher Elgesem --- CLAUDE.md | 11 +++++++++ src/cfengine_cli/dev.py | 3 +++ src/cfengine_cli/main.py | 2 ++ src/cfengine_cli/syntax_tree.py | 42 +++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 src/cfengine_cli/syntax_tree.py diff --git a/CLAUDE.md b/CLAUDE.md index 9aa03b0..c674094 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,6 +29,17 @@ When fixing issues, these are usually the files to look at: - The implementation of `cfengine format` is in `src/cfengine_cli/format.py`. - The implementation of `cfengine lint` is in `src/cfengine_cli/lint.py`. +## Syntax trees + +When working on the formatter or the linter, it is often useful to look at the syntax tree of the policy file. +There is a `dev` subcommand for this: + +```bash +uv run cfengine dev syntax-tree tests/lint/001_hello_world.cf +``` + +The command above prints the syntax tree for `tests/lint/001_hello_world.cf` to the terminal (standard output). + ## Test suites As mentioned above, the `make check` command runs all the tests. diff --git a/src/cfengine_cli/dev.py b/src/cfengine_cli/dev.py index c082785..b242815 100644 --- a/src/cfengine_cli/dev.py +++ b/src/cfengine_cli/dev.py @@ -8,6 +8,7 @@ print_release_dependency_tables, ) from cfengine_cli.docs import update_docs, check_docs +from cfengine_cli.syntax_tree import syntax_tree def generate_release_information_command( @@ -72,6 +73,8 @@ def dispatch_dev_subcommand(subcommand, args) -> int: return format_docs(args.files) if subcommand == "lint-docs": return lint_docs() + if subcommand == "syntax-tree": + return syntax_tree(args.file) if subcommand == "generate-release-information": return generate_release_information( args.omit_download, args.check_against_git, args.minimum_version diff --git a/src/cfengine_cli/main.py b/src/cfengine_cli/main.py index d805e32..39a7100 100644 --- a/src/cfengine_cli/main.py +++ b/src/cfengine_cli/main.py @@ -97,6 +97,8 @@ def _get_arg_parser(): parser.add_argument("files", nargs="*") parser = dev_subparsers.add_parser("lint-docs") parser.add_argument("files", nargs="*") + parser = dev_subparsers.add_parser("syntax-tree") + parser.add_argument("file", help="CFEngine policy file to print syntax tree for") parser = dev_subparsers.add_parser("generate-release-information") parser.add_argument( diff --git a/src/cfengine_cli/syntax_tree.py b/src/cfengine_cli/syntax_tree.py new file mode 100644 index 0000000..4017e66 --- /dev/null +++ b/src/cfengine_cli/syntax_tree.py @@ -0,0 +1,42 @@ +"""Print tree-sitter syntax tree for a .cf file.""" + +import tree_sitter_cfengine as tscfengine +from tree_sitter import Language, Parser + +_LANGUAGE = Language(tscfengine.language()) +_PARSER = Parser(_LANGUAGE) + + +def format_sexp(sexp: str) -> str: + """Format an S-expression with indentation.""" + out = [] + indent = 0 + i = 0 + while i < len(sexp): + c = sexp[i] + if c == "(": + if out and out[-1] != "\n": + out.append("\n") + out.append(" " * indent) + out.append("(") + indent += 1 + i += 1 + elif c == ")": + indent -= 1 + out.append(")") + i += 1 + elif c == " " and i + 1 < len(sexp) and sexp[i + 1] == "(": + i += 1 # skip space before '(', the '(' handler adds newline + else: + out.append(c) + i += 1 + out.append("\n") + return "".join(out) + + +def syntax_tree(path: str) -> int: + with open(path, "rb") as f: + data = f.read() + tree = _PARSER.parse(data) + print(format_sexp(str(tree.root_node)), end="") + return 0