Skip to content

Commit 580f840

Browse files
authored
Merge pull request #23 from olehermanse/improvements
Expanded cfengine lint with more linting rules and the ability to specify files / folders
2 parents 42cfc51 + b119edd commit 580f840

File tree

3 files changed

+129
-27
lines changed

3 files changed

+129
-27
lines changed

src/cfengine_cli/commands.py

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import sys
22
import os
33
import re
4+
import itertools
45
import json
56
from cfengine_cli.profile import profile_cfengine, generate_callstack
67
from cfengine_cli.dev import dispatch_dev_subcommand
@@ -94,26 +95,63 @@ def format(names, line_length) -> int:
9495
return 0
9596

9697

97-
def lint() -> int:
98+
def _lint_folder(folder):
9899
errors = 0
99-
for filename in find(".", extension=".json"):
100-
if filename.startswith(("./.", "./out/")):
100+
while folder.endswith(("/.", "/")):
101+
folder = folder[0:-1]
102+
for filename in itertools.chain(
103+
find(folder, extension=".json"), find(folder, extension=".cf")
104+
):
105+
if filename.startswith(("./.", "./out/", folder + "/.", folder + "/out/")):
101106
continue
102-
if filename.endswith("/cfbs.json"):
103-
lint_cfbs_json(filename)
107+
if filename.startswith(".") and not filename.startswith("./"):
104108
continue
105-
errors += lint_json(filename)
109+
errors += _lint_single_file(filename)
110+
return errors
106111

107-
for filename in find(".", extension=".cf"):
108-
if filename.startswith(("./.", "./out/")):
109-
continue
110-
errors += lint_policy_file(filename)
112+
113+
def _lint_single_file(file):
114+
assert os.path.isfile(file)
115+
if file.endswith("/cfbs.json"):
116+
return lint_cfbs_json(file)
117+
if file.endswith(".json"):
118+
return lint_json(file)
119+
assert file.endswith(".cf")
120+
return lint_policy_file(file)
121+
122+
123+
def _lint_single_arg(arg):
124+
if os.path.isdir(arg):
125+
return _lint_folder(arg)
126+
assert os.path.isfile(arg)
127+
_lint_single_file(arg)
128+
return 0
129+
130+
131+
def _lint(files) -> int:
132+
133+
if not files:
134+
return _lint_folder(".")
135+
136+
errors = 0
137+
138+
for file in files:
139+
errors += _lint_single_arg(file)
111140

112141
if errors == 0:
113142
return 0
114143
return 1
115144

116145

146+
def lint(files) -> int:
147+
errors = _lint(files)
148+
if errors == 0:
149+
print("Success, no errors found.")
150+
else:
151+
print(f"Failure, {errors} errors in total.")
152+
return errors
153+
154+
117155
def report() -> int:
118156
_require_cfhub()
119157
_require_cfagent()

src/cfengine_cli/lint.py

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
from cfbs.validate import validate_config
1818
from cfbs.cfbs_config import CFBSConfig
1919

20+
DEPRECATED_PROMISE_TYPES = ["defaults", "guest_environments"]
21+
ALLOWED_BUNDLE_TYPES = ["agent", "common", "monitor", "server", "edit_line"]
22+
2023

2124
def lint_cfbs_json(filename) -> int:
2225
assert os.path.isfile(filename)
@@ -72,34 +75,94 @@ def _text(node):
7275
return node.text.decode()
7376

7477

75-
def _walk(filename, lines, node) -> int:
78+
def _walk_generic(filename, lines, node, visitor):
79+
visitor(node)
80+
for node in node.children:
81+
_walk_generic(filename, lines, node, visitor)
82+
83+
84+
def _find_node_type(filename, lines, node, node_type):
85+
matches = []
86+
visitor = lambda x: matches.extend([x] if x.type == node_type else [])
87+
_walk_generic(filename, lines, node, visitor)
88+
return matches
89+
90+
91+
def _find_nodes(filename, lines, node):
92+
matches = []
93+
visitor = lambda x: matches.append(x)
94+
_walk_generic(filename, lines, node, visitor)
95+
return matches
96+
97+
98+
def _single_node_checks(filename, lines, node):
99+
"""Things which can be checked by only looking at one node,
100+
not needing to recurse into children."""
76101
line = node.range.start_point[0] + 1
77-
column = node.range.start_point[1]
78-
errors = 0
79-
# Checking for syntax errors (already detected by parser / grammar).
80-
# These are represented in the syntax tree as special ERROR nodes.
81-
if node.type == "ERROR":
102+
column = node.range.start_point[1] + 1
103+
if node.type == "attribute_name" and _text(node) == "ifvarclass":
82104
_highlight_range(node, lines)
83-
print(f"Error: Syntax error at {filename}:{line}:{column}")
84-
errors += 1
85-
86-
if node.type == "attribute_name":
87-
if _text(node) == "ifvarclass":
105+
print(
106+
f"Deprecation: Use 'if' instead of 'ifvarclass' at {filename}:{line}:{column}"
107+
)
108+
return 1
109+
if node.type == "promise_guard":
110+
assert _text(node) and len(_text(node)) > 1 and _text(node)[-1] == ":"
111+
promise_type = _text(node)[0:-1]
112+
if promise_type in DEPRECATED_PROMISE_TYPES:
113+
_highlight_range(node, lines)
114+
print(
115+
f"Deprecation: Promise type '{promise_type}' is deprecated at {filename}:{line}:{column}"
116+
)
117+
return 1
118+
if node.type == "bundle_block_name":
119+
if _text(node) != _text(node).lower():
120+
_highlight_range(node, lines)
121+
print(
122+
f"Convention: Bundle name should be lowercase at {filename}:{line}:{column}"
123+
)
124+
return 1
125+
if node.type == "promise_block_name":
126+
if _text(node) != _text(node).lower():
88127
_highlight_range(node, lines)
89128
print(
90-
f"Error: Use 'if' instead of 'ifvarclass' (deprecated) at {filename}:{line}:{column}"
129+
f"Convention: Promise type should be lowercase at {filename}:{line}:{column}"
91130
)
92-
errors += 1
131+
return 1
132+
if node.type == "bundle_block_type":
133+
if _text(node) not in ALLOWED_BUNDLE_TYPES:
134+
_highlight_range(node, lines)
135+
print(
136+
f"Error: Bundle type must be one of ({', '.join(ALLOWED_BUNDLE_TYPES)}), not '{_text(node)}' at {filename}:{line}:{column}"
137+
)
138+
return 1
139+
return 0
93140

94-
for node in node.children:
95-
errors += _walk(filename, lines, node)
141+
142+
def _walk(filename, lines, node) -> int:
143+
error_nodes = _find_node_type(filename, lines, node, "ERROR")
144+
if error_nodes:
145+
for node in error_nodes:
146+
line = node.range.start_point[0] + 1
147+
column = node.range.start_point[1] + 1
148+
_highlight_range(node, lines)
149+
print(f"Error: Syntax error at {filename}:{line}:{column}")
150+
return len(error_nodes)
151+
152+
line = node.range.start_point[0] + 1
153+
column = node.range.start_point[1] + 1
154+
155+
errors = 0
156+
for node in _find_nodes(filename, lines, node):
157+
errors += _single_node_checks(filename, lines, node)
96158

97159
return errors
98160

99161

100162
def lint_policy_file(
101163
filename, original_filename=None, original_line=None, snippet=None, prefix=None
102164
):
165+
print(f"Linting: {filename}")
103166
assert original_filename is None or type(original_filename) is str
104167
assert original_line is None or type(original_line) is int
105168
assert snippet is None or type(snippet) is int

src/cfengine_cli/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,11 @@ def _get_arg_parser():
4848
fmt = subp.add_parser("format", help="Autoformat .json and .cf files")
4949
fmt.add_argument("files", nargs="*", help="Files to format")
5050
fmt.add_argument("--line-length", default=80, type=int, help="Maximum line length")
51-
subp.add_parser(
51+
lnt = subp.add_parser(
5252
"lint",
5353
help="Look for syntax errors and other simple mistakes",
5454
)
55+
lnt.add_argument("files", nargs="*", help="Files to format")
5556
subp.add_parser(
5657
"report",
5758
help="Run the agent and hub commands necessary to get new reporting data",
@@ -131,7 +132,7 @@ def run_command_with_args(args) -> int:
131132
if args.command == "format":
132133
return commands.format(args.files, args.line_length)
133134
if args.command == "lint":
134-
return commands.lint()
135+
return commands.lint(args.files)
135136
if args.command == "report":
136137
return commands.report()
137138
if args.command == "run":

0 commit comments

Comments
 (0)