diff --git a/src/cfengine_cli/lint.py b/src/cfengine_cli/lint.py index 84aadd6..89b68d5 100644 --- a/src/cfengine_cli/lint.py +++ b/src/cfengine_cli/lint.py @@ -51,7 +51,8 @@ "string", } PROMISE_BLOCK_ATTRIBUTES = ("path", "interpreter") -KNOWN_FAULTY_FUNCTION_DEFS = {"regex_replace"} +KNOWN_FAULTY_FUNCTION_DEFS = {"regex_replace", "peers"} +# Functions that have known overrides in masterfiles, e.g body with the name regex_replace, conflicting with the function @dataclass @@ -566,6 +567,7 @@ def _lint_node( if state.strict and ( node.type in ("bundle_block_name", "body_block_name") and _text(node) in syntax_data.BUILTIN_FUNCTIONS + and _text(node) not in KNOWN_FAULTY_FUNCTION_DEFS ): _highlight_range(node, lines) print( @@ -673,33 +675,62 @@ def _lint_node( if call in KNOWN_FAULTY_FUNCTION_DEFS: return 0 - args = list(filter(",".__ne__, iter(_text(x) for x in args))) + args = list( + filter(",".__ne__, iter(_text(x) for x in args if x.type != "comment")) + ) if call in syntax_data.BUILTIN_FUNCTIONS: - variadic = syntax_data.BUILTIN_FUNCTIONS.get(call, {}).get("variadic", True) - params = syntax_data.BUILTIN_FUNCTIONS.get(call, {}).get("parameters", {}) - if not variadic and (len(params) != len(args)): + func = syntax_data.BUILTIN_FUNCTIONS.get(call, {}) + variadic = func.get("variadic", True) + # variadic meaning variable amount of arguments allowed + # -1, -1 // default -- all required, aka. non-variadic func + # 1, -1 // 1-n + # 0, -1 // 0-n + # 2, 3 // 2-3 + min_args = func.get("minArgs", -1) + max_args = func.get("maxArgs", -1) + if variadic: + assert min_args != -1 + assert min_args != max_args + if max_args == -1: + max_args = float("inf") # N args allowed + else: + assert min_args == -1 and max_args == -1 + # If min args -1 (meaning all required), max should be the same + # All args required, use len of parameter list + min_args = max_args = len(func.get("parameters", [])) + + if not (min_args <= len(args) <= max_args): _highlight_range(node, lines) + argc_str = ( + f"at least {min_args}" + if max_args == float("inf") + else ( + f"{min_args}-{max_args}" + if min_args != max_args + else str(max_args) + ) + ) print( - f"Error: Expected {len(params)} arguments, received {len(args)} for function '{call}' {location}" + f"Error: Expected {argc_str} arguments, received {len(args)} for function '{call}' {location}" ) return 1 - # TODO: Handle variadic functions with varying number of required arguments (0-N, 1-N, 2-N and so on) + qualified_name = _qualify(call, state.namespace) if qualified_name in state.bundles: - params = state.bundles[qualified_name].get("parameters", []) - if len(params) != len(args): + max_args = len(state.bundles[qualified_name].get("parameters", [])) + if max_args != len(args): _highlight_range(node, lines) print( - f"Error: Expected {len(params)} arguments, received {len(args)} for bundle '{call}' {location}" + f"Error: Expected {max_args} arguments, received {len(args)} for bundle '{call}' {location}" ) return 1 if qualified_name in state.bodies: - params = state.bodies[qualified_name].get("parameters", []) - if len(params) != len(args): + max_args = len(state.bodies[qualified_name].get("parameters", [])) + if max_args != len(args): _highlight_range(node, lines) print( - f"Error: Expected {len(params)} arguments, received {len(args)} for body '{call}' {location}" + f"Error: Expected {max_args} arguments, received {len(args)} for body '{call}' {location}" ) return 1 diff --git a/src/cfengine_cli/syntax-description.json b/src/cfengine_cli/syntax-description.json index c5fabae..eef63d5 100644 --- a/src/cfengine_cli/syntax-description.json +++ b/src/cfengine_cli/syntax-description.json @@ -3429,6 +3429,7 @@ "cached": false, "category": "data", "collecting": false, + "minArgs": 0, "parameters": [], "returnType": "context", "status": "normal", @@ -3438,6 +3439,8 @@ "cached": false, "category": "data", "collecting": false, + "maxArgs": 2, + "minArgs": 1, "parameters": [ { "description": "File path", @@ -3458,6 +3461,7 @@ "cached": false, "category": "data", "collecting": false, + "minArgs": 1, "parameters": [ { "description": "Regular expression", @@ -3651,6 +3655,7 @@ "cached": false, "category": "utils", "collecting": false, + "minArgs": 1, "parameters": [], "returnType": "slist", "status": "normal", @@ -3660,6 +3665,8 @@ "cached": false, "category": "io", "collecting": false, + "maxArgs": 4, + "minArgs": 3, "parameters": [ { "description": "File name", @@ -3690,6 +3697,8 @@ "cached": false, "category": "io", "collecting": true, + "maxArgs": 3, + "minArgs": 2, "parameters": [ { "description": "CFEngine variable identifier or inline JSON", @@ -3730,6 +3739,7 @@ "cached": false, "category": "utils", "collecting": false, + "minArgs": 1, "parameters": [], "returnType": "context", "status": "normal", @@ -3739,6 +3749,7 @@ "cached": false, "category": "data", "collecting": false, + "minArgs": 0, "parameters": [], "returnType": "string", "status": "normal", @@ -3748,6 +3759,7 @@ "cached": false, "category": "utils", "collecting": false, + "minArgs": 1, "parameters": [], "returnType": "int", "status": "normal", @@ -3965,6 +3977,8 @@ "cached": false, "category": "data", "collecting": false, + "maxArgs": 3, + "minArgs": 1, "parameters": [ { "description": "Input string", @@ -4010,6 +4024,8 @@ "cached": true, "category": "utils", "collecting": false, + "maxArgs": 3, + "minArgs": 2, "parameters": [ { "description": "Fully qualified command path", @@ -4035,6 +4051,8 @@ "cached": true, "category": "utils", "collecting": false, + "maxArgs": 3, + "minArgs": 2, "parameters": [ { "description": "Fully qualified command path", @@ -4200,6 +4218,7 @@ "cached": false, "category": "files", "collecting": false, + "minArgs": 1, "parameters": [], "returnType": "slist", "status": "normal", @@ -4209,6 +4228,8 @@ "cached": false, "category": "files", "collecting": false, + "maxArgs": 3, + "minArgs": 2, "parameters": [ { "description": "Path to search from", @@ -4258,7 +4279,7 @@ ], "returnType": "data", "status": "normal", - "variadic": true + "variadic": false }, "findprocesses": { "cached": true, @@ -4279,6 +4300,7 @@ "cached": false, "category": "data", "collecting": false, + "minArgs": 1, "parameters": [ { "description": "CFEngine format string", @@ -4314,6 +4336,7 @@ "cached": false, "category": "utils", "collecting": false, + "minArgs": 1, "parameters": [ { "description": "Variable identifier", @@ -4329,6 +4352,7 @@ "cached": false, "category": "utils", "collecting": false, + "minArgs": 1, "parameters": [ { "description": "Class identifier", @@ -4409,6 +4433,8 @@ "cached": false, "category": "system", "collecting": false, + "maxArgs": 1, + "minArgs": 0, "parameters": [ { "description": "Group name or group ID as string", @@ -4424,6 +4450,8 @@ "cached": false, "category": "system", "collecting": false, + "maxArgs": 2, + "minArgs": 0, "parameters": [ { "description": "Comma separated list of Group names", @@ -4474,6 +4502,8 @@ "cached": false, "category": "system", "collecting": false, + "maxArgs": 1, + "minArgs": 0, "parameters": [ { "description": "User name in text", @@ -4489,6 +4519,8 @@ "cached": false, "category": "system", "collecting": false, + "maxArgs": 2, + "minArgs": 0, "parameters": [ { "description": "Comma separated list of User names", @@ -4524,6 +4556,7 @@ "cached": false, "category": "utils", "collecting": false, + "minArgs": 1, "parameters": [ { "description": "Variable identifier", @@ -4774,6 +4807,7 @@ "cached": false, "category": "data", "collecting": false, + "minArgs": 1, "parameters": [], "returnType": "string", "status": "normal", @@ -4833,6 +4867,7 @@ "cached": false, "category": "communication", "collecting": false, + "minArgs": 1, "parameters": [ { "description": "IP address range syntax", @@ -4882,12 +4917,14 @@ ], "returnType": "string", "status": "normal", - "variadic": true + "variadic": false }, "isconnectable": { "cached": false, "category": "communication", "collecting": false, + "maxArgs": 3, + "minArgs": 2, "parameters": [ { "description": "Host name, domain name or IP address", @@ -4963,6 +5000,7 @@ "cached": false, "category": "communication", "collecting": false, + "minArgs": 1, "parameters": [ { "description": "IP address range syntax", @@ -5068,6 +5106,8 @@ "cached": false, "category": "files", "collecting": false, + "maxArgs": 2, + "minArgs": 1, "parameters": [ { "description": "Path to file", @@ -5463,6 +5503,7 @@ "cached": false, "category": "data", "collecting": true, + "minArgs": 1, "parameters": [], "returnType": "data", "status": "normal", @@ -5605,6 +5646,7 @@ "cached": false, "category": "data", "collecting": false, + "minArgs": 0, "parameters": [], "returnType": "context", "status": "normal", @@ -6004,6 +6046,8 @@ "cached": false, "category": "io", "collecting": false, + "maxArgs": 2, + "minArgs": 1, "parameters": [ { "description": "File name", @@ -6044,6 +6088,8 @@ "cached": false, "category": "io", "collecting": false, + "maxArgs": 2, + "minArgs": 1, "parameters": [ { "description": "File name", @@ -6064,6 +6110,8 @@ "cached": false, "category": "io", "collecting": false, + "maxArgs": 2, + "minArgs": 1, "parameters": [ { "description": "File name", @@ -6159,6 +6207,8 @@ "cached": false, "category": "io", "collecting": false, + "maxArgs": 2, + "minArgs": 1, "parameters": [ { "description": "File name", @@ -6399,6 +6449,8 @@ "cached": false, "category": "io", "collecting": false, + "maxArgs": 2, + "minArgs": 1, "parameters": [ { "description": "File name", @@ -6729,6 +6781,8 @@ "cached": false, "category": "files", "collecting": false, + "maxArgs": 3, + "minArgs": 2, "parameters": [ { "description": "Path to search from", @@ -6834,6 +6888,8 @@ "cached": false, "category": "data", "collecting": true, + "maxArgs": 2, + "minArgs": 1, "parameters": [ { "description": "CFEngine variable identifier or inline JSON", @@ -7024,6 +7080,8 @@ "cached": false, "category": "data", "collecting": true, + "maxArgs": 2, + "minArgs": 1, "parameters": [], "returnType": "string", "status": "normal", @@ -7218,6 +7276,8 @@ "cached": false, "category": "data", "collecting": false, + "maxArgs": 2, + "minArgs": 1, "parameters": [ { "description": "Variable identifier", @@ -7328,6 +7388,8 @@ "cached": false, "category": "data", "collecting": false, + "maxArgs": 3, + "minArgs": 2, "parameters": [ { "description": "String to validate as JSON", @@ -7353,6 +7415,8 @@ "cached": false, "category": "data", "collecting": false, + "maxArgs": 2, + "minArgs": 1, "parameters": [ { "description": "String to validate as JSON", @@ -7373,6 +7437,7 @@ "cached": false, "category": "data", "collecting": false, + "minArgs": 1, "parameters": [], "returnType": "slist", "status": "normal", @@ -7382,6 +7447,7 @@ "cached": false, "category": "data", "collecting": false, + "minArgs": 1, "parameters": [], "returnType": "data", "status": "normal", diff --git a/tests/lint/015_variadic_func_arg_count.cf b/tests/lint/015_variadic_func_arg_count.cf new file mode 100644 index 0000000..ab2fdb5 --- /dev/null +++ b/tests/lint/015_variadic_func_arg_count.cf @@ -0,0 +1,16 @@ +bundle agent main +{ + vars: + "users1" slist => getusers(); + "users2" slist => getusers("a"); + "users3" slist => getusers("a", "b"); + "file1" string => readfile("/tmp/tmp.txt"); + "file2" string => readfile("/tmp/tmp.txt", "100"); + + reports: + "found user1 $(users1)"; + "found user2 $(users2)"; + "found user3 $(users3)"; + "found file1 $(file1)"; + "found file2 $(file2)"; +} diff --git a/tests/lint/015_variadic_func_arg_count.expected.txt b/tests/lint/015_variadic_func_arg_count.expected.txt new file mode 100644 index 0000000..93a9aa6 --- /dev/null +++ b/tests/lint/015_variadic_func_arg_count.expected.txt @@ -0,0 +1,12 @@ + + "users3" slist => getusers("a", "b"); + "users4" slist => getusers("a", "b", "c", "d"); + ^--------------------------^ +Error: Expected 0-2 arguments, received 4 for function 'getusers' at tests/lint/015_variadic_func_arg_count.x.cf:7:23 + + "file1" string => readfile("/tmp/tmp.txt"); + "file2" string => readfile("/tmp/tmp.txt", "100", "/tmp/tmp2.txt"); + ^----------------------------------------------^ +Error: Expected 1-2 arguments, received 3 for function 'readfile' at tests/lint/015_variadic_func_arg_count.x.cf:9:23 +FAIL: tests/lint/015_variadic_func_arg_count.x.cf (2 errors) +Failure, 2 errors in total. diff --git a/tests/lint/015_variadic_func_arg_count.x.cf b/tests/lint/015_variadic_func_arg_count.x.cf new file mode 100644 index 0000000..7833688 --- /dev/null +++ b/tests/lint/015_variadic_func_arg_count.x.cf @@ -0,0 +1,18 @@ +bundle agent main +{ + vars: + "users1" slist => getusers(); + "users2" slist => getusers("a"); + "users3" slist => getusers("a", "b"); + "users4" slist => getusers("a", "b", "c", "d"); + "file1" string => readfile("/tmp/tmp.txt"); + "file2" string => readfile("/tmp/tmp.txt", "100", "/tmp/tmp2.txt"); + + reports: + "found user1 $(users1)"; + "found user2 $(users2)"; + "found user3 $(users3)"; + "found user4 $(users4)"; + "found file1 $(file1)"; + "found file2 $(file2)"; +}