Skip to content

Commit a87d99e

Browse files
authored
Merge pull request #94 from olehermanse/cpt
cfengine lint: Started recording and using all definitions of bodies and bundles in case there are multiple
2 parents 54fc795 + acfce43 commit a87d99e

4 files changed

Lines changed: 107 additions & 41 deletions

File tree

src/cfengine_cli/lint.py

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

294-
def add_bundle(self, name: str) -> None:
295-
"""This is called during discovery wherever a bundle is defined.
296-
297-
For example:
298-
bundle agent my_bundle {}
294+
def _add_definition(self, name: str, node: Node, definitions: dict) -> None:
295+
"""Add a definition (bundle or body) to the given dictionary.
299296
297+
The value for each qualified name is a list of definitions, since
298+
the same name can appear multiple times (e.g. inside macro if/else
299+
branches). Each definition records the file, line, and parameter
300+
list.
300301
"""
301302
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:
312-
"""This is called during discovery wherever a body is defined.
303+
assert self.policy_file
304+
n = node.next_named_sibling
305+
if n and n.type == "parameter_list":
306+
_, *args, _ = n.children
307+
parameters = list(filter(",".__ne__, iter(_text(x) for x in args)))
308+
else:
309+
parameters = []
310+
definition = {
311+
"filename": self.policy_file.filename,
312+
"line": node.range.start_point[0] + 1,
313+
"parameters": parameters,
314+
}
315+
if name not in definitions:
316+
definitions[name] = []
317+
definitions[name].append(definition)
313318

314-
For example:
315-
body contain my_body {}
319+
def add_bundle(self, name: str, node: Node) -> None:
320+
"""This is called during discovery wherever a bundle is defined."""
321+
self._add_definition(name, node, self.bundles)
316322

317-
Control bundles are a special case, so would not be called for:
318-
body file control {}
323+
def add_body(self, name: str, node: Node) -> None:
324+
"""This is called during discovery wherever a body is defined.
325+
326+
Control bodies are a special case and should not be passed here.
319327
"""
320-
name = _qualify(name, self.namespace)
321-
self.bodies[name] = {"is_defined": True}
328+
self._add_definition(name, node, self.bodies)
322329

323330
def add_promise_type(self, name: str) -> None:
324331
"""This is called during discovery wherever a custom promise type is
@@ -479,28 +486,19 @@ def _discover_node(node: Node, state: State) -> int:
479486
name = _text(node)
480487
if name == "control":
481488
return 0 # No need to define control blocks
482-
state.add_body(name)
483-
qualified_name = _qualify(name, state.namespace)
484-
if (n := node.next_named_sibling) and n.type == "parameter_list":
485-
_, *args, _ = n.children
486-
args = list(filter(",".__ne__, iter(_text(x) for x in args)))
487-
state.bodies[qualified_name].update({"parameters": args})
489+
state.add_body(name, node)
488490
return 0
489491

490492
# Define bundles:
491493
if node.type == "bundle_block_name":
492494
name = _text(node)
493-
qualified_name = _qualify(name, state.namespace)
494-
state.add_bundle(name)
495-
if (n := node.next_named_sibling) and n.type == "parameter_list":
496-
_, *args, _ = n.children
497-
args = list(filter(",".__ne__, iter(_text(x) for x in args)))
498-
state.bundles[qualified_name].update({"parameters": args})
495+
state.add_bundle(name, node)
499496
return 0
500497

501498
# Define custom promise types:
502499
if node.type == "promise_block_name":
503-
state.add_promise_type(_text(node))
500+
name = _text(node)
501+
state.add_promise_type(name)
504502
return 0
505503

506504
return 0
@@ -727,19 +725,25 @@ def _lint_node(
727725

728726
qualified_name = _qualify(call, state.namespace)
729727
if qualified_name in state.bundles:
730-
max_args = len(state.bundles[qualified_name].get("parameters", []))
731-
if max_args != len(args):
728+
definitions = state.bundles[qualified_name]
729+
valid_counts = {len(d.get("parameters", [])) for d in definitions}
730+
if len(args) not in valid_counts:
732731
_highlight_range(node, lines)
732+
counts = sorted(valid_counts)
733+
expected = " or ".join(str(c) for c in counts)
733734
print(
734-
f"Error: Expected {max_args} arguments, received {len(args)} for bundle '{call}' {location}"
735+
f"Error: Expected {expected} arguments, received {len(args)} for bundle '{call}' {location}"
735736
)
736737
return 1
737738
if qualified_name in state.bodies:
738-
max_args = len(state.bodies[qualified_name].get("parameters", []))
739-
if max_args != len(args):
739+
definitions = state.bodies[qualified_name]
740+
valid_counts = {len(d.get("parameters", [])) for d in definitions}
741+
if len(args) not in valid_counts:
740742
_highlight_range(node, lines)
743+
counts = sorted(valid_counts)
744+
expected = " or ".join(str(c) for c in counts)
741745
print(
742-
f"Error: Expected {max_args} arguments, received {len(args)} for body '{call}' {location}"
746+
f"Error: Expected {expected} arguments, received {len(args)} for body '{call}' {location}"
743747
)
744748
return 1
745749

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)