Skip to content

Commit 46b9a07

Browse files
authored
Merge pull request #86 from SimonThalvorsen/ENT-13880
ENT-13880: Added error-checks for half-promises / attrs without promiser outside of macros
2 parents a87d99e + bfed5d1 commit 46b9a07

4 files changed

Lines changed: 105 additions & 2 deletions

File tree

src/cfengine_cli/lint.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@
2727
are a bit awkward. Could make iteration nicer.
2828
"""
2929

30+
from copy import deepcopy
3031
from enum import Enum
3132
import os
3233
import json
3334
from typing import Callable, Iterable
3435
import tree_sitter_cfengine as tscfengine
35-
from dataclasses import dataclass
36+
from dataclasses import dataclass, field
3637
from tree_sitter import Language, Node, Parser, Tree
3738
from cfbs.validate import validate_config
3839
from cfbs.cfbs_config import CFBSConfig
@@ -179,6 +180,13 @@ def visit(x):
179180

180181
_walk_callback(tree.root_node, visit)
181182

183+
def __deepcopy__(self, _):
184+
"""Overrides deepcopy for state-snapshotting.
185+
treesitter-tree is not pickleable, and PolicyFile
186+
should not change across state snapshots
187+
"""
188+
return self
189+
182190

183191
@dataclass
184192
class State:
@@ -199,6 +207,8 @@ class State:
199207
promise_type: str | None = None # "vars" | "files" | "classes" | ... | None
200208
attribute_name: str | None = None # "if" | "string" | "slist" | ... | None
201209
namespace: str = DEFAULT_NAMESPACE # "ns" | "default" | ... |
210+
macro: str | None = None # "minimum_version()", "else", "between_versions()"
211+
old_state: dict = field(default_factory=dict)
202212
mode: Mode = Mode.NONE
203213
walking: bool = False
204214
strict: bool = True
@@ -307,11 +317,14 @@ def _add_definition(self, name: str, node: Node, definitions: dict) -> None:
307317
parameters = list(filter(",".__ne__, iter(_text(x) for x in args)))
308318
else:
309319
parameters = []
320+
310321
definition = {
311322
"filename": self.policy_file.filename,
312323
"line": node.range.start_point[0] + 1,
313324
"parameters": parameters,
314325
}
326+
if self.macro:
327+
definition["macro"] = self.macro
315328
if name not in definitions:
316329
definitions[name] = []
317330
definitions[name].append(definition)
@@ -429,6 +442,22 @@ def navigate(self, node) -> None:
429442
self.promise_type = None
430443
return
431444

445+
if node.type == "macro":
446+
macro_type = _text(node)
447+
if macro_type.startswith("@if"):
448+
self.old_state = deepcopy(self.__dict__)
449+
self.macro = macro_type.split(" ")[-1]
450+
elif macro_type.startswith("@else"):
451+
self.macro = "else"
452+
self.old_state = deepcopy(self.__dict__)
453+
self.__dict__.update(self.old_state)
454+
elif macro_type.startswith("@endif"):
455+
self.macro = None
456+
self.__dict__.update(self.old_state)
457+
# NOTE: this or that? maybe a dict of states based on the macro-type?
458+
self.old_state = {}
459+
return
460+
432461

433462
def _check_syntax(policy_file: PolicyFile, state: State) -> int:
434463
"""Iterate over a syntax tree and print errors if it is empty or has syntax
@@ -746,7 +775,23 @@ def _lint_node(
746775
f"Error: Expected {expected} arguments, received {len(args)} for body '{call}' {location}"
747776
)
748777
return 1
749-
778+
if node.type == "half_promise":
779+
prev_sib = node.prev_named_sibling
780+
while prev_sib and prev_sib.type == "comment":
781+
prev_sib = prev_sib.prev_named_sibling
782+
prev_type = prev_sib.type if prev_sib else None
783+
if not state.macro:
784+
_highlight_range(node, lines)
785+
print(
786+
f"Error: Found promise attribute with no parent-promiser outside of a macro {location}"
787+
)
788+
return 1
789+
elif prev_type != "macro":
790+
_highlight_range(node, lines)
791+
print(
792+
f"Error: Multiple promise attributes with ending semicolon found inside macro '{state.macro}' {location}"
793+
)
794+
return 1
750795
return 0
751796

752797

tests/lint/017_half_promises.cf

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
bundle agent main
2+
{
3+
vars:
4+
"string" string => $(bar);
5+
6+
"string"
7+
@if minimum_version(3.18)
8+
string => $($(baz));
9+
@else
10+
string => $($(baz));
11+
@endif
12+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
string => $(bar);
3+
string => $(bar);
4+
^---------------^
5+
Error: Found promise attribute with no parent-promiser outside of a macro at tests/lint/017_half_promises.x.cf:6:7
6+
7+
string => $($(baz));
8+
string => $($(baz));
9+
^------------------^
10+
Error: Multiple promise attributes with ending semicolon found inside macro 'minimum_version(3.18)' at tests/lint/017_half_promises.x.cf:9:7
11+
12+
# comment
13+
string => $($(baz));
14+
^------------------^
15+
Error: Multiple promise attributes with ending semicolon found inside macro 'else' at tests/lint/017_half_promises.x.cf:26:7
16+
FAIL: tests/lint/017_half_promises.x.cf (3 errors)
17+
Failure, 3 errors in total.

tests/lint/017_half_promises.x.cf

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
bundle agent main
2+
{
3+
vars:
4+
"string"
5+
string => $(bar);
6+
string => $(bar);
7+
@if minimum_version(3.18)
8+
string => $($(baz));
9+
string => $($(baz));
10+
@else
11+
string => $($(baz));
12+
@endif
13+
14+
"string"
15+
# comment
16+
# comment
17+
# comment
18+
@if minimum_version(3.20)
19+
string => $($(baz));
20+
21+
# comment
22+
@else
23+
string => $($(baz));
24+
25+
# comment
26+
string => $($(baz));
27+
# comment
28+
@endif
29+
}

0 commit comments

Comments
 (0)