Coverage for manila/tests/hacking/checks.py: 83%

111 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Copyright (c) 2012, Cloudscaling 

2# All Rights Reserved. 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"); you may 

5# not use this file except in compliance with the License. You may obtain 

6# a copy of the License at 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

13# License for the specific language governing permissions and limitations 

14# under the License. 

15 

16import ast 

17import re 

18 

19from hacking import core 

20 

21 

22""" 

23Guidelines for writing new hacking checks 

24 

25 - Use only for Manila specific tests. OpenStack general tests 

26 should be submitted to the common 'hacking' module. 

27 - Pick numbers in the range M3xx. Find the current test with 

28 the highest allocated number and then pick the next value. 

29 - Keep the test method code in the source file ordered based 

30 on the M3xx value. 

31 - List the new rule in the top level HACKING.rst file 

32 - Add test cases for each new rule to manila/tests/test_hacking.py 

33 

34""" 

35 

36UNDERSCORE_IMPORT_FILES = [] 

37 

38translated_log = re.compile( 

39 r"(.)*LOG\." 

40 r"(audit|debug|error|info|warn|warning|critical|exception)" 

41 r"\(" 

42 r"(_|_LE|_LI|_LW)" 

43 r"\(") 

44string_translation = re.compile(r"[^_]*_\(\s*('|\")") 

45underscore_import_check = re.compile(r"(.)*import _$") 

46underscore_import_check_multi = re.compile(r"(.)*import (.)*_, (.)*") 

47# We need this for cases where they have created their own _ function. 

48custom_underscore_check = re.compile(r"(.)*_\s*=\s*(.)*") 

49dict_constructor_with_list_copy_re = re.compile(r".*\bdict\((\[)?(\(|\[)") 

50assert_True = re.compile(r".*assertEqual\(True, .*\)") 

51no_log_warn = re.compile(r"\s*LOG.warn\(.*") 

52 

53 

54class BaseASTChecker(ast.NodeVisitor): 

55 """Provides a simple framework for writing AST-based checks. 

56 

57 Subclasses should implement visit_* methods like any other AST visitor 

58 implementation. When they detect an error for a particular node the 

59 method should call ``self.add_error(offending_node)``. Details about 

60 where in the code the error occurred will be pulled from the node 

61 object. 

62 

63 Subclasses should also provide a class variable named CHECK_DESC to 

64 be used for the human readable error message. 

65 

66 """ 

67 

68 CHECK_DESC = 'No check message specified' 

69 

70 def __init__(self, tree, filename): 

71 """This object is created automatically by pep8. 

72 

73 :param tree: an AST tree 

74 :param filename: name of the file being analyzed 

75 (ignored by our checks) 

76 """ 

77 self._tree = tree 

78 self._errors = [] 

79 

80 def run(self): 

81 """Called automatically by pep8.""" 

82 self.visit(self._tree) 

83 return self._errors 

84 

85 def add_error(self, node, message=None): 

86 """Add an error caused by a node to the list of errors for pep8.""" 

87 message = message or self.CHECK_DESC 

88 error = (node.lineno, node.col_offset, message, self.__class__) 

89 self._errors.append(error) 

90 

91 def _check_call_names(self, call_node, names): 

92 if isinstance(call_node, ast.Call): 

93 if isinstance(call_node.func, ast.Name): 93 ↛ 96line 93 didn't jump to line 96 because the condition on line 93 was always true

94 if call_node.func.id in names: 94 ↛ 96line 94 didn't jump to line 96 because the condition on line 94 was always true

95 return True 

96 return False 

97 

98 

99@core.flake8ext 

100def no_translate_logs(logical_line): 

101 if translated_log.match(logical_line): 

102 yield (0, "M359 Don't translate log messages!") 

103 

104 

105class CheckLoggingFormatArgs(BaseASTChecker): 

106 """Check for improper use of logging format arguments. 

107 

108 LOG.debug("Volume %s caught fire and is at %d degrees C and climbing.", 

109 ('volume1', 500)) 

110 

111 The format arguments should not be a tuple as it is easy to miss. 

112 

113 """ 

114 

115 name = "check_logging_format_args" 

116 version = "1.0" 

117 CHECK_DESC = 'M310 Log method arguments should not be a tuple.' 

118 LOG_METHODS = [ 

119 'debug', 'info', 

120 'warn', 'warning', 

121 'error', 'exception', 

122 'critical', 'fatal', 

123 'trace', 'log' 

124 ] 

125 

126 def _find_name(self, node): 

127 """Return the fully qualified name or a Name or Attribute.""" 

128 if isinstance(node, ast.Name): 128 ↛ 130line 128 didn't jump to line 130 because the condition on line 128 was always true

129 return node.id 

130 elif (isinstance(node, ast.Attribute) 

131 and isinstance(node.value, (ast.Name, ast.Attribute))): 

132 method_name = node.attr 

133 obj_name = self._find_name(node.value) 

134 if obj_name is None: 

135 return None 

136 return obj_name + '.' + method_name 

137 elif isinstance(node, str): 

138 return node 

139 else: # could be Subscript, Call or many more 

140 return None 

141 

142 def visit_Call(self, node): 

143 """Look for the 'LOG.*' calls.""" 

144 # extract the obj_name and method_name 

145 if isinstance(node.func, ast.Attribute): 

146 obj_name = self._find_name(node.func.value) 

147 if isinstance(node.func.value, ast.Name): 147 ↛ 149line 147 didn't jump to line 149 because the condition on line 147 was always true

148 method_name = node.func.attr 

149 elif isinstance(node.func.value, ast.Attribute): 

150 obj_name = self._find_name(node.func.value) 

151 method_name = node.func.attr 

152 else: # could be Subscript, Call or many more 

153 return super(CheckLoggingFormatArgs, self).generic_visit(node) 

154 

155 # obj must be a logger instance and method must be a log helper 

156 if (obj_name != 'LOG' 

157 or method_name not in self.LOG_METHODS): 

158 return super(CheckLoggingFormatArgs, self).generic_visit(node) 

159 

160 # the call must have arguments 

161 if not len(node.args): 161 ↛ 162line 161 didn't jump to line 162 because the condition on line 161 was never true

162 return super(CheckLoggingFormatArgs, self).generic_visit(node) 

163 

164 # any argument should not be a tuple 

165 for arg in node.args: 

166 if isinstance(arg, ast.Tuple): 

167 self.add_error(arg) 

168 

169 return super(CheckLoggingFormatArgs, self).generic_visit(node) 

170 

171 

172@core.flake8ext 

173def check_explicit_underscore_import(logical_line, filename): 

174 """Check for explicit import of the _ function 

175 

176 We need to ensure that any files that are using the _() function 

177 to translate logs are explicitly importing the _ function. We 

178 can't trust unit test to catch whether the import has been 

179 added so we need to check for it here. 

180 """ 

181 

182 # Build a list of the files that have _ imported. No further 

183 # checking needed once it is found. 

184 if filename in UNDERSCORE_IMPORT_FILES: 

185 pass 

186 elif (underscore_import_check.match(logical_line) or 

187 underscore_import_check_multi.match(logical_line) or 

188 custom_underscore_check.match(logical_line)): 

189 UNDERSCORE_IMPORT_FILES.append(filename) 

190 elif string_translation.match(logical_line): 

191 yield (0, "M323: Found use of _() without explicit import of _ !") 

192 

193 

194class CheckForTransAdd(BaseASTChecker): 

195 """Checks for the use of concatenation on a translated string. 

196 

197 Translations should not be concatenated with other strings, but 

198 should instead include the string being added to the translated 

199 string to give the translators the most information. 

200 """ 

201 

202 name = "check_for_trans_add" 

203 version = "1.0" 

204 CHECK_DESC = ('M326 Translated messages cannot be concatenated. ' 

205 'String should be included in translated message.') 

206 

207 TRANS_FUNC = ['_', '_LI', '_LW', '_LE', '_LC'] 

208 

209 def visit_BinOp(self, node): 

210 if isinstance(node.op, ast.Add): 210 ↛ 215line 210 didn't jump to line 215 because the condition on line 210 was always true

211 if self._check_call_names(node.left, self.TRANS_FUNC): 

212 self.add_error(node.left) 

213 elif self._check_call_names(node.right, self.TRANS_FUNC): 

214 self.add_error(node.right) 

215 super(CheckForTransAdd, self).generic_visit(node) 

216 

217 

218@core.flake8ext 

219def dict_constructor_with_list_copy(logical_line): 

220 msg = ("M336: Must use a dict comprehension instead of a dict constructor" 

221 " with a sequence of key-value pairs." 

222 ) 

223 if dict_constructor_with_list_copy_re.match(logical_line): 

224 yield (0, msg) 

225 

226 

227@core.flake8ext 

228def validate_assertTrue(logical_line): 

229 if re.match(assert_True, logical_line): 

230 msg = ("M313: Unit tests should use assertTrue(value) instead" 

231 " of using assertEqual(True, value).") 

232 yield (0, msg) 

233 

234 

235@core.flake8ext 

236def check_uuid4(logical_line): 

237 """Generating UUID 

238 

239 Use oslo_utils.uuidutils to generate UUID instead of uuid4(). 

240 

241 M354 

242 """ 

243 

244 msg = ("M354: Use oslo_utils.uuidutils to generate UUID instead " 

245 "of uuid4().") 

246 

247 if "uuid4()." in logical_line: 

248 return 

249 

250 if "uuid4()" in logical_line: 

251 yield (0, msg) 

252 

253 

254@core.flake8ext 

255def no_log_warn_check(logical_line): 

256 """Disallow 'LOG.warn' 

257 

258 Deprecated LOG.warn(), instead use LOG.warning 

259 ://bugs.launchpad.net/manila/+bug/1508442 

260 

261 M338 

262 """ 

263 msg = ("M338: LOG.warn is deprecated, use LOG.warning.") 

264 if re.match(no_log_warn, logical_line): 

265 yield (0, msg)