diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c8429e9..25d7743 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,7 @@ Changelog TBA === +* ``exclude_files`` patterns now match paths relative to their ``CPPLINT.cfg`` instead of only the next path component. (#254) * Fixed a whitespace/newline false positive for control conditions containing lambdas. (#410) * We now error on relative include paths (``./``, ``../``). (#432) * This makes ``#include "./foo.h"`` produce two separate errors: that foo.cpp should include foo.h and that relative paths are not allowed. diff --git a/cpplint.py b/cpplint.py index 41c3bb0..3f001aa 100755 --- a/cpplint.py +++ b/cpplint.py @@ -268,8 +268,10 @@ through --filter command-line flag. "exclude_files" allows to specify a regular expression to be matched against - a file name. If the expression matches, the file is skipped and not run - through the linter. + the file path relative to the CPPLINT.cfg directory. Paths use "/" as the + separator on all platforms. If the expression matches, the file is skipped + and not run through the linter. To exclude a directory subtree, use a prefix + pattern such as "^bar/" rather than an anchored directory pattern like "^bar$". "linelength" allows to specify the allowed line length for the project. @@ -7602,12 +7604,14 @@ def ProcessConfigOverrides(filename): """ abs_filename = os.path.abspath(filename) + relative_filename = "" cfg_filters = [] keep_looking = True while keep_looking: abs_path, base_name = os.path.split(abs_filename) if not base_name: break # Reached the root directory. + relative_filename = f"{base_name}/{relative_filename}" if relative_filename else base_name cfg_file = os.path.join(abs_path, _config_filename) abs_filename = abs_path @@ -7629,24 +7633,20 @@ def ProcessConfigOverrides(filename): elif name == "filter": cfg_filters.append(val) elif name == "exclude_files": - # When matching exclude_files pattern, use the base_name of - # the current file name or the directory name we are processing. + # Match the target path relative to this configuration file. # For example, if we are checking for lint errors in /foo/bar/baz.cc - # and we found the .cfg file at /foo/CPPLINT.cfg, then the config - # file's "exclude_files" filter is meant to be checked against "bar" - # and not "baz" nor "bar/baz.cc". - if base_name: - pattern = re.compile(val) - if pattern.match(base_name): - if _cpplint_state.quiet: - # Suppress "Ignoring file" warning when using --quiet. - return False - _cpplint_state.PrintInfo( - f'Ignoring "{filename}": file excluded by "{cfg_file}". ' - 'File path component "%s" matches ' - 'pattern "%s"\n' % (base_name, val) - ) + # and we found the .cfg file at /foo/CPPLINT.cfg, match against + # "bar/baz.cc". Use forward slashes consistently across platforms. + pattern = re.compile(val) + if pattern.match(relative_filename): + if _cpplint_state.quiet: + # Suppress "Ignoring file" warning when using --quiet. return False + _cpplint_state.PrintInfo( + f'Ignoring "{filename}": file excluded by "{cfg_file}". ' + 'File path "%s" matches pattern "%s"\n' % (relative_filename, val) + ) + return False elif name == "linelength": global _line_length try: diff --git a/cpplint_unittest.py b/cpplint_unittest.py index 032ecc5..9075712 100755 --- a/cpplint_unittest.py +++ b/cpplint_unittest.py @@ -7286,6 +7286,63 @@ def testInlineAssembly(self): assert len(self.nesting_state.stack) == 0 +class TestConfigOverrides: + @staticmethod + def _write_file(path, content=""): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + @pytest.mark.parametrize( + ("relative_path", "pattern", "should_process"), + [ + ("baz.cc", "^baz[.]cc$", False), + ("bar/baz.cc", "^bar/baz[.]cc$", False), + ("bar/qux.cc", "^bar/baz[.]cc$", True), + ("bar/baz.cc", "^bar$", True), + ("bar/baz.cc", "^bar/", False), + ], + ) + def test_exclude_files_matches_relative_path( + self, tmp_path, relative_path, pattern, should_process + ): + source = tmp_path.joinpath(*relative_path.split("/")) + self._write_file(source) + self._write_file( + tmp_path / "CPPLINT.cfg", + f"exclude_files={pattern}\n", + ) + + assert cpplint.ProcessConfigOverrides(str(source)) is should_process + + def test_parent_exclude_applies_through_nested_config(self, tmp_path): + source = tmp_path / "deps" / "library" / "source.cc" + self._write_file(source) + self._write_file( + tmp_path / "CPPLINT.cfg", + "exclude_files=^deps/library/source[.]cc$\n", + ) + self._write_file( + tmp_path / "deps" / "CPPLINT.cfg", + "filter=-legal/copyright\n", + ) + + assert not cpplint.ProcessConfigOverrides(str(source)) + + def test_noparent_stops_parent_exclude(self, tmp_path): + source = tmp_path / "deps" / "library" / "source.cc" + self._write_file(source) + self._write_file( + tmp_path / "CPPLINT.cfg", + "exclude_files=^deps/library/source[.]cc$\n", + ) + self._write_file( + tmp_path / "deps" / "CPPLINT.cfg", + "set noparent\n", + ) + + assert cpplint.ProcessConfigOverrides(str(source)) + + class TestQuiet: @pytest.fixture(autouse=True) def setUp(self):