diff --git a/cpplint.py b/cpplint.py index ff25278..86c1027 100755 --- a/cpplint.py +++ b/cpplint.py @@ -3217,20 +3217,33 @@ def CheckEnd(self, filename, clean_lines, linenum, error): ) -class _WrappedInfo(_BlockInfo): +class _WrapInfo(_BlockInfo): """Stores information about parentheses, initializer lists, etc. Not exactly a block but we do need the same signature. Needed to avoid namespace indentation false positives, though parentheses tracking would slow us down a lot and is effectively already done by open_parentheses.""" - pass + def __init__(self, linenum: int, index: int): + _BlockInfo.__init__(self, linenum, seen_open_brace=False) + # The index on lines[starting_linenum] where the wrap started + self.starting_index = index -class _MemInitListInfo(_WrappedInfo): +class _MemInitListInfo(_WrapInfo): """Stores information about member initializer lists.""" - pass + def __init__(self, linenum: int, index: int): + _WrapInfo.__init__(self, linenum, index) + # For `: a(b)`, a is the identifier + self.expecting_identifier = True + # Whether we are expecting the value that follows the identifier + self.expecting_braces = False + # Whether we are currently within braces + self.in_braces = False + # Only used when within braces + self.is_curly = False + self.open_brace_count = 0 class _PreprocessorInfo: @@ -3384,6 +3397,20 @@ def InTemplateArgumentList(self, clean_lines, linenum, pos): pos = end_pos return False + def _BequeathParensAndAppend(self, appendee: _BlockInfo, parens_changed: int): + """Pass down number of open parentheses and append to the stack. + + Args: + appendee: The object to append to the stack. + parens_changed: The number of parentheses opened, as computed by _ChangeParentheses(). + """ + # Compensate for adding parentheses to the previous top block before this update + if self.stack: + self.stack[-1].open_parentheses -= parens_changed + appendee.open_parentheses += parens_changed + + self.stack.append(appendee) + def UpdatePreprocessor(self, line): """Update preprocessor stack. @@ -3444,9 +3471,15 @@ def _Pop(self): """Pop the innermost state (top of the stack) and remember the popped item.""" self.popped_top = self.stack.pop() - def _CountOpenParentheses(self, line: str): - # Count parentheses. This is to avoid adding struct arguments to - # the nesting stack. + def _ChangeParentheses(self, line: str) -> int: + """ + Count parentheses. This is to avoid adding struct arguments to the nesting stack. + Args: + line: The line to count parentheses on. + + Returns: + The number of parentheses opened on this line. + """ if self.stack: inner_block = self.stack[-1] depth_change = line.count("(") - line.count(")") @@ -3469,7 +3502,10 @@ def _CountOpenParentheses(self, line: str): # Exit assembly block inner_block.inline_asm = _END_ASM - def _UpdateNamesapce(self, line: str, linenum: int) -> str | None: + return depth_change + return 0 + + def _ConsumeNamespace(self, line: str, linenum: int) -> str: """ Match start of namespace, append to stack, and consume line Args: @@ -3477,40 +3513,160 @@ def _UpdateNamesapce(self, line: str, linenum: int) -> str | None: linenum: Line number of the line to check Returns: - The consumed line if namespace matched; None otherwise + Consumed line """ - # Match start of namespace. The "\b\s*" below catches namespace - # declarations even if it weren't followed by a whitespace, this - # is so that we don't confuse our namespace checker. The - # missing spaces will be flagged by CheckSpacing. - namespace_decl_match = re.match(r"^\s*namespace\b\s*([:\w]+)?(.*)$", line) - if not namespace_decl_match: - return None + while True: + # Match start of namespace. The "\b\s*" below catches namespace + # declarations even if it weren't followed by a whitespace, this + # is so that we don't confuse our namespace checker. The + # missing spaces will be flagged by CheckSpacing. + namespace_decl_match = re.match(r"^\s*namespace\b\s*([:\w]+)?(.*)$", line) + if not namespace_decl_match: + break - new_namespace = _NamespaceInfo(namespace_decl_match.group(1), linenum) - self.stack.append(new_namespace) + new_namespace = _NamespaceInfo(namespace_decl_match.group(1), linenum) + self.stack.append(new_namespace) - line = namespace_decl_match.group(2) - if line.find("{") != -1: - new_namespace.seen_open_brace = True - line = line[line.find("{") + 1 :] + line = namespace_decl_match.group(2) + if line.find("{") != -1: + new_namespace.seen_open_brace = True + line = line[line.find("{") + 1 :] return line - def _UpdateConstructor(self, line: str, linenum: int, class_name: str | None = None): + def _UpdateConstructor( + self, line: str, linenum: int, parens_changed: int, class_name: str | None = None + ) -> bool: """ Check if the given line is a constructor. Args: line: Line to check. class_name: If line checked is inside of a class block, a str of the class's name; otherwise, None. + parens_changed: The number of parentheses opened on this line. + + Returns: + Whether the given line was a constructor. """ + prefix = r"(?:^|public|private|protected|friend|inline|explicit|constexpr)\s*" if not class_name: - if not re.match(r"\s*(\w*)\s*::\s*\1\s*\(", line): - return - elif not re.match(rf"\s*{re.escape(class_name)}\s*\(", line): - return + if not re.search(prefix + r"(\w*)\s*::\s*\1\s*\(", line): + return False + elif not re.search(prefix + rf"{re.escape(class_name)}\s*\(", line): + return False + + self._BequeathParensAndAppend(_ConstructorInfo(linenum), parens_changed) + return True + + def _ConsumeMemInitList(self, line: str, item: _MemInitListInfo) -> str: + """ + Consume a line with a member initializer list and update its state. + Args: + line: Line to check and consume + item: Stack item for the checked line to read from and update + + Returns: + Processed line + """ + while True: + if line == "": + break # see last line of `if item.in_braces` + if item.expecting_identifier: + if searched := re.search(r"(\w+)", line): + item.expecting_identifier = False + line = line[searched.end(0) :] + item.expecting_braces = True + else: + break + if item.expecting_braces: + if searched := re.match(r"\s*([({])", line): + item.in_braces = True + item.open_brace_count = 1 + item.is_curly = searched.group(1) == "{" + line = line[searched.end(0) :] + item.expecting_braces = False + else: + break + if item.in_braces: + lbrace, rbrace = ("{", "}") if item.is_curly else ("(", ")") + # Matching braces + while found := line.find(rbrace) + 1: # the index after that of the rbrace + item.open_brace_count -= line[:found].count(rbrace) - line[:found].count(lbrace) + line = line[found:] + if item.open_brace_count == 0: + item.in_braces = False + break + if item.in_braces: + # Nothing meaningful for _ConsumeEnd() if we're still within the MemInitList + # Plus the mentioned function would pick up the }{ + return "" + if not (item.expecting_identifier or item.expecting_braces or item.in_braces): + if found := line.find(",") + 1: + item.expecting_identifier = True + line = line[found:] + else: + break + return line - self.stack.append(_ConstructorInfo(linenum)) + def _ConsumeEnd( + self, line: str, filename: str, clean_lines: CleansedLines, linenum: int, error + ) -> str | None: + """ + Consume braces or semicolons from what's left of the line. + Checks that should only be run after all other Update checks have been. + Args: + line: Line to check and consume + filename: Name of the current file + clean_lines: CleansedLines instance containing the file + linenum: Number of the line to check + error: Function to call with any errors found + + Returns: + The consumed line if any checks were successful; None otherwise. + """ + # Match first brace, semicolon, or closed parenthesis. + searched = re.search(r"([{;)}])", line) + if not searched: + return None + + token = searched.group(1) + if token == "{": + # If namespace or class hasn't seen an opening brace yet, mark + # namespace/class head as complete. Push a new block onto the + # stack otherwise. + if not self.SeenOpenBrace(): + # End of initializer list wrap if present + if isinstance(self.stack[-1], _MemInitListInfo): + self._Pop() + self.stack[-1].seen_open_brace = True + elif re.match(r'^extern\s*"[^"]*"\s*\{', line): + self.stack.append(_ExternCInfo(linenum)) + else: + self.stack.append(_BlockInfo(linenum, True)) + if _MATCH_ASM.match(line): + self.stack[-1].inline_asm = _BLOCK_ASM + elif token == ";": + # If we haven't seen an opening brace yet, but we already saw + # a semicolon, this is probably a forward declaration. Pop + # the stack for these. + if not self.SeenOpenBrace(): + self._Pop() + elif token == ")": + # Similarly, if we haven't seen an opening brace yet, but we + # already saw a closing parenthesis, then these are probably + # function arguments with extra "class" or "struct" keywords. + # Also pop these stack for these. + if ( + self.stack + and not self.stack[-1].seen_open_brace + and isinstance(self.stack[-1], _ClassInfo) + ): + self._Pop() + else: # token == '}' + # Perform end of block checks and pop the stack. + if self.stack: + self.stack[-1].CheckEnd(filename, clean_lines, linenum, error) + self._Pop() + return line[searched.end(0) :] # TODO(google): Update() is too long, but we will refactor later. def Update(self, filename: str, clean_lines: CleansedLines, linenum: int, error): @@ -3538,13 +3694,12 @@ def Update(self, filename: str, clean_lines: CleansedLines, linenum: int, error) # Update pp_stack self.UpdatePreprocessor(line) - self._CountOpenParentheses(line) + parens_changed = self._ChangeParentheses(line) # Consume namespace declaration at the beginning of the line. Do # this in a loop so that we catch same line declarations like this: # namespace proto2 { namespace bridge { class MessageSet; } } - while (new_line := self._UpdateNamesapce(line, linenum)) is not None: # could be empty str - line = new_line + line = self._ConsumeNamespace(line, linenum) # Look for a class declaration in whatever is left of the line # after parsing namespaces. The regexp accounts for decorated classes @@ -3615,9 +3770,11 @@ def Update(self, filename: str, clean_lines: CleansedLines, linenum: int, error) line = access_match.group(4) else: - self._UpdateConstructor(line, linenum, class_name=classinfo.name) + self._UpdateConstructor( + line, linenum, parens_changed, class_name=classinfo.name + ) else: # Not in class - self._UpdateConstructor(line, linenum) + self._UpdateConstructor(line, linenum, parens_changed) # If brace not open and we just finished a parenthetical definition, # check if we're in a member initializer list following a constructor. @@ -3628,56 +3785,22 @@ def Update(self, filename: str, clean_lines: CleansedLines, linenum: int, error) or isinstance(self.previous_stack_top, _ConstructorInfo) ) and not self.stack[-1].seen_open_brace - and re.search(r"[^:]:[^:]", line) + and (searched := re.search(r"[^:]:[^:]", line)) ): - self.stack.append(_MemInitListInfo(linenum, seen_open_brace=False)) + self._BequeathParensAndAppend( + _MemInitListInfo(linenum, index=searched.start(0) + 1), parens_changed + ) + line = line[searched.end(0) :] # Consume everything b4 the MemInitList, incl. the ':' - # Consume braces or semicolons from what's left of the line - while True: - # Match first brace, semicolon, or closed parenthesis. - matched = re.match(r"^[^{;)}]*([{;)}])(.*)$", line) - if not matched: - break + # Consume contents of MemInitList if we're in one + if self.stack and isinstance(self.stack[-1], _MemInitListInfo): + line = self._ConsumeMemInitList(line, self.stack[-1]) - token = matched.group(1) - if token == "{": - # If namespace or class hasn't seen an opening brace yet, mark - # namespace/class head as complete. Push a new block onto the - # stack otherwise. - if not self.SeenOpenBrace(): - # End of initializer list wrap if present - if isinstance(self.stack[-1], _MemInitListInfo): - self._Pop() - self.stack[-1].seen_open_brace = True - elif re.match(r'^extern\s*"[^"]*"\s*\{', line): - self.stack.append(_ExternCInfo(linenum)) - else: - self.stack.append(_BlockInfo(linenum, True)) - if _MATCH_ASM.match(line): - self.stack[-1].inline_asm = _BLOCK_ASM - elif token == ";": - # If we haven't seen an opening brace yet, but we already saw - # a semicolon, this is probably a forward declaration. Pop - # the stack for these. - if not self.SeenOpenBrace(): - self._Pop() - elif token == ")": - # Similarly, if we haven't seen an opening brace yet, but we - # already saw a closing parenthesis, then these are probably - # function arguments with extra "class" or "struct" keywords. - # Also pop these stack for these. - if ( - self.stack - and not self.stack[-1].seen_open_brace - and isinstance(self.stack[-1], _ClassInfo) - ): - self._Pop() - else: # token == '}' - # Perform end of block checks and pop the stack. - if self.stack: - self.stack[-1].CheckEnd(filename, clean_lines, linenum, error) - self._Pop() - line = matched.group(2) + # Consume braces or semicolons from what's left of the line + while ( + new_line := self._ConsumeEnd(line, filename, clean_lines, linenum, error) + ) is not None: + line = new_line def InnermostClass(self): """Get class info on the top of the stack. @@ -7248,7 +7371,7 @@ def IsBlockInNameSpace(nesting_state: NestingState, is_forward_declaration: bool and ( isinstance(nesting_state.stack[-2], _NamespaceInfo) or len(nesting_state.stack) > 2 # Accommodate for WrappedInfo - and issubclass(type(nesting_state.stack[-1]), _WrappedInfo) + and issubclass(type(nesting_state.stack[-1]), _WrapInfo) and not nesting_state.stack[-2].seen_open_brace and isinstance(nesting_state.stack[-3], _NamespaceInfo) ) @@ -7294,15 +7417,22 @@ def ShouldCheckNamespaceIndentation( # Skip if we are extra-indenting a member initializer list. if ( - isinstance(nesting_state.previous_stack_top, _ConstructorInfo) # F/N (A::A() : _a(0) {/{}) - and ( - isinstance(nesting_state.stack[-1], _MemInitListInfo) - or isinstance(nesting_state.popped_top, _MemInitListInfo) + ( + isinstance(nesting_state.previous_stack_top, _ConstructorInfo) # F/N (A::A() : _a(0){) + and ( + isinstance(nesting_state.stack[-1], _MemInitListInfo) + or isinstance(nesting_state.popped_top, _MemInitListInfo) + ) + ) + or ( # empty constructor in multiline list + isinstance(nesting_state.previous_stack_top, _MemInitListInfo) + and isinstance(nesting_state.popped_top, _ConstructorInfo) + ) + or ( # popping constructor after MemInitList on the same line (: _a(a) {}) + isinstance(nesting_state.previous_stack_top, _ConstructorInfo) + and isinstance(nesting_state.popped_top, _ConstructorInfo) + and re.search(r"[^:]:[^:]", raw_lines_no_comments[linenum]) ) - ) or ( # popping constructor after MemInitList on the same line (: _a(a) {}) - isinstance(nesting_state.previous_stack_top, _ConstructorInfo) - and isinstance(nesting_state.popped_top, _ConstructorInfo) - and re.search(r"[^:]:[^:]", raw_lines_no_comments[linenum]) ): return False diff --git a/cpplint_unittest.py b/cpplint_unittest.py index fb1eb69..1066907 100755 --- a/cpplint_unittest.py +++ b/cpplint_unittest.py @@ -327,20 +327,20 @@ def testNamespaceIndentationMemberInitializerList(self): lines = [ "namespace Opossum {", "", - "Acme::Acme(const std::shared_ptr left,", - " const std::shared_ptr nigh)", - " : _left(left), _behind(nigh) {}", + "protected Acme::Acme(const std::shared_ptr left,", + " const std::shared_ptr nigh)", + " : _left(left), _behind{nigh} {}", "", "} // namespace Opossum", ] assert self.GetNamespaceResults(lines) == "" - # Multiline member initializer List + # Multiline member initializer list; constructor on different line from list lines = [ "namespace Rosenfield {", "class Crush : public Habitual {", " public:", - " Crush() : _a(1),", + " explicit Crush() : _a(1),", " _b(2)", " {}", "};", @@ -348,6 +348,29 @@ def testNamespaceIndentationMemberInitializerList(self): ] assert self.GetNamespaceResults(lines) == "" + # Inline + multiline, ends on the same line as a list's end + lines = [ + "namespace Humpety {", + "Dumpety::Dumpety(const int a)", + " : number(a),", + " looooooong_attr1(0),", + " looooooong_attr2(0),", + " looooooong_attr3(0),", + " looooooong_attr4(0) {}", + ] + assert self.GetNamespaceResults(lines) == "" + + # Uniform initialization + lines = [ + "namespace Unifor {", + "class MIA {", + ' private MIA() : cali({"nia"}),', + " parme({3}),", + ' carne{"val", ipsmo}', + " gat(o) {}", + ] + assert self.GetNamespaceResults(lines) == "" + # Same line as constructor declaration lines = [ "namespace Boucher {", @@ -363,7 +386,7 @@ def testNamespaceIndentationMemberInitializerList(self): lines = [ "namespace Store {", "", - " Color::Color() : my_name_is('b')", + " Color::Color() : my_name_is('b'),", " this_is_true(true) {", ] assert (