Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/cfengine_cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import json
from cfengine_cli.profile import profile_cfengine, generate_callstack
from cfengine_cli.dev import dispatch_dev_subcommand
from cfengine_cli.lint import lint_single_arg, lint_folder
from cfengine_cli.lint import lint_folder, lint_single_arg, set_strict
from cfengine_cli.shell import user_command
from cfengine_cli.paths import bin
from cfengine_cli.version import cfengine_cli_version_string
Expand Down Expand Up @@ -94,7 +94,8 @@ def format(names, line_length) -> int:
return 0


def _lint(files) -> int:
def _lint(files, strict) -> int:
set_strict(strict)

if not files:
return lint_folder(".")
Expand All @@ -107,8 +108,8 @@ def _lint(files) -> int:
return errors


def lint(files) -> int:
errors = _lint(files)
def lint(files, strict) -> int:
errors = _lint(files, strict)
if errors == 0:
print("Success, no errors found.")
else:
Expand Down
104 changes: 96 additions & 8 deletions src/cfengine_cli/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,44 @@

DEPRECATED_PROMISE_TYPES = ["defaults", "guest_environments"]
ALLOWED_BUNDLE_TYPES = ["agent", "common", "monitor", "server", "edit_line", "edit_xml"]
BUILTIN_PROMISE_TYPES = {
"access",
"build_xpath",
"classes",
"commands",
"databases",
"defaults",
"delete_attribute",
"delete_lines",
"delete_text",
"delete_tree",
"field_edits",
"files",
"guest_environments",
"insert_lines",
"insert_text",
"insert_tree",
"measurements",
"meta",
"methods",
"packages",
"processes",
"replace_patterns",
"reports",
"roles",
"services",
"set_attribute",
"set_text",
"storage",
"users",
"vars",
}

custom_promise_types = set()

# Globally set as there might be more future cases where we want to
# classify rules that only apply in strict cases
strict = True


def lint_cfbs_json(filename) -> int:
Expand Down Expand Up @@ -117,6 +155,17 @@ def _single_node_checks(filename, lines, node):
f"Deprecation: Promise type '{promise_type}' is deprecated at {filename}:{line}:{column}"
)
return 1
if (
(promise_type not in BUILTIN_PROMISE_TYPES)
and (promise_type not in custom_promise_types)
and strict
):
_highlight_range(node, lines)
print(
f"Error: Undefined promise type '{promise_type}' at {filename}:{line}:{column}"
)
return 1

if node.type == "bundle_block_name":
if _text(node) != _text(node).lower():
_highlight_range(node, lines)
Expand All @@ -138,6 +187,7 @@ def _single_node_checks(filename, lines, node):
f"Error: Bundle type must be one of ({', '.join(ALLOWED_BUNDLE_TYPES)}), not '{_text(node)}' at {filename}:{line}:{column}"
)
return 1

return 0


Expand All @@ -161,6 +211,26 @@ def _walk(filename, lines, node) -> int:
return errors


def _parse_custom(filename, lines, root_node):
promise_blocks = _find_node_type(filename, lines, root_node, "promise_block_name")
for node in promise_blocks:
custom_promise_types.add(_text(node))
return 0


def _parse_policy_file(filename):
assert os.path.isfile(filename)
PY_LANGUAGE = Language(tscfengine.language())
parser = Parser(PY_LANGUAGE)

with open(filename, "rb") as f:
original_data = f.read()
tree = parser.parse(original_data)
lines = original_data.decode().split("\n")

return tree, lines, original_data


def lint_policy_file(
filename, original_filename=None, original_line=None, snippet=None, prefix=None
):
Expand All @@ -177,14 +247,8 @@ def lint_policy_file(
assert snippet and snippet > 0
assert os.path.isfile(filename)
assert filename.endswith((".cf", ".cfengine3", ".cf3", ".cf.sub"))
PY_LANGUAGE = Language(tscfengine.language())
parser = Parser(PY_LANGUAGE)

with open(filename, "rb") as f:
original_data = f.read()
tree = parser.parse(original_data)
lines = original_data.decode().split("\n")

tree, lines, original_data = _parse_policy_file(filename)
root_node = tree.root_node
if root_node.type != "source_file":
if snippet:
Expand Down Expand Up @@ -237,6 +301,7 @@ def lint_policy_file(

def lint_folder(folder):
errors = 0
policy_files = []
while folder.endswith(("/.", "/")):
folder = folder[0:-1]
for filename in itertools.chain(
Expand All @@ -246,7 +311,21 @@ def lint_folder(folder):
continue
if filename.startswith(".") and not filename.startswith("./"):
continue
errors += lint_single_file(filename)

if filename.endswith((".cf", ".cfengine3", ".cf3", ".cf.sub")):
policy_files.append(filename)
else:
errors += lint_single_file(filename)

# First pass: Gather custom types/bundles/+++
for filename in policy_files:
tree, lines, _ = _parse_policy_file(filename)
if tree.root_node.type == "source_file":
_parse_custom(filename, lines, tree.root_node)

# Second pass: lint all policy files
for filename in policy_files:
errors += lint_policy_file(filename)
return errors


Expand All @@ -265,3 +344,12 @@ def lint_single_arg(arg):
return lint_folder(arg)
assert os.path.isfile(arg)
return lint_single_file(arg)


def set_strict(is_strict):
"""
Used to set the global variable 'strict' inside 'lint.py'.
Used for ignoring/handling specific linting rules.
"""
global strict
strict = is_strict
8 changes: 7 additions & 1 deletion src/cfengine_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ def _get_arg_parser():
"lint",
help="Look for syntax errors and other simple mistakes",
)
lnt.add_argument(
"--strict",
type=str,
default="yes",
help="Strict mode. Default=yes, checks for custom promisetypes",
)
lnt.add_argument("files", nargs="*", help="Files to format")
subp.add_parser(
"report",
Expand Down Expand Up @@ -132,7 +138,7 @@ def run_command_with_args(args) -> int:
if args.command == "format":
return commands.format(args.files, args.line_length)
if args.command == "lint":
return commands.lint(args.files)
return commands.lint(args.files, args.strict.lower() in ("y", "ye", "yes"))
if args.command == "report":
return commands.report()
if args.command == "run":
Expand Down
Loading