# Copyright (c) 2014 OpenStack Foundation. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import re import ast import six oslo_namespace_imports_dot = re.compile(r"import[\s]+oslo[.][^\s]+") oslo_namespace_imports_from_dot = re.compile(r"from[\s]+oslo[.]") oslo_namespace_imports_from_root = re.compile(r"from[\s]+oslo[\s]+import[\s]+") def check_oslo_namespace_imports(logical_line): if re.match(oslo_namespace_imports_from_dot, logical_line): msg = ("O323: '%s' must be used instead of '%s'.") % ( logical_line.replace('oslo.', 'oslo_'), logical_line) yield(0, msg) elif re.match(oslo_namespace_imports_from_root, logical_line): msg = ("O323: '%s' must be used instead of '%s'.") % ( logical_line.replace('from oslo import ', 'import oslo_'), logical_line) yield(0, msg) elif re.match(oslo_namespace_imports_dot, logical_line): msg = ("O323: '%s' must be used instead of '%s'.") % ( logical_line.replace('import', 'from').replace('.', ' import '), logical_line) yield(0, msg) class BaseASTChecker(ast.NodeVisitor): """Provides a simple framework for writing AST-based checks. Subclasses should implement visit_* methods like any other AST visitor implementation. When they detect an error for a particular node the method should call ``self.add_error(offending_node)``. Details about where in the code the error occurred will be pulled from the node object. Subclasses should also provide a class variable named CHECK_DESC to be used for the human readable error message. """ def __init__(self, tree, filename): """This object is created automatically by pep8. :param tree: an AST tree :param filename: name of the file being analyzed (ignored by our checks) """ self._tree = tree self._errors = [] def run(self): """Called automatically by pep8.""" self.visit(self._tree) return self._errors def add_error(self, node, message=None): """Add an error caused by a node to the list of errors for pep8.""" message = message or self.CHECK_DESC error = (node.lineno, node.col_offset, message, self.__class__) self._errors.append(error) class CheckForLoggingIssues(BaseASTChecker): DEBUG_CHECK_DESC = 'O324 Using translated string in debug logging' NONDEBUG_CHECK_DESC = 'O325 Not using translating helper for logging' EXCESS_HELPER_CHECK_DESC = 'O326 Using hints when _ is necessary' LOG_MODULES = ('logging') I18N_MODULES = ( 'oslo_messaging._i18n._', 'oslo_messaging._i18n._LI', 'oslo_messaging._i18n._LW', 'oslo_messaging._i18n._LE', 'oslo_messaging._i18n._LC', ) TRANS_HELPER_MAP = { 'debug': None, 'info': '_LI', 'warn': '_LW', 'warning': '_LW', 'error': '_LE', 'exception': '_LE', 'critical': '_LC', } def __init__(self, tree, filename): super(CheckForLoggingIssues, self).__init__(tree, filename) self.logger_names = [] self.logger_module_names = [] self.i18n_names = {} # NOTE(dstanek): this kinda accounts for scopes when talking # about only leaf node in the graph self.assignments = {} def generic_visit(self, node): """Called if no explicit visitor function exists for a node.""" for field, value in ast.iter_fields(node): if isinstance(value, list): for item in value: if isinstance(item, ast.AST): item._parent = node self.visit(item) elif isinstance(value, ast.AST): value._parent = node self.visit(value) def _filter_imports(self, module_name, alias): """Keeps lists of logging and i18n imports.""" if module_name in self.LOG_MODULES: self.logger_module_names.append(alias.asname or alias.name) elif module_name in self.I18N_MODULES: self.i18n_names[alias.asname or alias.name] = alias.name def visit_Import(self, node): for alias in node.names: self._filter_imports(alias.name, alias) return super(CheckForLoggingIssues, self).generic_visit(node) def visit_ImportFrom(self, node): for alias in node.names: full_name = '%s.%s' % (node.module, alias.name) self._filter_imports(full_name, alias) return super(CheckForLoggingIssues, self).generic_visit(node) def _find_name(self, node): """Return the fully qualified name or a Name or Attribute.""" if isinstance(node, ast.Name): return node.id elif (isinstance(node, ast.Attribute) and isinstance(node.value, (ast.Name, ast.Attribute))): method_name = node.attr obj_name = self._find_name(node.value) if obj_name is None: return None return obj_name + '.' + method_name elif isinstance(node, six.string_types): return node else: # could be Subscript, Call or many more return None def visit_Assign(self, node): """Look for 'LOG = logging.getLogger' This handles the simple case: name = [logging_module].getLogger(...) - or - name = [i18n_name](...) And some much more comple ones: name = [i18n_name](...) % X - or - self.name = [i18n_name](...) % X """ attr_node_types = (ast.Name, ast.Attribute) if (len(node.targets) != 1 or not isinstance(node.targets[0], attr_node_types)): # say no to: "x, y = ..." return super(CheckForLoggingIssues, self).generic_visit(node) target_name = self._find_name(node.targets[0]) if (isinstance(node.value, ast.BinOp) and isinstance(node.value.op, ast.Mod)): if (isinstance(node.value.left, ast.Call) and isinstance(node.value.left.func, ast.Name) and node.value.left.func.id in self.i18n_names): # NOTE(dstanek): this is done to match cases like: # `msg = _('something %s') % x` node = ast.Assign(value=node.value.left) if not isinstance(node.value, ast.Call): # node.value must be a call to getLogger self.assignments.pop(target_name, None) return super(CheckForLoggingIssues, self).generic_visit(node) # is this a call to an i18n function? if (isinstance(node.value.func, ast.Name) and node.value.func.id in self.i18n_names): self.assignments[target_name] = node.value.func.id return super(CheckForLoggingIssues, self).generic_visit(node) if (not isinstance(node.value.func, ast.Attribute) or not isinstance(node.value.func.value, attr_node_types)): # function must be an attribute on an object like # logging.getLogger return super(CheckForLoggingIssues, self).generic_visit(node) object_name = self._find_name(node.value.func.value) func_name = node.value.func.attr if (object_name in self.logger_module_names and func_name == 'getLogger'): self.logger_names.append(target_name) return super(CheckForLoggingIssues, self).generic_visit(node) def visit_Call(self, node): """Look for the 'LOG.*' calls.""" # obj.method if isinstance(node.func, ast.Attribute): obj_name = self._find_name(node.func.value) if isinstance(node.func.value, ast.Name): method_name = node.func.attr elif isinstance(node.func.value, ast.Attribute): obj_name = self._find_name(node.func.value) method_name = node.func.attr else: # could be Subscript, Call or many more return super(CheckForLoggingIssues, self).generic_visit(node) # if dealing with a logger the method can't be "warn" if obj_name in self.logger_names and method_name == 'warn': msg = node.args[0] # first arg to a logging method is the msg self.add_error(msg, message=self.USING_DEPRECATED_WARN) # must be a logger instance and one of the support logging methods if (obj_name not in self.logger_names or method_name not in self.TRANS_HELPER_MAP): return super(CheckForLoggingIssues, self).generic_visit(node) # the call must have arguments if not node.args: return super(CheckForLoggingIssues, self).generic_visit(node) if method_name == 'debug': self._process_debug(node) elif method_name in self.TRANS_HELPER_MAP: self._process_non_debug(node, method_name) return super(CheckForLoggingIssues, self).generic_visit(node) def _process_debug(self, node): msg = node.args[0] # first arg to a logging method is the msg # if first arg is a call to a i18n name if (isinstance(msg, ast.Call) and isinstance(msg.func, ast.Name) and msg.func.id in self.i18n_names): self.add_error(msg, message=self.DEBUG_CHECK_DESC) # if the first arg is a reference to a i18n call elif (isinstance(msg, ast.Name) and msg.id in self.assignments and not self._is_raised_later(node, msg.id)): self.add_error(msg, message=self.DEBUG_CHECK_DESC) def _process_non_debug(self, node, method_name): msg = node.args[0] # first arg to a logging method is the msg # if first arg is a call to a i18n name if isinstance(msg, ast.Call): try: func_name = msg.func.id except AttributeError: # in the case of logging only an exception, the msg function # will not have an id associated with it, for instance: # LOG.warning(six.text_type(e)) return # the function name is the correct translation helper # for the logging method if func_name == self.TRANS_HELPER_MAP[method_name]: return # the function name is an alias for the correct translation # helper for the loggine method if (self.i18n_names[func_name] == self.TRANS_HELPER_MAP[method_name]): return self.add_error(msg, message=self.NONDEBUG_CHECK_DESC) # if the first arg is not a reference to the correct i18n hint elif isinstance(msg, ast.Name): # FIXME(dstanek): to make sure more robust we should be checking # all names passed into a logging method. we can't right now # because: # 1. We have code like this that we'll fix when dealing with the %: # msg = _('....') % {} # LOG.warning(msg) # 2. We also do LOG.exception(e) in several places. I'm not sure # exactly what we should be doing about that. if msg.id not in self.assignments: return helper_method_name = self.TRANS_HELPER_MAP[method_name] if (self.assignments[msg.id] != helper_method_name and not self._is_raised_later(node, msg.id)): self.add_error(msg, message=self.NONDEBUG_CHECK_DESC) elif (self.assignments[msg.id] == helper_method_name and self._is_raised_later(node, msg.id)): self.add_error(msg, message=self.EXCESS_HELPER_CHECK_DESC) def _is_raised_later(self, node, name): def find_peers(node): node_for_line = node._parent for _field, value in ast.iter_fields(node._parent._parent): if isinstance(value, list) and node_for_line in value: return value[value.index(node_for_line) + 1:] continue return [] peers = find_peers(node) for peer in peers: if isinstance(peer, ast.Raise): if six.PY3: exc = peer.exc else: exc = peer.type if (isinstance(exc, ast.Call) and len(exc.args) > 0 and isinstance(exc.args[0], ast.Name) and name in (a.id for a in exc.args)): return True else: return False elif isinstance(peer, ast.Assign): if name in (t.id for t in peer.targets if hasattr(t, 'id')): return False def factory(register): register(CheckForLoggingIssues) register(check_oslo_namespace_imports)