Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Changelog
TBA
===

* Detect missing spaces before control-statement parentheses at the start of a line or after tabs. (#226)
* Use path-aware prefix checks instead of deprecated os.path.commonprefix(). (#445)
* Fixed a whitespace/newline false positive for control conditions containing lambdas. (#410)
* We now error on relative include paths (``./``, ``../``). (#432)
Expand Down
13 changes: 10 additions & 3 deletions cpplint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2160,6 +2160,11 @@ def NumLines(self):
"""Returns the number of lines represented."""
return self.num_lines

def GetLineWithSpaceForComments(self, linenum):
"""Returns an elided line with comments replaced by spaces."""
line = self._CollapseStrings(self.lines_without_raw_strings[linenum])
return CleanseComments(re.sub(_RE_PATTERN_C_COMMENTS, " ", line))

@staticmethod
def _CollapseStrings(elided):
"""Collapses strings and chars on a line to simple "" or '' blocks.
Expand Down Expand Up @@ -4547,9 +4552,11 @@ def CheckParenthesisSpacing(filename, clean_lines, linenum, error):
"""
line = clean_lines.elided[linenum]

# No spaces after an if, while, switch, or for
match = re.search(r" (if\(|for\(|while\(|switch\()", line)
if match:
# Require a space after if, while, switch, or for.
# Comments separate tokens even when CleanseComments removes adjacent spaces.
spacing_line = clean_lines.GetLineWithSpaceForComments(linenum)
match = re.search(r"(?:^|\s)(if\(|for\(|while\(|switch\()", spacing_line)
if match and not re.match(r"\s*#\s*if\b", spacing_line):
error(
filename, linenum, "whitespace/parens", 5, f"Missing space before ( in {match.group(1)}"
)
Expand Down
77 changes: 76 additions & 1 deletion cpplint_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2706,7 +2706,7 @@ def testCheckAltTokens(self):
"line_continuation or", "Use operator || instead of or [readability/alt_tokens] [2]"
)
self.TestLint(
"if(true and(parentheses",
"if (true and(parentheses",
"Use operator && instead of and [readability/alt_tokens] [2]",
)

Expand Down Expand Up @@ -2979,6 +2979,81 @@ def testInlineControlBodyWithLambda(self):
" [whitespace/newline] [5]",
)

@pytest.mark.parametrize("keyword", ["if", "for", "while", "switch"])
@pytest.mark.parametrize(
"prefix",
[
"",
" ",
"\t",
"#define CHECK() ",
"#define CHECK() \t",
"/* KEYWORD(ignored) */ ",
'const char* text = "KEYWORD(ignored)"; ',
'const char* text = R"(KEYWORD(ignored))"; ',
"KEYWORD /* reason */(first) {} ",
"KEYWORD_handler(first); ",
],
)
def testMissingSpaceBeforeControlParenthesis(self, keyword, prefix):
error_collector = ErrorCollector(self.assertTrue)
code = prefix.replace("KEYWORD", keyword) + f"{keyword}(condition) {{"
clean_lines = cpplint.CleansedLines([code])
cpplint.CheckParenthesisSpacing("foo.cc", clean_lines, 0, error_collector)
assert error_collector.Results() == (
f"Missing space before ( in {keyword}( [whitespace/parens] [5]"
)

@pytest.mark.parametrize("keyword", ["if", "for", "while", "switch"])
@pytest.mark.parametrize("prefix", ["", " ", "\t", "#define CHECK() "])
@pytest.mark.parametrize(
"code",
[
"KEYWORD (condition) {",
"KEYWORD /* reason */(condition) {",
"KEYWORD/* reason */(condition) {",
"KEYWORD/* first *//* second */(condition) {",
"/* KEYWORD(ignored) */ KEYWORD /* reason */(condition) {",
"// KEYWORD(condition) {",
"/* KEYWORD(condition) { */",
'const char* text = "KEYWORD(condition) {";',
'const char* text = R"(KEYWORD(condition) {)";',
"my_KEYWORD(condition);",
"KEYWORD_handler(condition);",
],
)
def testMissingSpaceBeforeControlParenthesisIgnoresNonKeywords(self, keyword, prefix, code):
error_collector = ErrorCollector(self.assertTrue)
clean_lines = cpplint.CleansedLines([prefix + code.replace("KEYWORD", keyword)])
cpplint.CheckParenthesisSpacing("foo.cc", clean_lines, 0, error_collector)
assert error_collector.Results() == ""

@pytest.mark.parametrize(
"code",
[
"i/**/f(condition);",
"f/**/or(condition);",
"whi/**/le(condition);",
"swi/**/tch(condition);",
],
)
def testControlParenthesisDoesNotJoinTokensAcrossComments(self, code):
error_collector = ErrorCollector(self.assertTrue)
clean_lines = cpplint.CleansedLines([code])
cpplint.CheckParenthesisSpacing("foo.cc", clean_lines, 0, error_collector)
assert error_collector.Results() == ""

@pytest.mark.parametrize("directive", ["if", "elif"])
@pytest.mark.parametrize("prefix", ["", " ", "\t"])
@pytest.mark.parametrize("separator", ["", " ", "\t", " \t "])
def testMissingSpaceBeforeControlParenthesisIgnoresPreprocessor(
self, directive, prefix, separator
):
error_collector = ErrorCollector(self.assertTrue)
clean_lines = cpplint.CleansedLines([f"{prefix}#{separator}{directive}(condition)"])
cpplint.CheckParenthesisSpacing("foo.cc", clean_lines, 0, error_collector)
assert error_collector.Results() == ""

def testMismatchingSpacesInParens(self):
self.TestLint("if (foo ) {", "Mismatching spaces inside () in if [whitespace/parens] [5]")
self.TestLint(
Expand Down
Loading
Loading