Checkpoint commit / importlib-inspect approach
[salty_linter.git] / salty_linter / __init__.py
1 import importlib
2 import inspect
3
4 from salt.state import STATE_INTERNAL_KEYWORDS
5
6 def lint(sls_yaml):
7 """
8 Check the sls_yaml for Salt errors / warnings
9
10 sls_yaml: ruamel.yaml[jinja2].load()'d output for some Salt state file.
11
12 Returns: (errors, warnings)
13 errors: List of (line#, error) tuples
14 warnings: List of (line#, warnings) tuples
15 """
16
17 module_cache = {}
18
19 def lint_parameters(state_name, function_name, actual_parameters, state_module):
20 """
21 """
22 perrors = []
23 pwarnings = []
24 seen_parameters = []
25
26 try:
27 allowable_parameters = inspect.signature(vars(state_module)[function_name]).parameters
28 except Exception as e:
29 s_error = "Unexpected error looking up {}.{} parameters: {}."
30 perrors.append((1, s_error.format(state_name, function_name, e)))
31 return (perrors, pwarnings)
32
33 for a_parameter in actual_parameters:
34
35 # TODO: A comment explaining this line...
36 parameter_name = next(iter(a_parameter.keys()))
37
38 # Accept any of the global state arguments
39 # (maybe ignore 'state' and 'fun' ??)
40 if parameter_name in STATE_INTERNAL_KEYWORDS:
41 continue
42 elif parameter_name not in allowable_parameters:
43 s_warning = "Unexpected parameter '{}' for function '{}.{}'."
44 pwarnings.append((a_parameter.lc.line + 1, s_warning.format(parameter_name, state_name, function_name)))
45
46 # If there are duplicate parameters in the state, it looks like salt will use the last seen parameter,
47 # but it looks like that's undefined behavior, but its more likely some copypasta error on your part.
48 if parameter_name in seen_parameters:
49 s_error = "Duplicate parameter '{}' for function '{}.{}'."
50 perrors.append((a_parameter.lc.line + 1, s_error.format(parameter_name, state_name, function_name)))
51 else:
52 seen_parameters.append(parameter_name)
53
54 return (perrors, pwarnings)
55
56
57 ## ------------------------------
58
59 errors = []
60 warnings = []
61 for label, label_value in sls_yaml.items():
62
63 # It's possible the label_value is just a 'state.function'. See example_sls/two-items.sls
64 if not isinstance(label_value, dict):
65 value = {label_value: []}
66 else:
67 value = label_value
68
69 for state_function_name, function_calls_or_parameters in value.items():
70
71 # state_function_name should be 'state_name' or 'state_name.function_name'
72 # sosf => 'state or state.function'
73 # = [state_name] or [state_name, function_name]
74 sosf = state_function_name.split('.')
75
76 # Load the state module if needed.
77 # If it's already been loaded, use the cached version
78 state_name = ''
79 if sosf[0] in module_cache:
80 state_name = sosf[0]
81 state_mod = module_cache[state_name]
82 else:
83 try:
84 state_mod = importlib.import_module('salt.states.' + sosf[0])
85 except ModuleNotFoundError:
86 warnings.append((value.lc.line + 1, "Unexpected state name {}.".format(sosf[0])))
87 continue
88 else:
89 state_name = sosf[0]
90 module_cache[state_name] = state_mod
91
92 # "state" case
93 # function_calls_or_parameters[0] will be the function name
94 # function_calls_or_parameters[1:] will be the parameters
95 if len(sosf) == 1:
96
97 if not function_calls_or_parameters:
98 errors.append(label_value.lc.line + 1, "Missing function name in parameter list for state {}.".format(state_name))
99 continue
100
101 function_name = function_calls_or_parameters[0]
102 parameters = function_calls_or_parameters[1:]
103
104 if function_name not in vars(state_mod):
105 warnings.append((state_function_name.lc.line + 1, "Unexpected function name '{}' for state {}.".format(function_name, state_name)))
106 continue
107
108 (perrors, pwarnings) = lint_parameters(state_name, function_name, parameters, state_mod)
109 errors += perrors
110 warnings += pwarnings
111
112 # "state.function" case
113 # function_calls_or_parameters will be a list of parameters
114 elif len(sosf) == 2:
115 function_name = sosf[1]
116 if function_name not in vars(state_mod):
117
118 # TODO: This gives the line number at the top of the label block which isn't great.
119 # It would be better to give the exact like number of the invalid function
120 warnings.append((sls_yaml.lc.key(label)[0] + 1, "Unexpected function name '{}' for state {}.".format(function_name, state_name)))
121 continue
122
123 parameters = function_calls_or_parameters
124 (perrors, pwarnings) = lint_parameters(state_name, function_name, parameters, state_mod)
125 errors += perrors
126 warnings += pwarnings
127
128 # Anything other than "state" or "state.function" is an error.
129 else:
130 errors.append((value.lc.line + 1, "Expected <state> or <state>.<function>: Too many '.'s in {}.".format(state_function_name)))
131 continue
132
133 return(errors, warnings)
134