Update cpplint.py to r62.
Review URL: http://codereview.chromium.org/5186001

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@66483 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/cpplint.py b/cpplint.py
index 807da6b..86154ed 100755
--- a/cpplint.py
+++ b/cpplint.py
@@ -102,8 +102,9 @@
   certain of the problem, and 1 meaning it could be a legitimate construct.
   This will miss some errors, and is not a substitute for a code review.
 
-  To prevent specific lines from being linted, add a '// NOLINT' comment to the
-  end of the line.
+  To suppress false-positive errors of a certain category, add a
+  'NOLINT(category)' comment to the line.  NOLINT or NOLINT(*)
+  suppresses errors of all categories on that line.
 
   The files passed in will be linted; at least one file must be provided.
   Linted extensions are .cc, .cpp, and .h.  Other file types will be ignored.
@@ -145,64 +146,65 @@
 # If you add a new error message with a new category, add it to the list
 # here!  cpplint_unittest.py should tell you if you forget to do this.
 # \ used for clearer layout -- pylint: disable-msg=C6013
-_ERROR_CATEGORIES = '''\
-  build/class
-  build/deprecated
-  build/endif_comment
-  build/forward_decl
-  build/header_guard
-  build/include
-  build/include_alpha
-  build/include_order
-  build/include_what_you_use
-  build/namespaces
-  build/printf_format
-  build/storage_class
-  legal/copyright
-  readability/braces
-  readability/casting
-  readability/check
-  readability/constructors
-  readability/fn_size
-  readability/function
-  readability/multiline_comment
-  readability/multiline_string
-  readability/streams
-  readability/todo
-  readability/utf8
-  runtime/arrays
-  runtime/casting
-  runtime/explicit
-  runtime/int
-  runtime/init
-  runtime/invalid_increment
-  runtime/member_string_references
-  runtime/memset
-  runtime/operator
-  runtime/printf
-  runtime/printf_format
-  runtime/references
-  runtime/rtti
-  runtime/sizeof
-  runtime/string
-  runtime/threadsafe_fn
-  runtime/virtual
-  whitespace/blank_line
-  whitespace/braces
-  whitespace/comma
-  whitespace/comments
-  whitespace/end_of_line
-  whitespace/ending_newline
-  whitespace/indent
-  whitespace/labels
-  whitespace/line_length
-  whitespace/newline
-  whitespace/operators
-  whitespace/parens
-  whitespace/semicolon
-  whitespace/tab
-  whitespace/todo
-'''
+_ERROR_CATEGORIES = [
+  'build/class',
+  'build/deprecated',
+  'build/endif_comment',
+  'build/forward_decl',
+  'build/header_guard',
+  'build/include',
+  'build/include_alpha',
+  'build/include_order',
+  'build/include_what_you_use',
+  'build/namespaces',
+  'build/printf_format',
+  'build/storage_class',
+  'legal/copyright',
+  'readability/braces',
+  'readability/casting',
+  'readability/check',
+  'readability/constructors',
+  'readability/fn_size',
+  'readability/function',
+  'readability/multiline_comment',
+  'readability/multiline_string',
+  'readability/nolint',
+  'readability/streams',
+  'readability/todo',
+  'readability/utf8',
+  'runtime/arrays',
+  'runtime/casting',
+  'runtime/explicit',
+  'runtime/int',
+  'runtime/init',
+  'runtime/invalid_increment',
+  'runtime/member_string_references',
+  'runtime/memset',
+  'runtime/operator',
+  'runtime/printf',
+  'runtime/printf_format',
+  'runtime/references',
+  'runtime/rtti',
+  'runtime/sizeof',
+  'runtime/string',
+  'runtime/threadsafe_fn',
+  'runtime/virtual',
+  'whitespace/blank_line',
+  'whitespace/braces',
+  'whitespace/comma',
+  'whitespace/comments',
+  'whitespace/end_of_line',
+  'whitespace/ending_newline',
+  'whitespace/indent',
+  'whitespace/labels',
+  'whitespace/line_length',
+  'whitespace/newline',
+  'whitespace/operators',
+  'whitespace/parens',
+  'whitespace/semicolon',
+  'whitespace/tab',
+  'whitespace/todo'
+  ]
 
 # The default state of the category filter. This is overrided by the --filter=
 # flag. By default all errors are on, so only add here categories that should be
@@ -218,8 +220,8 @@
 _STL_HEADERS = frozenset([
     'algobase.h', 'algorithm', 'alloc.h', 'bitset', 'deque', 'exception',
     'function.h', 'functional', 'hash_map', 'hash_map.h', 'hash_set',
-    'hash_set.h', 'iterator', 'list', 'list.h', 'map', 'memory', 'pair.h',
-    'pthread_alloc', 'queue', 'set', 'set.h', 'sstream', 'stack',
+    'hash_set.h', 'iterator', 'list', 'list.h', 'map', 'memory', 'new',
+    'pair.h', 'pthread_alloc', 'queue', 'set', 'set.h', 'sstream', 'stack',
     'stl_alloc.h', 'stl_relops.h', 'type_traits.h',
     'utility', 'vector', 'vector.h',
     ])
@@ -287,6 +289,61 @@
 
 _regexp_compile_cache = {}
 
+# Finds occurrences of NOLINT or NOLINT(...).
+_RE_SUPPRESSION = re.compile(r'\bNOLINT\b(\([^)]*\))?')
+
+# {str, set(int)}: a map from error categories to sets of linenumbers
+# on which those errors are expected and should be suppressed.
+_error_suppressions = {}
+
+def ParseNolintSuppressions(filename, raw_line, linenum, error):
+  """Updates the global list of error-suppressions.
+
+  Parses any NOLINT comments on the current line, updating the global
+  error_suppressions store.  Reports an error if the NOLINT comment
+  was malformed.
+
+  Args:
+    filename: str, the name of the input file.
+    raw_line: str, the line of input text, with comments.
+    linenum: int, the number of the current line.
+    error: function, an error handler.
+  """
+  # FIXME(adonovan): "NOLINT(" is misparsed as NOLINT(*).
+  m = _RE_SUPPRESSION.search(raw_line)
+  if m:
+    category = m.group(1)
+    if category in (None, '(*)'):  # => "suppress all"
+      _error_suppressions.setdefault(None, set()).add(linenum)
+    else:
+      if category.startswith('(') and category.endswith(')'):
+        category = category[1:-1]
+        if category in _ERROR_CATEGORIES:
+          _error_suppressions.setdefault(category, set()).add(linenum)
+        else:
+          error(filename, linenum, 'readability/nolint', 5,
+            'Unknown NOLINT error category: %s' % category)
+
+
+def ResetNolintSuppressions():
+  "Resets the set of NOLINT suppressions to empty."
+  _error_suppressions.clear()
+
+
+def IsErrorSuppressedByNolint(category, linenum):
+  """Returns true if the specified error category is suppressed on this line.
+
+  Consults the global error_suppressions map populated by
+  ParseNolintSuppressions/ResetNolintSuppressions.
+
+  Args:
+    category: str, the category of the error.
+    linenum: int, the current line number.
+  Returns:
+    bool, True iff the error should be suppressed due to a NOLINT comment.
+  """
+  return (linenum in _error_suppressions.get(category, set()) or
+          linenum in _error_suppressions.get(None, set()))
 
 def Match(pattern, s):
   """Matches the string with the pattern, caching the compiled regexp."""
@@ -655,15 +712,18 @@
         prefix = os.path.commonprefix([root_dir, project_dir])
         return fullname[len(prefix) + 1:]
 
-      # Not SVN? Try to find a git top level directory by searching up from the
-      # current path.
+      # Not SVN? Try to find a git or hg top level directory by searching up
+      # from the current path.
       root_dir = os.path.dirname(fullname)
       while (root_dir != os.path.dirname(root_dir) and
-             not os.path.exists(os.path.join(root_dir, ".git"))):
+             not os.path.exists(os.path.join(root_dir, ".git")) and
+             not os.path.exists(os.path.join(root_dir, ".hg"))):
         root_dir = os.path.dirname(root_dir)
-        if os.path.exists(os.path.join(root_dir, ".git")):
-          prefix = os.path.commonprefix([root_dir, project_dir])
-          return fullname[len(prefix) + 1:]
+
+      if (os.path.exists(os.path.join(root_dir, ".git")) or
+          os.path.exists(os.path.join(root_dir, ".hg"))):
+        prefix = os.path.commonprefix([root_dir, project_dir])
+        return fullname[len(prefix) + 1:]
 
     # Don't know what to do; header guard warnings may be wrong...
     return fullname
@@ -699,10 +759,15 @@
     return self.Extension()[1:] in ('c', 'cc', 'cpp', 'cxx')
 
 
-def _ShouldPrintError(category, confidence):
-  """Returns true iff confidence >= verbose, and category passes filter."""
-  # There are two ways we might decide not to print an error message:
+def _ShouldPrintError(category, confidence, linenum):
+  """Returns true iff confidence >= verbose, category passes
+  filter and is not NOLINT-suppressed."""
+
+  # There are three ways we might decide not to print an error message:
+  # a "NOLINT(category)" comment appears in the source,
   # the verbosity level isn't high enough, or the filters filter it out.
+  if IsErrorSuppressedByNolint(category, linenum):
+    return False
   if confidence < _cpplint_state.verbose_level:
     return False
 
@@ -729,6 +794,10 @@
   that is, how certain we are this is a legitimate style regression, and
   not a misidentification or a use that's sometimes justified.
 
+  False positives can be suppressed by the use of
+  "cpplint(category)"  comments on the offending line.  These are
+  parsed into _error_suppressions.
+
   Args:
     filename: The name of the file containing the error.
     linenum: The number of the line containing the error.
@@ -740,9 +809,7 @@
       and 1 meaning that it could be a legitimate construct.
     message: The error message.
   """
-  # There are two ways we might decide not to print an error message:
-  # the verbosity level isn't high enough, or the filters filter it out.
-  if _ShouldPrintError(category, confidence):
+  if _ShouldPrintError(category, confidence, linenum):
     _cpplint_state.IncrementErrorCount(category)
     if _cpplint_state.output_format == 'vs7':
       sys.stderr.write('%s(%s):  %s  [%s] [%d]\n' % (
@@ -960,6 +1027,10 @@
 
   """
 
+  # Restores original filename in case that cpplint is invoked from Emacs's
+  # flymake.
+  filename = re.sub(r'_flymake\.h$', '.h', filename)
+
   fileinfo = FileInfo(filename)
   return re.sub(r'[-./\s]', '_', fileinfo.RepositoryName()).upper() + '_'
 
@@ -1006,20 +1077,23 @@
 
   # The guard should be PATH_FILE_H_, but we also allow PATH_FILE_H__
   # for backward compatibility.
-  if ifndef != cppvar and not Search(r'\bNOLINT\b', lines[ifndef_linenum]):
+  if ifndef != cppvar:
     error_level = 0
     if ifndef != cppvar + '_':
       error_level = 5
 
+    ParseNolintSuppressions(filename, lines[ifndef_linenum], ifndef_linenum,
+                            error)
     error(filename, ifndef_linenum, 'build/header_guard', error_level,
           '#ifndef header guard has wrong style, please use: %s' % cppvar)
 
-  if (endif != ('#endif  // %s' % cppvar) and
-      not Search(r'\bNOLINT\b', lines[endif_linenum])):
+  if endif != ('#endif  // %s' % cppvar):
     error_level = 0
     if endif != ('#endif  // %s' % (cppvar + '_')):
       error_level = 5
 
+    ParseNolintSuppressions(filename, lines[endif_linenum], endif_linenum,
+                            error)
     error(filename, endif_linenum, 'build/header_guard', error_level,
           '#endif line should be "#endif  // %s"' % cppvar)
 
@@ -1517,8 +1591,7 @@
       error(filename, linenum, 'readability/fn_size', 5,
             'Lint failed to find start of function body.')
   elif Match(r'^\}\s*$', line):  # function end
-    if not Search(r'\bNOLINT\b', raw_line):
-      function_state.Check(error, filename, linenum)
+    function_state.Check(error, filename, linenum)
     function_state.End()
   elif not Match(r'^\s*$', line):
     function_state.Count()  # Count non-blank/non-comment lines.
@@ -1663,9 +1736,12 @@
         # but some lines are exceptions -- e.g. if they're big
         # comment delimiters like:
         # //----------------------------------------------------------
+        # or are an empty C++ style Doxygen comment, like:
+        # ///
         # or they begin with multiple slashes followed by a space:
         # //////// Header comment
         match = (Search(r'[=/-]{4,}\s*$', line[commentend:]) or
+                 Search(r'^/$', line[commentend:]) or
                  Search(r'^/+ ', line[commentend:]))
         if not match:
           error(filename, linenum, 'whitespace/comments', 4,
@@ -2025,8 +2101,10 @@
                                                           line):
     error(filename, linenum, 'whitespace/labels', 4,
           'Labels should always be indented at least one space.  '
-          'If this is a member-initializer list in a constructor, '
-          'the colon should be on the line after the definition header.')
+          'If this is a member-initializer list in a constructor or '
+          'the base class list in a class definition, the colon should '
+          'be on the following line.')
+
 
   # Check if the line is a header guard.
   is_header_guard = False
@@ -2399,7 +2477,8 @@
 
   # When snprintf is used, the second argument shouldn't be a literal.
   match = Search(r'snprintf\s*\(([^,]*),\s*([0-9]*)\s*,', line)
-  if match:
+  if match and match.group(2) != '0':
+    # If 2nd arg is zero, snprintf is used to calculate size.
     error(filename, linenum, 'runtime/printf', 3,
           'If you can, use sizeof(%s) instead of %s as the 2nd arg '
           'to snprintf.' % (match.group(1), match.group(2)))
@@ -2748,8 +2827,13 @@
       continue
 
     # String is special -- it is a non-templatized type in STL.
-    if _RE_PATTERN_STRING.search(line):
-      required['<string>'] = (linenum, 'string')
+    m = _RE_PATTERN_STRING.search(line)
+    if m:
+      # Don't warn about strings in non-STL namespaces:
+      # (We check only the first match per line; good enough.)
+      prefix = line[:m.start()]
+      if prefix.endswith('std::') or not prefix.endswith('::'):
+        required['<string>'] = (linenum, 'string')
 
     for pattern, template, header in _re_pattern_algorithm_header:
       if pattern.search(line):
@@ -2781,9 +2865,7 @@
   # found.
   # e.g. If the file name is 'foo_flymake.cc', we should search for 'foo.h'
   # instead of 'foo_flymake.h'
-  emacs_flymake_suffix = '_flymake.cc'
-  if abs_filename.endswith(emacs_flymake_suffix):
-    abs_filename = abs_filename[:-len(emacs_flymake_suffix)] + '.cc'
+  abs_filename = re.sub(r'_flymake\.cc$', '.cc', abs_filename)
 
   # include_state is modified during iteration, so we iterate over a copy of
   # the keys.
@@ -2834,9 +2916,8 @@
 
   """
   raw_lines = clean_lines.raw_lines
+  ParseNolintSuppressions(filename, raw_lines[line], line, error)
   CheckForFunctionLengths(filename, clean_lines, line, function_state, error)
-  if Search(r'\bNOLINT\b', raw_lines[line]):  # ignore nolint lines
-    return
   CheckForMultilineCommentsAndStrings(filename, clean_lines, line, error)
   CheckStyle(filename, clean_lines, line, file_extension, error)
   CheckLanguage(filename, clean_lines, line, file_extension, include_state,
@@ -2864,6 +2945,8 @@
   function_state = _FunctionState()
   class_state = _ClassState()
 
+  ResetNolintSuppressions()
+
   CheckForCopyright(filename, lines, error)
 
   if file_extension == 'h':
@@ -2884,7 +2967,6 @@
 
   CheckForNewlineAtEOF(filename, lines, error)
 
-
 def ProcessFile(filename, vlevel):
   """Does google-lint on a single file.
 
@@ -2966,7 +3048,7 @@
 
   These are the categories used to filter messages via --filter.
   """
-  sys.stderr.write(_ERROR_CATEGORIES)
+  sys.stderr.write(''.join('  %s\n' % cat for cat in _ERROR_CATEGORIES))
   sys.exit(0)