a5cf233dcde76b40b7bb276da7c34a06b024b2c3
[salty_linter.git] / salty_linter / __init__.py
1 import pprint
2
3 def lint(sls_yaml, types):
4 """
5 Check the sls_yaml for Salt errors / warnings
6
7 typedb : Nested dictionary of Salt states/functions/parameters (partial)
8 sls_yaml: ruamel.yaml[jinja2].load()'d output for some Salt state file.
9
10 Returns: (errors, warnings)
11 errors: List of (line#, error) tuples
12 warnings: List of (line#, warnings) tuples
13 """
14
15 pp = pprint.PrettyPrinter(indent=4)
16 #pp.pprint(dir(sls_yaml))
17 #pp.pprint(sls_yaml.__dict__)
18
19
20 def lint_parameters(function_name, parameters, parameter_types):
21 """
22 Input:
23 function_name: String of the form "<state_name>.<function>" currently being linted
24 parameters: Map of {paramter_name: parameter_data} items currently being linted
25 parameter_types: Map of {parameter_name: parameter_type} items
26 Output:
27 errors: List of error string
28 warnings: List of warning strings
29 """
30 errors = []
31 warnings = []
32 for parameter in parameters:
33 parameter_name = next(iter(parameter.keys()))
34
35 ## parameter is either just a string or a {'parameter' : data} map, or always a map ?
36
37 if parameter_name in types['globals']:
38 continue
39 elif parameter_name not in parameter_types:
40 s_warning = "Unexpected parameter name {} for {}"
41 warnings.append((parameter.lc.line + 1, s_warning.format(parameter_name, function_name)))
42 else:
43 ## TODO: Add that parameter.values()[0] matches the against parameter_types[parameter] type
44 pass
45 return (errors, warnings)
46
47
48 def lint_function(state_name, function_calls, function_types):
49 """
50 Input:
51 state_name: String of the state_name currently being linted
52 function_calls: Map of {function_name: parameters} items currently being linteds
53 function_types; Map of {function_name: parameter_types} items
54 Output:
55 errors: List of error string
56 warnings: List of warning strings
57 """
58 errors = []
59 warnings = []
60 for function_name, parameters in function_calls.items():
61 if function_name not in function_types:
62 warnings.append((parameters.lc.line+1, "Unexpected function name {}.{}.".format(state_name, function_name)))
63 continue
64 else:
65 (parameter_errors, parameter_warnings) = lint_parameters(state_name + "." + function_name, parameters, function_types[function_name])
66 errors += parameter_errors
67 warnings += parameter_warnings
68 return (errors, warnings)
69
70 ## ------------------------------
71
72 state_types = types['states']
73
74 errors = []
75 warnings = []
76 for label, label_value in sls_yaml.items():
77
78 # It's possible the label_value is just a 'state.function'. See example_sls/two-items.sls
79 if not isinstance(label_value, dict):
80 value = {label_value: []}
81 else:
82 value = label_value
83
84 for state_function_name, function_calls_or_parameters in value.items():
85
86 # state_function_name should be 'state_name' or 'state_name.function_name'
87 # sosf => 'state or state.function'
88 # = [state_name] or [state_name, function_name]
89 sosf = state_function_name.split('.')
90
91 # Verify that sosf[0] is an expected state:
92 state_name = ''
93 if sosf[0] not in state_types:
94 warnings.append((value.lc.line + 1, "Unexpected state name {}.".format(sosf[0])))
95 continue
96 else:
97 state_name = sosf[0]
98
99 # "state" case
100 # function_calls_or_parameters[0] will be the function name
101 # function_call_or_parameters[1:] will be the parameters
102 if len(sosf) == 1:
103
104 if not function_calls_or_parameters:
105 errors.append(label_value.lc.line + 1, "Missing function name in parameter list for state {}.".format(state_name))
106 continue
107
108 function_name = function_calls_or_parameters[0]
109 if function_name not in state_types[state_name]:
110 warnings.append((state_function_name.lc.line + 1, "Unexpected function name {} for {}.".format(function_name, state_name)))
111 continue
112 parameters = function_call_or_parameters[1:]
113 (function_errors, function_warnings) = lint_function(state_name + "." + function_name, parameters, state_types[state_name][function_name])
114 errors += function_errors
115 warnings += function_warnings
116
117 # "state.function" case
118 # function_calls_or_parameters will be a list of parameters
119 elif len(sosf) == 2:
120 function_name = sosf[1]
121 if function_name not in state_types[state_name]:
122
123 # TODO: This gives the line number at the top of the label block which isn't great.
124 # It would be better to give the exact like number of the invalid function
125 warnings.append((sls_yaml.lc.key(label)[0] + 1, "Unexpected function name {} for {}.".format(function_name, state_name)))
126 continue
127
128 # Children should be parameters of "state.function"
129 (parameter_errors, parameter_warnings) = lint_parameters(state_name + "." + function_name, function_calls_or_parameters, state_types[state_name][function_name])
130 errors += parameter_errors
131 warnings += parameter_warnings
132
133 # Anything other than "state" or "state.function" is an error.
134 else:
135 errors.append((value.lc.line + 1, "Expected <state> or <state>.<function>: Too many '.'s in {}".format(state_function_name)))
136 continue
137
138 return(errors, warnings)
139