Skip to content

Commit 2e0cfc2

Browse files
olehermanseclaude
andcommitted
cfengine lint: Started recording and using all definitions of bodies and bundles in case there are multiple
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Ole Herman Schumacher Elgesem <ole@northern.tech>
1 parent 54fc795 commit 2e0cfc2

4 files changed

Lines changed: 107 additions & 22 deletions

File tree

src/cfengine_cli/lint.py

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -291,34 +291,51 @@ def end_file(self) -> None:
291291
self.walking = False
292292
self.policy_file = None
293293

294-
def add_bundle(self, name: str) -> None:
294+
def add_bundle(self, name: str, node: Node) -> None:
295295
"""This is called during discovery wherever a bundle is defined.
296296
297297
For example:
298298
bundle agent my_bundle {}
299299
300+
We create a dictionary where the key is the qualified name,
301+
and the value is a list of all the definionts. This is because the
302+
same qualified name can appear multiple times (e.g. inside macro
303+
if/else branches). Each definition records the file, line number,
304+
and parameter list.
300305
"""
301306
name = _qualify(name, self.namespace)
302-
# TODO: In the future we will record more information than True, like:
303-
# - Can be a list / dict of all places a bundle with that
304-
# qualified name is defined in cases there are duplicates.
305-
# - Can record the location of each definition
306-
# - Can record the parameters / signature
307-
# - Can record whether the bundle is inside a macro
308-
# - Can have a list of classes and vars defined inside
309-
self.bundles[name] = {"is_defined": True}
310-
311-
def add_body(self, name: str) -> None:
307+
assert self.policy_file
308+
line = node.range.start_point[0] + 1
309+
definition = {
310+
"filename": self.policy_file.filename,
311+
"line": line,
312+
}
313+
if name not in self.bundles:
314+
self.bundles[name] = []
315+
self.bundles[name].append(definition)
316+
317+
def add_body(self, name: str, node: Node) -> None:
312318
"""This is called during discovery wherever a body is defined.
313319
314320
For example:
315321
body contain my_body {}
316322
317323
Control bundles are a special case, so would not be called for:
318324
body file control {}
325+
326+
Similar to add_bundles, we record a list of definitions, since
327+
each body can be defined multiple times (inside different macros).
319328
"""
320329
name = _qualify(name, self.namespace)
321-
self.bodies[name] = {"is_defined": True}
330+
assert self.policy_file
331+
line = node.range.start_point[0] + 1
332+
definition = {
333+
"filename": self.policy_file.filename,
334+
"line": line,
335+
}
336+
if name not in self.bodies:
337+
self.bodies[name] = []
338+
self.bodies[name].append(definition)
322339

323340
def add_promise_type(self, name: str) -> None:
324341
"""This is called during discovery wherever a custom promise type is
@@ -479,23 +496,23 @@ def _discover_node(node: Node, state: State) -> int:
479496
name = _text(node)
480497
if name == "control":
481498
return 0 # No need to define control blocks
482-
state.add_body(name)
499+
state.add_body(name, node)
483500
qualified_name = _qualify(name, state.namespace)
484501
if (n := node.next_named_sibling) and n.type == "parameter_list":
485502
_, *args, _ = n.children
486503
args = list(filter(",".__ne__, iter(_text(x) for x in args)))
487-
state.bodies[qualified_name].update({"parameters": args})
504+
state.bodies[qualified_name][-1]["parameters"] = args
488505
return 0
489506

490507
# Define bundles:
491508
if node.type == "bundle_block_name":
492509
name = _text(node)
493510
qualified_name = _qualify(name, state.namespace)
494-
state.add_bundle(name)
511+
state.add_bundle(name, node)
495512
if (n := node.next_named_sibling) and n.type == "parameter_list":
496513
_, *args, _ = n.children
497514
args = list(filter(",".__ne__, iter(_text(x) for x in args)))
498-
state.bundles[qualified_name].update({"parameters": args})
515+
state.bundles[qualified_name][-1]["parameters"] = args
499516
return 0
500517

501518
# Define custom promise types:
@@ -727,19 +744,25 @@ def _lint_node(
727744

728745
qualified_name = _qualify(call, state.namespace)
729746
if qualified_name in state.bundles:
730-
max_args = len(state.bundles[qualified_name].get("parameters", []))
731-
if max_args != len(args):
747+
definitions = state.bundles[qualified_name]
748+
valid_counts = {len(d.get("parameters", [])) for d in definitions}
749+
if len(args) not in valid_counts:
732750
_highlight_range(node, lines)
751+
counts = sorted(valid_counts)
752+
expected = " or ".join(str(c) for c in counts)
733753
print(
734-
f"Error: Expected {max_args} arguments, received {len(args)} for bundle '{call}' {location}"
754+
f"Error: Expected {expected} arguments, received {len(args)} for bundle '{call}' {location}"
735755
)
736756
return 1
737757
if qualified_name in state.bodies:
738-
max_args = len(state.bodies[qualified_name].get("parameters", []))
739-
if max_args != len(args):
758+
definitions = state.bodies[qualified_name]
759+
valid_counts = {len(d.get("parameters", [])) for d in definitions}
760+
if len(args) not in valid_counts:
740761
_highlight_range(node, lines)
762+
counts = sorted(valid_counts)
763+
expected = " or ".join(str(c) for c in counts)
741764
print(
742-
f"Error: Expected {max_args} arguments, received {len(args)} for body '{call}' {location}"
765+
f"Error: Expected {expected} arguments, received {len(args)} for body '{call}' {location}"
743766
)
744767
return 1
745768

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
@if minimum_version(3.24)
2+
bundle agent test(a, b)
3+
{
4+
reports:
5+
"$(a) and $(b)";
6+
}
7+
@else
8+
bundle agent test(a)
9+
{
10+
reports:
11+
"$(a)";
12+
}
13+
@endif
14+
15+
bundle agent main
16+
{
17+
methods:
18+
@if minimum_version(3.24)
19+
"test1"
20+
usebundle => test("hello", "world");
21+
@else
22+
"test2"
23+
usebundle => test("hello");
24+
@endif
25+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
"test1"
3+
usebundle => test();
4+
^----^
5+
Error: Expected 1 or 2 arguments, received 0 for bundle 'test' at tests/lint/016_macro_multi_def_bundle.x.cf:20:20
6+
7+
"test2"
8+
usebundle => test("hello", "world", "!");
9+
^-------------------------^
10+
Error: Expected 1 or 2 arguments, received 3 for bundle 'test' at tests/lint/016_macro_multi_def_bundle.x.cf:23:20
11+
FAIL: tests/lint/016_macro_multi_def_bundle.x.cf (2 errors)
12+
Failure, 2 errors in total.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
@if minimum_version(3.24)
2+
bundle agent test(a, b)
3+
{
4+
reports:
5+
"$(a) and $(b)";
6+
}
7+
@else
8+
bundle agent test(a)
9+
{
10+
reports:
11+
"$(a)";
12+
}
13+
@endif
14+
15+
bundle agent main
16+
{
17+
methods:
18+
@if minimum_version(3.24)
19+
"test1"
20+
usebundle => test();
21+
@else
22+
"test2"
23+
usebundle => test("hello", "world", "!");
24+
@endif
25+
}

0 commit comments

Comments
 (0)