diff --git a/cpplint.py b/cpplint.py index 7e614c0..37f5a6d 100644 --- a/cpplint.py +++ b/cpplint.py @@ -44,6 +44,7 @@ import codecs import copy import getopt +import itertools import math # for log import os import re @@ -52,15 +53,42 @@ import sys import unicodedata +# if empty, use defaults +_header_extensions = set([]) + +# if empty, use defaults +_valid_extensions = set([]) + + +# Files with any of these extensions are considered to be +# header files (and will undergo different style checks). +# This set can be extended by using the --headers +# option (also supported in CPPLINT.cfg) +def GetHeaderExtensions(): + if not _header_extensions: + return set(['h', 'hpp', 'hxx', 'h++', 'cuh']) + return _header_extensions + # The allowed extensions for file names -# This is set by --extensions flag. -_valid_extensions = set(['c', 'cc', 'cpp', 'cxx', 'c++', 'h', 'hpp', 'hxx', - 'h++']) +# This is set by --extensions flag +def GetAllExtensions(): + if not _valid_extensions: + return GetHeaderExtensions().union(set(['c', 'cc', 'cpp', 'cxx', 'c++', 'cu'])) + return _valid_extensions + +def GetNonHeaderExtensions(): + return GetAllExtensions().difference(GetHeaderExtensions()) + + +# files with this suffix before the extension will be treated as test files +_test_suffixes = set(['_unittest', '_test', '_regtest']) _USAGE = """ Syntax: cpplint.py [--verbose=#] [--output=vs7] [--filter=-x,+y,...] [--counting=total|toplevel|detailed] [--root=subdir] [--linelength=digits] + [--headers=ext1,ext2] + [--extensions=ext1,ext2] [file] ... The style guidelines this tries to follow are those in @@ -137,7 +165,15 @@ The allowed file extensions that cpplint will check Examples: - --extensions=hpp,cpp + --extensions=%s + + headers=extension,extension,... + The allowed header extensions that cpplint will consider to be header files + (by default, only files with extensions %s + will be assumed to be headers) + + Examples: + --headers=%s cpplint.py supports per-directory configurations specified in CPPLINT.cfg files. CPPLINT.cfg file can contain a number of key=value pairs. @@ -173,7 +209,10 @@ build/include_alpha as well as excludes all .cc from being processed by linter, in the current directory (where the .cfg file is located) and all sub-directories. -""" % (list(_valid_extensions)) +""" % (list(GetAllExtensions()), + ','.join(list(GetAllExtensions())), + GetHeaderExtensions(), + ','.join(GetHeaderExtensions())) # We categorize each error message we print. Here are the categories. # We want an explicit list so we can list them all in cpplint --filter=. @@ -525,6 +564,7 @@ def u(x): itervalues = dict.values iteritems = dict.items + def ParseNolintSuppressions(filename, raw_line, linenum, error): """Updates the global list of error-suppressions. @@ -1074,7 +1114,7 @@ def BaseName(self): return self.Split()[1] def Extension(self): - """File extension - text following the final period.""" + """File extension - text following the final period, includes that period.""" return self.Split()[2] def NoExtension(self): @@ -1083,7 +1123,7 @@ def NoExtension(self): def IsSource(self): """File has a source file extension.""" - return self.Extension()[1:] in _valid_extensions + return self.Extension()[1:] in GetAllExtensions() def _ShouldPrintError(category, confidence, linenum): @@ -1798,28 +1838,29 @@ def CheckForHeaderGuard(filename, clean_lines, error): def CheckHeaderFileIncluded(filename, include_state, error): - """Logs an error if a .cc file does not include its header.""" + """Logs an error if a source file does not include its header.""" # Do not check test files - if filename.endswith('_test.cc') or filename.endswith('_unittest.cc'): + if _IsTestFilename(filename): return fileinfo = FileInfo(filename) - headerfile = filename[0:len(filename) - 2] + 'h' - if not os.path.exists(headerfile): - return - headername = FileInfo(headerfile).RepositoryName() - first_include = 0 - for section_list in include_state.include_list: - for f in section_list: - if headername in f[0] or f[0] in headername: - return - if not first_include: - first_include = f[1] + for ext in GetHeaderExtensions(): + headerfile = filename[:filename.rfind('.') + 1] + ext + if not os.path.exists(headerfile): + continue + headername = FileInfo(headerfile).RepositoryName() + first_include = None + for section_list in include_state.include_list: + for f in section_list: + if headername in f[0] or f[0] in headername: + return + if not first_include: + first_include = f[1] - error(filename, first_include, 'build/include', 5, - '%s should include its header file %s' % (fileinfo.RepositoryName(), - headername)) + error(filename, first_include, 'build/include', 5, + '%s should include its header file %s' % (fileinfo.RepositoryName(), + headername)) def CheckForBadCharacters(filename, lines, error): @@ -4460,7 +4501,7 @@ def CheckStyle(filename, clean_lines, linenum, file_extension, nesting_state, # Check if the line is a header guard. is_header_guard = False - if file_extension == 'h': + if file_extension in GetHeaderExtensions(): cppvar = GetHeaderGuardCPPVariable(filename) if (line.startswith('#ifndef %s' % cppvar) or line.startswith('#define %s' % cppvar) or @@ -4553,8 +4594,11 @@ def _DropCommonSuffixes(filename): Returns: The filename with the common suffix removed. """ - for suffix in ('test.cc', 'regtest.cc', 'unittest.cc', - 'inl.h', 'impl.h', 'internal.h'): + for suffix in itertools.chain( + ('%s.%s' % (test_suffix.lstrip('_'), ext) + for test_suffix, ext in itertools.product(_test_suffixes, GetNonHeaderExtensions())), + ('%s.%s' % (suffix, ext) + for suffix, ext in itertools.product(['inl', 'imp', 'internal'], GetHeaderExtensions()))): if (filename.endswith(suffix) and len(filename) > len(suffix) and filename[-len(suffix) - 1] in ('-', '_')): return filename[:-len(suffix) - 1] @@ -4570,12 +4614,10 @@ def _IsTestFilename(filename): Returns: True if 'filename' looks like a test, False otherwise. """ - if (filename.endswith('_test.cc') or - filename.endswith('_unittest.cc') or - filename.endswith('_regtest.cc')): - return True - else: - return False + for test_suffix, ext in itertools.product(_test_suffixes, GetNonHeaderExtensions()): + if filename.endswith(test_suffix + '.' + ext): + return True + return False def _ClassifyInclude(fileinfo, include, is_system): @@ -4679,11 +4721,16 @@ def CheckIncludeLine(filename, clean_lines, linenum, include_state, error): error(filename, linenum, 'build/include', 4, '"%s" already included at %s:%s' % (include, filename, duplicate_line)) - elif (include.endswith('.cc') and + return + + for extension in GetNonHeaderExtensions(): + if (include.endswith('.' + extension) and os.path.dirname(fileinfo.RepositoryName()) != os.path.dirname(include)): - error(filename, linenum, 'build/include', 4, - 'Do not include .cc files from other packages') - elif not _THIRD_PARTY_HEADERS_PATTERN.match(include): + error(filename, linenum, 'build/include', 4, + 'Do not include .' + extension + ' files from other packages') + return + + if not _THIRD_PARTY_HEADERS_PATTERN.match(include): include_state.include_list[-1].append((include, linenum)) # We want to ensure that headers appear in the right order: @@ -4833,7 +4880,7 @@ def CheckLanguage(filename, clean_lines, linenum, file_extension, CheckGlobalStatic(filename, clean_lines, linenum, error) CheckPrintf(filename, clean_lines, linenum, error) - if file_extension == 'h': + if file_extension in GetHeaderExtensions(): # TODO(unknown): check that 1-arg constructors are explicit. # How to tell it's a constructor? # (handled in CheckForNonStandardConstructs for now) @@ -4940,7 +4987,7 @@ def CheckLanguage(filename, clean_lines, linenum, file_extension, # Check for use of unnamed namespaces in header files. Registration # macros are typically OK, so we allow use of "namespace {" on lines # that end with backslashes. - if (file_extension == 'h' + if (file_extension in GetHeaderExtensions() and Search(r'\bnamespace\s*{', line) and line[-1] != '\\'): error(filename, linenum, 'build/namespaces', 4, @@ -5573,7 +5620,7 @@ def FilesBelongToSameModule(filename_cc, filename_h): some false positives. This should be sufficiently rare in practice. Args: - filename_cc: is the path for the .cc file + filename_cc: is the path for the source (e.g. .cc) file filename_h: is the path for the header path Returns: @@ -5581,20 +5628,24 @@ def FilesBelongToSameModule(filename_cc, filename_h): bool: True if filename_cc and filename_h belong to the same module. string: the additional prefix needed to open the header file. """ + fileinfo_cc = FileInfo(filename_cc) + if not fileinfo_cc.Extension().lstrip('.') in GetNonHeaderExtensions(): + return (False, '') - if not filename_cc.endswith('.cc'): + fileinfo_h = FileInfo(filename_h) + if not fileinfo_h.Extension().lstrip('.') in GetHeaderExtensions(): return (False, '') - filename_cc = filename_cc[:-len('.cc')] - if filename_cc.endswith('_unittest'): - filename_cc = filename_cc[:-len('_unittest')] - elif filename_cc.endswith('_test'): - filename_cc = filename_cc[:-len('_test')] + + filename_cc = filename_cc[:-(len(fileinfo_cc.Extension()))] + for suffix in _test_suffixes: + if filename_cc.endswith(suffix): + filename_cc = filename_cc[:-len(suffix)] + break + filename_cc = filename_cc.replace('/public/', '/') filename_cc = filename_cc.replace('/internal/', '/') - if not filename_h.endswith('.h'): - return (False, '') - filename_h = filename_h[:-len('.h')] + filename_h = filename_h[:-(len(fileinfo_h.Extension()))] if filename_h.endswith('-inl'): filename_h = filename_h[:-len('-inl')] filename_h = filename_h.replace('/public/', '/') @@ -5716,8 +5767,10 @@ def CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error, # didn't include it in the .h file. # TODO(unknown): Do a better job of finding .h files so we are confident that # not having the .h file means there isn't one. - if filename.endswith('.cc') and not header_found: - return + if not header_found: + for extension in GetNonHeaderExtensions(): + if filename.endswith('.' + extension): + return # All the lines have been processed, report the errors found. for required_header_unstripped in required: @@ -6056,7 +6109,7 @@ def ProcessFileData(filename, file_extension, lines, error, RemoveMultiLineComments(filename, lines, error) clean_lines = CleansedLines(lines) - if file_extension == 'h': + if file_extension in GetHeaderExtensions(): CheckForHeaderGuard(filename, clean_lines, error) for line in range(clean_lines.NumLines()): @@ -6069,7 +6122,7 @@ def ProcessFileData(filename, file_extension, lines, error, CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error) # Check that the .cc file has included its header if it exists. - if file_extension == 'cc': + if file_extension in GetNonHeaderExtensions(): CheckHeaderFileIncluded(filename, include_state, error) # We check here rather than inside ProcessLine so that we see raw @@ -6136,6 +6189,24 @@ def ProcessConfigOverrides(filename): _line_length = int(val) except ValueError: sys.stderr.write('Line length must be numeric.') + elif name == 'extensions': + global _valid_extensions + try: + extensions = [ext.strip() for ext in val.split(',')] + _valid_extensions = set(extensions) + except ValueError: + sys.stderr.write('Extensions should be a comma-separated list of values;' + 'for example: extensions=hpp,cpp\n' + 'This could not be parsed: "%s"' % (val,)) + elif name == 'headers': + global _header_extensions + try: + extensions = [ext.strip() for ext in val.split(',')] + _header_extensions = set(extensions) + except ValueError: + sys.stderr.write('Extensions should be a comma-separated list of values;' + 'for example: extensions=hpp,cpp\n' + 'This could not be parsed: "%s"' % (val,)) else: sys.stderr.write( 'Invalid configuration option (%s) in file %s\n' % @@ -6213,9 +6284,9 @@ def ProcessFile(filename, vlevel, extra_check_functions=[]): # When reading from stdin, the extension is unknown, so no cpplint tests # should rely on the extension. - if filename != '-' and file_extension not in _valid_extensions: + if filename != '-' and file_extension not in GetAllExtensions(): sys.stderr.write('Ignoring %s; not a valid file name ' - '(%s)\n' % (filename, ', '.join(_valid_extensions))) + '(%s)\n' % (filename, ', '.join(GetAllExtensions()))) else: ProcessFileData(filename, file_extension, lines, Error, extra_check_functions) @@ -6282,7 +6353,8 @@ def ParseArguments(args): 'filter=', 'root=', 'linelength=', - 'extensions=']) + 'extensions=', + 'headers=']) except getopt.GetoptError: PrintUsage('Invalid arguments.') @@ -6323,6 +6395,12 @@ def ParseArguments(args): _valid_extensions = set(val.split(',')) except ValueError: PrintUsage('Extensions must be comma seperated list.') + elif opt == '--headers': + global _header_extensions + try: + _header_extensions = set(val.split(',')) + except ValueError: + PrintUsage('Extensions must be comma seperated list.') if not filenames: PrintUsage('No files were specified.') diff --git a/cpplint_unittest.py b/cpplint_unittest.py index bb64ce5..2b1767b 100755 --- a/cpplint_unittest.py +++ b/cpplint_unittest.py @@ -37,10 +37,11 @@ import os import random import re +import shutil import sys -import unittest import tempfile -import shutil +import unittest + import cpplint @@ -257,18 +258,19 @@ def TestIncludeWhatYouUse(self, code, expected_message): self.PerformIncludeWhatYouUse(code)) def TestBlankLinesCheck(self, lines, start_errors, end_errors): - error_collector = ErrorCollector(self.assert_) - cpplint.ProcessFileData('foo.cc', 'cc', lines, error_collector) - self.assertEquals( - start_errors, - error_collector.Results().count( - 'Redundant blank line at the start of a code block ' - 'should be deleted. [whitespace/blank_line] [2]')) - self.assertEquals( - end_errors, - error_collector.Results().count( - 'Redundant blank line at the end of a code block ' - 'should be deleted. [whitespace/blank_line] [3]')) + for extension in ['c', 'cc', 'cpp', 'cxx', 'c++', 'cu']: + error_collector = ErrorCollector(self.assert_) + cpplint.ProcessFileData('foo.' + extension, extension, lines, error_collector) + self.assertEquals( + start_errors, + error_collector.Results().count( + 'Redundant blank line at the start of a code block ' + 'should be deleted. [whitespace/blank_line] [2]')) + self.assertEquals( + end_errors, + error_collector.Results().count( + 'Redundant blank line at the end of a code block ' + 'should be deleted. [whitespace/blank_line] [3]')) class CpplintTest(CpplintTestBase): @@ -777,11 +779,13 @@ def testTypedefForPointerToFunction(self): def testIncludeWhatYouUseNoImplementationFiles(self): code = 'std::vector foo;' - self.assertEquals('Add #include for vector<>' - ' [build/include_what_you_use] [4]', - self.PerformIncludeWhatYouUse(code, 'foo.h')) - self.assertEquals('', - self.PerformIncludeWhatYouUse(code, 'foo.cc')) + for extension in ['h', 'hpp', 'hxx', 'h++', 'cuh']: + self.assertEquals('Add #include for vector<>' + ' [build/include_what_you_use] [4]', + self.PerformIncludeWhatYouUse(code, 'foo.' + extension)) + for extension in ['c', 'cc', 'cpp', 'cxx', 'c++', 'cu']: + self.assertEquals('', + self.PerformIncludeWhatYouUse(code, 'foo.' + extension)) def testIncludeWhatYouUse(self): self.TestIncludeWhatYouUse( @@ -966,7 +970,12 @@ def testFilesBelongToSameModule(self): f = cpplint.FilesBelongToSameModule self.assertEquals((True, ''), f('a.cc', 'a.h')) self.assertEquals((True, ''), f('base/google.cc', 'base/google.h')) - self.assertEquals((True, ''), f('base/google_test.cc', 'base/google.h')) + self.assertEquals((True, ''), f('base/google_test.c', 'base/google.h')) + self.assertEquals((True, ''), f('base/google_test.cc', 'base/google.hpp')) + self.assertEquals((True, ''), f('base/google_test.cxx', 'base/google.hxx')) + self.assertEquals((True, ''), f('base/google_test.cpp', 'base/google.hpp')) + self.assertEquals((True, ''), f('base/google_test.c++', 'base/google.h++')) + self.assertEquals((True, ''), f('base/google_test.cu', 'base/google.cuh')) self.assertEquals((True, ''), f('base/google_unittest.cc', 'base/google.h')) self.assertEquals((True, ''), @@ -1123,16 +1132,17 @@ def testMultilineStrings(self): 'Use C++11 raw strings or concatenation instead.' ' [readability/multiline_string] [5]') - file_path = 'mydir/foo.cc' + for extension in ['c', 'cc', 'cpp', 'cxx', 'c++', 'cu']: + file_path = 'mydir/foo.' + extension - error_collector = ErrorCollector(self.assert_) - cpplint.ProcessFileData(file_path, 'cc', - ['const char* str = "This is a\\', - ' multiline string.";'], - error_collector) - self.assertEquals( - 2, # One per line. - error_collector.ResultList().count(multiline_string_error_message)) + error_collector = ErrorCollector(self.assert_) + cpplint.ProcessFileData(file_path, extension, + ['const char* str = "This is a\\', + ' multiline string.";'], + error_collector) + self.assertEquals( + 2, # One per line. + error_collector.ResultList().count(multiline_string_error_message)) # Test non-explicit single-argument constructors def testExplicitSingleArgumentConstructors(self): @@ -3943,18 +3953,20 @@ def testDuplicateHeader(self): error_collector.ResultList()) def testUnnamedNamespacesInHeaders(self): - self.TestLanguageRulesCheck( - 'foo.h', 'namespace {', - 'Do not use unnamed namespaces in header files. See' - ' http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Namespaces' - ' for more information. [build/namespaces] [4]') - # namespace registration macros are OK. - self.TestLanguageRulesCheck('foo.h', 'namespace { \\', '') - # named namespaces are OK. - self.TestLanguageRulesCheck('foo.h', 'namespace foo {', '') - self.TestLanguageRulesCheck('foo.h', 'namespace foonamespace {', '') - self.TestLanguageRulesCheck('foo.cc', 'namespace {', '') - self.TestLanguageRulesCheck('foo.cc', 'namespace foo {', '') + for extension in ['h', 'hpp', 'hxx', 'h++', 'cuh']: + self.TestLanguageRulesCheck( + 'foo.' + extension, 'namespace {', + 'Do not use unnamed namespaces in header files. See' + ' http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Namespaces' + ' for more information. [build/namespaces] [4]') + # namespace registration macros are OK. + self.TestLanguageRulesCheck('foo.' + extension, 'namespace { \\', '') + # named namespaces are OK. + self.TestLanguageRulesCheck('foo.' + extension, 'namespace foo {', '') + self.TestLanguageRulesCheck('foo.' + extension, 'namespace foonamespace {', '') + for extension in ['c', 'cc', 'cpp', 'cxx', 'c++', 'cu']: + self.TestLanguageRulesCheck('foo.' + extension, 'namespace {', '') + self.TestLanguageRulesCheck('foo.' + extension, 'namespace foo {', '') def testBuildClass(self): # Test that the linter can parse to the end of class definitions, @@ -4671,15 +4683,30 @@ def testClassifyInclude(self): def testTryDropCommonSuffixes(self): self.assertEqual('foo/foo', cpplint._DropCommonSuffixes('foo/foo-inl.h')) + self.assertEqual('foo/foo', cpplint._DropCommonSuffixes('foo/foo-inl.hxx')) + self.assertEqual('foo/foo', cpplint._DropCommonSuffixes('foo/foo-inl.h++')) + self.assertEqual('foo/foo', cpplint._DropCommonSuffixes('foo/foo-inl.hpp')) self.assertEqual('foo/bar/foo', cpplint._DropCommonSuffixes('foo/bar/foo_inl.h')) self.assertEqual('foo/foo', cpplint._DropCommonSuffixes('foo/foo.cc')) + self.assertEqual('foo/foo', cpplint._DropCommonSuffixes('foo/foo.cxx')) + self.assertEqual('foo/foo', cpplint._DropCommonSuffixes('foo/foo.c')) self.assertEqual('foo/foo_unusualinternal', cpplint._DropCommonSuffixes('foo/foo_unusualinternal.h')) + self.assertEqual('foo/foo_unusualinternal', + cpplint._DropCommonSuffixes('foo/foo_unusualinternal.hpp')) self.assertEqual('', cpplint._DropCommonSuffixes('_test.cc')) + self.assertEqual('', + cpplint._DropCommonSuffixes('_test.c')) + self.assertEqual('', + cpplint._DropCommonSuffixes('_test.c++')) + self.assertEqual('test', + cpplint._DropCommonSuffixes('test.c')) self.assertEqual('test', cpplint._DropCommonSuffixes('test.cc')) + self.assertEqual('test', + cpplint._DropCommonSuffixes('test.c++')) def testRegression(self): def Format(includes):