From aacde5032d801a03e4250c108ddadb22bd9fe119 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 23 Mar 2020 01:46:29 +0900 Subject: [PATCH 01/13] Fix #7364: autosummary: crashed when autosummary_generate is False --- CHANGES | 2 ++ sphinx/ext/autosummary/__init__.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index 710ca51da..272bd29b1 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,8 @@ Features added Bugs fixed ---------- +* #7364: autosummary: crashed when :confval:`autosummary_generate` is False + Testing -------- diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 9c550c622..ee44e77d3 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -722,6 +722,8 @@ def process_generate_options(app: Sphinx) -> None: env = app.builder.env genfiles = [env.doc2path(x, base=None) for x in env.found_docs if os.path.isfile(env.doc2path(x))] + elif genfiles is False: + pass else: ext = list(app.config.source_suffix) genfiles = [genfile + (ext[0] if not genfile.endswith(tuple(ext)) else '') From 263b7a021c2853d0fde9a1a2604bd49d929c0294 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Wed, 25 Mar 2020 00:25:03 +0900 Subject: [PATCH 02/13] Fix #7370: autosummary: raises UnboundLocalError when unknown module given --- CHANGES | 2 ++ sphinx/ext/autosummary/generate.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 710ca51da..4d99e2c59 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,8 @@ Features added Bugs fixed ---------- +* #7370: autosummary: raises UnboundLocalError when unknown module given + Testing -------- diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index 5a87e4abf..932856aa6 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -279,7 +279,7 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None, try: name, obj, parent, mod_name = import_by_name(entry.name) except ImportError as e: - _warn(__('[autosummary] failed to import %r: %s') % (name, e)) + _warn(__('[autosummary] failed to import %r: %s') % (entry.name, e)) continue content = generate_autosummary_content(name, obj, parent, template, entry.template, From 077df4a0848b6c791c83215af34dddbc13517b34 Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Tue, 24 Mar 2020 17:26:02 +0100 Subject: [PATCH 03/13] C++, support alternate spellings of operators Fixes sphinx-doc/sphinx#7367 --- CHANGES | 1 + sphinx/domains/cpp.py | 76 +++++++++++++++++++++------------- tests/test_domain_cpp.py | 89 +++++++++++++++++++++++++++++----------- 3 files changed, 115 insertions(+), 51 deletions(-) diff --git a/CHANGES b/CHANGES index 4d99e2c59..652db65ab 100644 --- a/CHANGES +++ b/CHANGES @@ -17,6 +17,7 @@ Bugs fixed ---------- * #7370: autosummary: raises UnboundLocalError when unknown module given +* #7367: C++, alternate operator spellings are now supported. Testing -------- diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 7f7558cb3..b5d9d7b76 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -306,6 +306,7 @@ _operator_re = re.compile(r'''(?x) | ->\*? | \, | (<<|>>)=? | && | \|\| | [!<>=/*%+|&^~-]=? + | (\b(and|and_eq|bitand|bitor|compl|not|not_eq|or|or_eq|xor|xor_eq)\b) ''') _fold_operator_re = re.compile(r'''(?x) ->\* | \.\* | \, @@ -464,37 +465,37 @@ _id_operator_v2 = { # '-(unary)' : 'ng', # '&(unary)' : 'ad', # '*(unary)' : 'de', - '~': 'co', + '~': 'co', 'compl': 'co', '+': 'pl', '-': 'mi', '*': 'ml', '/': 'dv', '%': 'rm', - '&': 'an', - '|': 'or', - '^': 'eo', + '&': 'an', 'bitand': 'an', + '|': 'or', 'bitor': 'or', + '^': 'eo', 'xor': 'eo', '=': 'aS', '+=': 'pL', '-=': 'mI', '*=': 'mL', '/=': 'dV', '%=': 'rM', - '&=': 'aN', - '|=': 'oR', - '^=': 'eO', + '&=': 'aN', 'and_eq': 'aN', + '|=': 'oR', 'or_eq': 'oR', + '^=': 'eO', 'xor_eq': 'eO', '<<': 'ls', '>>': 'rs', '<<=': 'lS', '>>=': 'rS', '==': 'eq', - '!=': 'ne', + '!=': 'ne', 'not_eq': 'ne', '<': 'lt', '>': 'gt', '<=': 'le', '>=': 'ge', - '!': 'nt', - '&&': 'aa', - '||': 'oo', + '!': 'nt', 'not': 'nt', + '&&': 'aa', 'and': 'aa', + '||': 'oo', 'or': 'oo', '++': 'pp', '--': 'mm', ',': 'cm', @@ -511,8 +512,8 @@ _id_operator_unary_v2 = { '&': 'ad', '+': 'ps', '-': 'ng', - '!': 'nt', - '~': 'co' + '!': 'nt', 'not': 'nt', + '~': 'co', 'compl': 'co' } _id_char_from_prefix = { None: 'c', 'u8': 'c', @@ -520,21 +521,21 @@ _id_char_from_prefix = { } # type: Dict[Any, str] # these are ordered by preceedence _expression_bin_ops = [ - ['||'], - ['&&'], - ['|'], - ['^'], - ['&'], - ['==', '!='], + ['||', 'or'], + ['&&', 'and'], + ['|', 'bitor'], + ['^', 'xor'], + ['&', 'bitand'], + ['==', '!=', 'not_eq'], ['<=', '>=', '<', '>'], ['<<', '>>'], ['+', '-'], ['*', '/', '%'], ['.*', '->*'] ] -_expression_unary_ops = ["++", "--", "*", "&", "+", "-", "!", "~"] +_expression_unary_ops = ["++", "--", "*", "&", "+", "-", "!", "not", "~", "compl"] _expression_assignment_ops = ["=", "*=", "/=", "%=", "+=", "-=", - ">>=", "<<=", "&=", "^=", "|="] + ">>=", "<<=", "&=", "and_eq", "^=", "|=", "xor_eq", "or_eq"] _id_explicit_cast = { 'dynamic_cast': 'dc', 'static_cast': 'sc', @@ -1260,7 +1261,10 @@ class ASTUnaryOpExpr(ASTExpression): self.expr = expr def _stringify(self, transform: StringifyTransform) -> str: - return transform(self.op) + transform(self.expr) + if self.op[0] in 'cn': + return transform(self.op) + " " + transform(self.expr) + else: + return transform(self.op) + transform(self.expr) def get_id(self, version: int) -> str: return _id_operator_unary_v2[self.op] + self.expr.get_id(version) @@ -1268,6 +1272,8 @@ class ASTUnaryOpExpr(ASTExpression): def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: signode.append(nodes.Text(self.op)) + if self.op[0] in 'cn': + signode.append(nodes.Text(' ')) self.expr.describe_signature(signode, mode, env, symbol) @@ -1584,6 +1590,8 @@ class ASTOperatorBuildIn(ASTOperator): def get_id(self, version: int) -> str: if version == 1: ids = _id_operator_v1 + if self.op not in ids: + raise NoOldIdError() else: ids = _id_operator_v2 if self.op not in ids: @@ -1592,7 +1600,7 @@ class ASTOperatorBuildIn(ASTOperator): return ids[self.op] def _stringify(self, transform: StringifyTransform) -> str: - if self.op in ('new', 'new[]', 'delete', 'delete[]'): + if self.op in ('new', 'new[]', 'delete', 'delete[]') or self.op[0] in "abcnox": return 'operator ' + self.op else: return 'operator' + self.op @@ -5016,7 +5024,11 @@ class DefinitionParser(BaseParser): self.skip_ws() for op in _expression_unary_ops: # TODO: hmm, should we be able to backtrack here? - if self.skip_string(op): + if op[0] in 'cn': + res = self.skip_word(op) + else: + res = self.skip_string(op) + if res: expr = self._parse_cast_expression() return ASTUnaryOpExpr(op, expr) if self.skip_word_and_ws('sizeof'): @@ -5144,8 +5156,12 @@ class DefinitionParser(BaseParser): pos = self.pos oneMore = False for op in _expression_bin_ops[opId]: - if not self.skip_string(op): - continue + if op[0] in 'abcnox': + if not self.skip_word(op): + continue + else: + if not self.skip_string(op): + continue if op == '&' and self.current_char == '&': # don't split the && 'token' self.pos -= 1 @@ -5187,8 +5203,12 @@ class DefinitionParser(BaseParser): oneMore = False self.skip_ws() for op in _expression_assignment_ops: - if not self.skip_string(op): - continue + if op[0] in 'anox': + if not self.skip_word(op): + continue + else: + if not self.skip_string(op): + continue expr = self._parse_logical_or_expression(False) exprs.append(expr) ops.append(op) diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py index aa8bb97b2..c6d0df8aa 100644 --- a/tests/test_domain_cpp.py +++ b/tests/test_domain_cpp.py @@ -197,7 +197,9 @@ def test_expressions(): exprCheck('+5', 'psL5E') exprCheck('-5', 'ngL5E') exprCheck('!5', 'ntL5E') + exprCheck('not 5', 'ntL5E') exprCheck('~5', 'coL5E') + exprCheck('compl 5', 'coL5E') exprCheck('sizeof...(a)', 'sZ1a') exprCheck('sizeof(T)', 'st1T') exprCheck('sizeof -42', 'szngL42E') @@ -221,13 +223,19 @@ def test_expressions(): exprCheck('(int)2', 'cviL2E') # binary op exprCheck('5 || 42', 'ooL5EL42E') + exprCheck('5 or 42', 'ooL5EL42E') exprCheck('5 && 42', 'aaL5EL42E') + exprCheck('5 and 42', 'aaL5EL42E') exprCheck('5 | 42', 'orL5EL42E') + exprCheck('5 bitor 42', 'orL5EL42E') exprCheck('5 ^ 42', 'eoL5EL42E') + exprCheck('5 xor 42', 'eoL5EL42E') exprCheck('5 & 42', 'anL5EL42E') + exprCheck('5 bitand 42', 'anL5EL42E') # ['==', '!='] exprCheck('5 == 42', 'eqL5EL42E') exprCheck('5 != 42', 'neL5EL42E') + exprCheck('5 not_eq 42', 'neL5EL42E') # ['<=', '>=', '<', '>'] exprCheck('5 <= 42', 'leL5EL42E') exprCheck('A <= 42', 'le1AL42E') @@ -261,8 +269,11 @@ def test_expressions(): exprCheck('a >>= 5', 'rS1aL5E') exprCheck('a <<= 5', 'lS1aL5E') exprCheck('a &= 5', 'aN1aL5E') + exprCheck('a and_eq 5', 'aN1aL5E') exprCheck('a ^= 5', 'eO1aL5E') + exprCheck('a xor_eq 5', 'eO1aL5E') exprCheck('a |= 5', 'oR1aL5E') + exprCheck('a or_eq 5', 'oR1aL5E') # Additional tests # a < expression that starts with something that could be a template @@ -531,30 +542,62 @@ def test_function_definitions(): def test_operators(): - check('function', 'void operator new [ ] ()', - {1: "new-array-operator", 2: "nav"}, output='void operator new[]()') - check('function', 'void operator delete ()', - {1: "delete-operator", 2: "dlv"}, output='void operator delete()') - check('function', 'operator bool() const', - {1: "castto-b-operatorC", 2: "NKcvbEv"}, output='operator bool() const') + check('function', 'void operator new()', {1: "new-operator", 2: "nwv"}) + check('function', 'void operator new[]()', {1: "new-array-operator", 2: "nav"}) + check('function', 'void operator delete()', {1: "delete-operator", 2: "dlv"}) + check('function', 'void operator delete[]()', {1: "delete-array-operator", 2: "dav"}) + check('function', 'operator bool() const', {1: "castto-b-operatorC", 2: "NKcvbEv"}) + check('function', 'void operator""_udl()', {2: 'li4_udlv'}) - check('function', 'void operator * ()', - {1: "mul-operator", 2: "mlv"}, output='void operator*()') - check('function', 'void operator - ()', - {1: "sub-operator", 2: "miv"}, output='void operator-()') - check('function', 'void operator + ()', - {1: "add-operator", 2: "plv"}, output='void operator+()') - check('function', 'void operator = ()', - {1: "assign-operator", 2: "aSv"}, output='void operator=()') - check('function', 'void operator / ()', - {1: "div-operator", 2: "dvv"}, output='void operator/()') - check('function', 'void operator % ()', - {1: "mod-operator", 2: "rmv"}, output='void operator%()') - check('function', 'void operator ! ()', - {1: "not-operator", 2: "ntv"}, output='void operator!()') - - check('function', 'void operator "" _udl()', - {2: 'li4_udlv'}, output='void operator""_udl()') + check('function', 'void operator~()', {1: "inv-operator", 2: "cov"}) + check('function', 'void operator compl()', {2: "cov"}) + check('function', 'void operator+()', {1: "add-operator", 2: "plv"}) + check('function', 'void operator-()', {1: "sub-operator", 2: "miv"}) + check('function', 'void operator*()', {1: "mul-operator", 2: "mlv"}) + check('function', 'void operator/()', {1: "div-operator", 2: "dvv"}) + check('function', 'void operator%()', {1: "mod-operator", 2: "rmv"}) + check('function', 'void operator&()', {1: "and-operator", 2: "anv"}) + check('function', 'void operator bitand()', {2: "anv"}) + check('function', 'void operator|()', {1: "or-operator", 2: "orv"}) + check('function', 'void operator bitor()', {2: "orv"}) + check('function', 'void operator^()', {1: "xor-operator", 2: "eov"}) + check('function', 'void operator xor()', {2: "eov"}) + check('function', 'void operator=()', {1: "assign-operator", 2: "aSv"}) + check('function', 'void operator+=()', {1: "add-assign-operator", 2: "pLv"}) + check('function', 'void operator-=()', {1: "sub-assign-operator", 2: "mIv"}) + check('function', 'void operator*=()', {1: "mul-assign-operator", 2: "mLv"}) + check('function', 'void operator/=()', {1: "div-assign-operator", 2: "dVv"}) + check('function', 'void operator%=()', {1: "mod-assign-operator", 2: "rMv"}) + check('function', 'void operator&=()', {1: "and-assign-operator", 2: "aNv"}) + check('function', 'void operator and_eq()', {2: "aNv"}) + check('function', 'void operator|=()', {1: "or-assign-operator", 2: "oRv"}) + check('function', 'void operator or_eq()', {2: "oRv"}) + check('function', 'void operator^=()', {1: "xor-assign-operator", 2: "eOv"}) + check('function', 'void operator xor_eq()', {2: "eOv"}) + check('function', 'void operator<<()', {1: "lshift-operator", 2: "lsv"}) + check('function', 'void operator>>()', {1: "rshift-operator", 2: "rsv"}) + check('function', 'void operator<<=()', {1: "lshift-assign-operator", 2: "lSv"}) + check('function', 'void operator>>=()', {1: "rshift-assign-operator", 2: "rSv"}) + check('function', 'void operator==()', {1: "eq-operator", 2: "eqv"}) + check('function', 'void operator!=()', {1: "neq-operator", 2: "nev"}) + check('function', 'void operator not_eq()', {2: "nev"}) + check('function', 'void operator<()', {1: "lt-operator", 2: "ltv"}) + check('function', 'void operator>()', {1: "gt-operator", 2: "gtv"}) + check('function', 'void operator<=()', {1: "lte-operator", 2: "lev"}) + check('function', 'void operator>=()', {1: "gte-operator", 2: "gev"}) + check('function', 'void operator!()', {1: "not-operator", 2: "ntv"}) + check('function', 'void operator not()', {2: "ntv"}) + check('function', 'void operator&&()', {1: "sand-operator", 2: "aav"}) + check('function', 'void operator and()', {2: "aav"}) + check('function', 'void operator||()', {1: "sor-operator", 2: "oov"}) + check('function', 'void operator or()', {2: "oov"}) + check('function', 'void operator++()', {1: "inc-operator", 2: "ppv"}) + check('function', 'void operator--()', {1: "dec-operator", 2: "mmv"}) + check('function', 'void operator,()', {1: "comma-operator", 2: "cmv"}) + check('function', 'void operator->*()', {1: "pointer-by-pointer-operator", 2: "pmv"}) + check('function', 'void operator->()', {1: "pointer-operator", 2: "ptv"}) + check('function', 'void operator()()', {1: "call-operator", 2: "clv"}) + check('function', 'void operator[]()', {1: "subscript-operator", 2: "ixv"}) def test_class_definitions(): From c6f1bf66e0adbe003a21b7d3fc480fbc184bf204 Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Tue, 24 Mar 2020 18:38:42 +0100 Subject: [PATCH 04/13] C, alternative spellings of operators --- CHANGES | 1 + sphinx/domains/c.py | 45 +++++++++++++++++++++++++++++------------- tests/test_domain_c.py | 11 +++++++++++ 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/CHANGES b/CHANGES index 2b6a8f47b..a75d24e2e 100644 --- a/CHANGES +++ b/CHANGES @@ -19,6 +19,7 @@ Bugs fixed * #7364: autosummary: crashed when :confval:`autosummary_generate` is False * #7370: autosummary: raises UnboundLocalError when unknown module given * #7367: C++, alternate operator spellings are now supported. +* C, alternate operator spellings are now supported. Testing -------- diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index 8f071a9aa..3f98b7261 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -53,21 +53,21 @@ _keywords = [ # these are ordered by preceedence _expression_bin_ops = [ - ['||'], - ['&&'], - ['|'], - ['^'], - ['&'], - ['==', '!='], + ['||', 'or'], + ['&&', 'and'], + ['|', 'bitor'], + ['^', 'xor'], + ['&', 'bitand'], + ['==', '!=', 'not_eq'], ['<=', '>=', '<', '>'], ['<<', '>>'], ['+', '-'], ['*', '/', '%'], ['.*', '->*'] ] -_expression_unary_ops = ["++", "--", "*", "&", "+", "-", "!", "~"] +_expression_unary_ops = ["++", "--", "*", "&", "+", "-", "!", "not", "~", "compl"] _expression_assignment_ops = ["=", "*=", "/=", "%=", "+=", "-=", - ">>=", "<<=", "&=", "^=", "|="] + ">>=", "<<=", "&=", "and_eq", "^=", "xor_eq", "|=", "or_eq"] _max_id = 1 _id_prefix = [None, 'c.', 'Cv2.'] @@ -423,11 +423,16 @@ class ASTUnaryOpExpr(ASTExpression): self.expr = expr def _stringify(self, transform: StringifyTransform) -> str: - return transform(self.op) + transform(self.expr) + if self.op[0] in 'cn': + return transform(self.op) + " " + transform(self.expr) + else: + return transform(self.op) + transform(self.expr) def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: signode.append(nodes.Text(self.op)) + if self.op[0] in 'cn': + signode.append(nodes.Text(" ")) self.expr.describe_signature(signode, mode, env, symbol) @@ -2241,7 +2246,11 @@ class DefinitionParser(BaseParser): self.skip_ws() for op in _expression_unary_ops: # TODO: hmm, should we be able to backtrack here? - if self.skip_string(op): + if op[0] in 'cn': + res = self.skip_word(op) + else: + res = self.skip_string(op) + if res: expr = self._parse_cast_expression() return ASTUnaryOpExpr(op, expr) if self.skip_word_and_ws('sizeof'): @@ -2313,8 +2322,12 @@ class DefinitionParser(BaseParser): pos = self.pos oneMore = False for op in _expression_bin_ops[opId]: - if not self.skip_string(op): - continue + if op[0] in 'abcnox': + if not self.skip_word(op): + continue + else: + if not self.skip_string(op): + continue if op == '&' and self.current_char == '&': # don't split the && 'token' self.pos -= 1 @@ -2353,8 +2366,12 @@ class DefinitionParser(BaseParser): oneMore = False self.skip_ws() for op in _expression_assignment_ops: - if not self.skip_string(op): - continue + if op[0] in 'abcnox': + if not self.skip_word(op): + continue + else: + if not self.skip_string(op): + continue expr = self._parse_logical_or_expression() exprs.append(expr) ops.append(op) diff --git a/tests/test_domain_c.py b/tests/test_domain_c.py index 6d4ba9cea..9daed2800 100644 --- a/tests/test_domain_c.py +++ b/tests/test_domain_c.py @@ -172,7 +172,9 @@ def test_expressions(): exprCheck('+5') exprCheck('-5') exprCheck('!5') + exprCheck('not 5') exprCheck('~5') + exprCheck('compl 5') exprCheck('sizeof(T)') exprCheck('sizeof -42') exprCheck('alignof(T)') @@ -180,13 +182,19 @@ def test_expressions(): exprCheck('(int)2') # binary op exprCheck('5 || 42') + exprCheck('5 or 42') exprCheck('5 && 42') + exprCheck('5 and 42') exprCheck('5 | 42') + exprCheck('5 bitor 42') exprCheck('5 ^ 42') + exprCheck('5 xor 42') exprCheck('5 & 42') + exprCheck('5 bitand 42') # ['==', '!='] exprCheck('5 == 42') exprCheck('5 != 42') + exprCheck('5 not_eq 42') # ['<=', '>=', '<', '>'] exprCheck('5 <= 42') exprCheck('5 >= 42') @@ -215,8 +223,11 @@ def test_expressions(): exprCheck('a >>= 5') exprCheck('a <<= 5') exprCheck('a &= 5') + exprCheck('a and_eq 5') exprCheck('a ^= 5') + exprCheck('a xor_eq 5') exprCheck('a |= 5') + exprCheck('a or_eq 5') def test_type_definitions(): From 1795cda56de3d641a050538a7f8e332136f3284b Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Tue, 24 Mar 2020 20:37:24 +0100 Subject: [PATCH 05/13] C, C++, improve error messages. --- sphinx/domains/c.py | 16 +++++++++++++--- sphinx/domains/cpp.py | 27 ++++++++++++++++----------- sphinx/util/cfamily.py | 12 ++++++++---- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index 3f98b7261..5360b3a9b 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -1986,6 +1986,10 @@ class DefinitionParser(BaseParser): _prefix_keys = ('struct', 'enum', 'union') + @property + def language(self) -> str: + return 'C' + def _parse_string(self) -> str: if self.current_char != '"': return None @@ -2697,7 +2701,7 @@ class DefinitionParser(BaseParser): restrict=restrict, volatile=volatile, const=const, attrs=attrs) if typed and self.current_char == '(': # note: peeking, not skipping - # maybe this is the beginning of params,try that first, + # maybe this is the beginning of params, try that first, # otherwise assume it's noptr->declarator > ( ptr-declarator ) pos = self.pos try: @@ -2706,7 +2710,10 @@ class DefinitionParser(BaseParser): typed) return res except DefinitionError as exParamQual: - prevErrors.append((exParamQual, "If declId and parameters")) + msg = "If declarator-id with parameters" + if paramMode == 'function': + msg += " (e.g., 'void f(int arg)')" + prevErrors.append((exParamQual, msg)) self.pos = pos try: assert self.current_char == '(' @@ -2723,7 +2730,10 @@ class DefinitionParser(BaseParser): return ASTDeclaratorParen(inner=inner, next=next) except DefinitionError as exNoPtrParen: self.pos = pos - prevErrors.append((exNoPtrParen, "If parenthesis in noptr-declarator")) + msg = "If parenthesis in noptr-declarator" + if paramMode == 'function': + msg += " (e.g., 'void (*f(int arg))(double)')" + prevErrors.append((exNoPtrParen, msg)) header = "Error in declarator" raise self._make_multi_error(prevErrors, header) pos = self.pos diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index b5d9d7b76..51bd41754 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -4576,6 +4576,10 @@ class DefinitionParser(BaseParser): super().__init__(definition, location=location) self.config = config + @property + def language(self) -> str: + return 'C++' + def _parse_string(self) -> str: if self.current_char != '"': return None @@ -5470,7 +5474,7 @@ class DefinitionParser(BaseParser): self.skip_ws() if not self.skip_string('('): if paramMode == 'function': - self.fail('Expecting "(" in parameters_and_qualifiers.') + self.fail('Expecting "(" in parameters-and-qualifiers.') else: return None args = [] @@ -5483,7 +5487,7 @@ class DefinitionParser(BaseParser): self.skip_ws() if not self.skip_string(')'): self.fail('Expected ")" after "..." in ' - 'parameters_and_qualifiers.') + 'parameters-and-qualifiers.') break # note: it seems that function arguments can always be named, # even in function pointers and similar. @@ -5498,7 +5502,7 @@ class DefinitionParser(BaseParser): break else: self.fail( - 'Expecting "," or ")" in parameters_and_qualifiers, ' + 'Expecting "," or ")" in parameters-and-qualifiers, ' 'got "%s".' % self.current_char) # TODO: why did we have this bail-out? @@ -5529,7 +5533,7 @@ class DefinitionParser(BaseParser): exceptionSpec = 'noexcept' self.skip_ws() if self.skip_string('('): - self.fail('Parameterised "noexcept" not implemented.') + self.fail('Parameterised "noexcept" not yet implemented.') self.skip_ws() override = self.skip_word_and_ws('override') @@ -5795,14 +5799,15 @@ class DefinitionParser(BaseParser): typed) return res except DefinitionError as exParamQual: - prevErrors.append((exParamQual, "If declId, parameters, and qualifiers")) + prevErrors.append((exParamQual, + "If declarator-id with parameters-and-qualifiers")) self.pos = pos try: assert self.current_char == '(' self.skip_string('(') # TODO: hmm, if there is a name, it must be in inner, right? - # TODO: hmm, if there must be parameters, they must b - # inside, right? + # TODO: hmm, if there must be parameters, they must be + # inside, right? inner = self._parse_declarator(named, paramMode, typed) if not self.skip_string(')'): self.fail("Expected ')' in \"( ptr-declarator )\"") @@ -5821,7 +5826,7 @@ class DefinitionParser(BaseParser): except DefinitionError as e: self.pos = pos prevErrors.append((e, "If declarator-id")) - header = "Error in declarator or parameters and qualifiers" + header = "Error in declarator or parameters-and-qualifiers" raise self._make_multi_error(prevErrors, header) def _parse_initializer(self, outer: str = None, allowFallback: bool = True @@ -5994,10 +5999,10 @@ class DefinitionParser(BaseParser): if eExpr is None: raise eType errs = [] - errs.append((eExpr, "If default is an expression")) - errs.append((eType, "If default is a type")) + errs.append((eExpr, "If default template argument is an expression")) + errs.append((eType, "If default template argument is a type")) msg = "Error in non-type template parameter" - msg += " or constrianted template paramter." + msg += " or constrained template parameter." raise self._make_multi_error(errs, msg) def _parse_type_using(self) -> ASTTypeUsing: diff --git a/sphinx/util/cfamily.py b/sphinx/util/cfamily.py index 516221cf9..73bc62d6f 100644 --- a/sphinx/util/cfamily.py +++ b/sphinx/util/cfamily.py @@ -154,19 +154,23 @@ class BaseParser: result = [header, '\n'] for e in errors: if len(e[1]) > 0: - ident = ' ' + indent = ' ' result.append(e[1]) result.append(':\n') for line in str(e[0]).split('\n'): if len(line) == 0: continue - result.append(ident) + result.append(indent) result.append(line) result.append('\n') else: result.append(str(e[0])) return DefinitionError(''.join(result)) + @property + def language(self) -> str: + raise NotImplementedError + def status(self, msg: str) -> None: # for debugging indicator = '-' * self.pos + '^' @@ -176,8 +180,8 @@ class BaseParser: errors = [] indicator = '-' * self.pos + '^' exMain = DefinitionError( - 'Invalid definition: %s [error at %d]\n %s\n %s' % - (msg, self.pos, self.definition, indicator)) + 'Invalid %s declaration: %s [error at %d]\n %s\n %s' % + (self.language, msg, self.pos, self.definition, indicator)) errors.append((exMain, "Main error")) for err in self.otherErrors: errors.append((err, "Potential other error")) From 1ab1e1c324fd0abc1aa4db28f62d0a30faf2be3f Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Tue, 24 Mar 2020 23:25:19 +0100 Subject: [PATCH 06/13] C++, improve error messages for template parameter/argument lists --- sphinx/domains/cpp.py | 72 +++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index b5d9d7b76..cdcb0d3b3 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -5334,13 +5334,7 @@ class DefinitionParser(BaseParser): prevErrors.append((e, "If type argument")) self.pos = pos try: - # actually here we shouldn't use the fallback parser (hence allow=False), - # because if actually took the < in an expression, then we _will_ fail, - # which is handled elsewhere. E.g., :cpp:expr:`A <= 0`. - def parser(): - return self._parse_constant_expression(inTemplate=True) - value = self._parse_expression_fallback( - [',', '>'], parser, allow=False) + value = self._parse_constant_expression(inTemplate=True) self.skip_ws() if self.skip_string('>'): parsedEnd = True @@ -5460,7 +5454,6 @@ class DefinitionParser(BaseParser): if self.skip_word_and_ws(k): prefix = k break - nestedName = self._parse_nested_name() return ASTTrailingTypeSpecName(prefix, nestedName) @@ -5754,32 +5747,6 @@ class DefinitionParser(BaseParser): if typed and self.skip_string("..."): next = self._parse_declarator(named, paramMode, False) return ASTDeclaratorParamPack(next=next) - if typed: # pointer to member - pos = self.pos - try: - name = self._parse_nested_name(memberPointer=True) - self.skip_ws() - if not self.skip_string('*'): - self.fail("Expected '*' in pointer to member declarator.") - self.skip_ws() - except DefinitionError as e: - self.pos = pos - prevErrors.append((e, "If pointer to member declarator")) - else: - volatile = False - const = False - while 1: - if not volatile: - volatile = self.skip_word_and_ws('volatile') - if volatile: - continue - if not const: - const = self.skip_word_and_ws('const') - if const: - continue - break - next = self._parse_declarator(named, paramMode, typed) - return ASTDeclaratorMemPtr(name, const, volatile, next=next) if typed and self.current_char == '(': # note: peeking, not skipping if paramMode == "operatorCast": # TODO: we should be able to parse cast operators which return @@ -5815,9 +5782,40 @@ class DefinitionParser(BaseParser): prevErrors.append((exNoPtrParen, "If parenthesis in noptr-declarator")) header = "Error in declarator" raise self._make_multi_error(prevErrors, header) + if typed: # pointer to member + pos = self.pos + try: + name = self._parse_nested_name(memberPointer=True) + self.skip_ws() + if not self.skip_string('*'): + self.fail("Expected '*' in pointer to member declarator.") + self.skip_ws() + except DefinitionError as e: + self.pos = pos + prevErrors.append((e, "If pointer to member declarator")) + else: + volatile = False + const = False + while 1: + if not volatile: + volatile = self.skip_word_and_ws('volatile') + if volatile: + continue + if not const: + const = self.skip_word_and_ws('const') + if const: + continue + break + next = self._parse_declarator(named, paramMode, typed) + return ASTDeclaratorMemPtr(name, const, volatile, next=next) pos = self.pos try: - return self._parse_declarator_name_suffix(named, paramMode, typed) + res = self._parse_declarator_name_suffix(named, paramMode, typed) + # this is a heuristic for error messages, for when there is a < after a + # nested name, but it was not a successful template argument list + if self.current_char == '<': + self.otherErrors.append(self._make_multi_error(prevErrors, "")) + return res except DefinitionError as e: self.pos = pos prevErrors.append((e, "If declarator-id")) @@ -5886,7 +5884,6 @@ class DefinitionParser(BaseParser): raise Exception('Internal error, unknown outer "%s".' % outer) if outer != 'operatorCast': assert named - if outer in ('type', 'function'): # We allow type objects to just be a name. # Some functions don't have normal return types: constructors, @@ -6080,8 +6077,8 @@ class DefinitionParser(BaseParser): self.skip_ws() if not self.skip_string("<"): self.fail("Expected '<' after 'template'") + prevErrors = [] while 1: - prevErrors = [] self.skip_ws() if self.skip_word('template'): # declare a tenplate template parameter @@ -6134,6 +6131,7 @@ class DefinitionParser(BaseParser): if self.skip_string('>'): return ASTTemplateParams(templateParams) elif self.skip_string(','): + prevErrors = [] continue else: header = "Error in template parameter list." From f9d36008e1d2260422945a612851ae51f2a1b4e8 Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Wed, 25 Mar 2020 11:00:24 +0100 Subject: [PATCH 07/13] C++, support comma operator in expressions --- sphinx/domains/cpp.py | 138 +++++++++++++++++++++++++-------------- tests/test_domain_cpp.py | 3 + 2 files changed, 93 insertions(+), 48 deletions(-) diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index cdcb0d3b3..2b4478820 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -1505,8 +1505,38 @@ class ASTBinOpExpr(ASTExpression): self.exprs[i].describe_signature(signode, mode, env, symbol) +class ASTBracedInitList(ASTBase): + def __init__(self, exprs: List[Union[ASTExpression, "ASTBracedInitList"]], + trailingComma: bool) -> None: + self.exprs = exprs + self.trailingComma = trailingComma + + def get_id(self, version: int) -> str: + return "il%sE" % ''.join(e.get_id(version) for e in self.exprs) + + def _stringify(self, transform: StringifyTransform) -> str: + exprs = [transform(e) for e in self.exprs] + trailingComma = ',' if self.trailingComma else '' + return '{%s%s}' % (', '.join(exprs), trailingComma) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + verify_description_mode(mode) + signode.append(nodes.Text('{')) + first = True + for e in self.exprs: + if not first: + signode.append(nodes.Text(', ')) + else: + first = False + e.describe_signature(signode, mode, env, symbol) + if self.trailingComma: + signode.append(nodes.Text(',')) + signode.append(nodes.Text('}')) + + class ASTAssignmentExpr(ASTExpression): - def __init__(self, exprs: List[ASTExpression], ops: List[str]): + def __init__(self, exprs: List[Union[ASTExpression, ASTBracedInitList]], ops: List[str]): assert len(exprs) > 0 assert len(exprs) == len(ops) + 1 self.exprs = exprs @@ -1540,6 +1570,31 @@ class ASTAssignmentExpr(ASTExpression): self.exprs[i].describe_signature(signode, mode, env, symbol) +class ASTCommaExpr(ASTExpression): + def __init__(self, exprs: List[ASTExpression]): + assert len(exprs) > 0 + self.exprs = exprs + + def _stringify(self, transform: StringifyTransform) -> str: + return ', '.join(transform(e) for e in self.exprs) + + def get_id(self, version: int) -> str: + id_ = _id_operator_v2[','] + res = [] + for i in range(len(self.exprs) - 1): + res.append(id_) + res.append(self.exprs[i].get_id(version)) + res.append(self.exprs[-1].get_id(version)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: "BuildEnvironment", symbol: "Symbol") -> None: + self.exprs[0].describe_signature(signode, mode, env, symbol) + for i in range(1, len(self.exprs)): + signode.append(nodes.Text(', ')) + self.exprs[i].describe_signature(signode, mode, env, symbol) + + class ASTFallbackExpr(ASTExpression): def __init__(self, expr: str): self.expr = expr @@ -2641,7 +2696,7 @@ class ASTDeclaratorParen(ASTDeclarator): ############################################################################################## class ASTPackExpansionExpr(ASTExpression): - def __init__(self, expr: ASTExpression): + def __init__(self, expr: Union[ASTExpression, ASTBracedInitList]): self.expr = expr def _stringify(self, transform: StringifyTransform) -> str: @@ -2658,7 +2713,7 @@ class ASTPackExpansionExpr(ASTExpression): class ASTParenExprList(ASTBase): - def __init__(self, exprs: List[ASTExpression]) -> None: + def __init__(self, exprs: List[Union[ASTExpression, ASTBracedInitList]]) -> None: self.exprs = exprs def get_id(self, version: int) -> str: @@ -2682,35 +2737,6 @@ class ASTParenExprList(ASTBase): signode.append(nodes.Text(')')) -class ASTBracedInitList(ASTBase): - def __init__(self, exprs: List[ASTExpression], trailingComma: bool) -> None: - self.exprs = exprs - self.trailingComma = trailingComma - - def get_id(self, version: int) -> str: - return "il%sE" % ''.join(e.get_id(version) for e in self.exprs) - - def _stringify(self, transform: StringifyTransform) -> str: - exprs = [transform(e) for e in self.exprs] - trailingComma = ',' if self.trailingComma else '' - return '{%s%s}' % (', '.join(exprs), trailingComma) - - def describe_signature(self, signode: TextElement, mode: str, - env: "BuildEnvironment", symbol: "Symbol") -> None: - verify_description_mode(mode) - signode.append(nodes.Text('{')) - first = True - for e in self.exprs: - if not first: - signode.append(nodes.Text(', ')) - else: - first = False - e.describe_signature(signode, mode, env, symbol) - if self.trailingComma: - signode.append(nodes.Text(',')) - signode.append(nodes.Text('}')) - - class ASTInitializer(ASTBase): def __init__(self, value: Union[ASTExpression, ASTBracedInitList], hasAssign: bool = True) -> None: @@ -4751,7 +4777,7 @@ class DefinitionParser(BaseParser): self.pos = pos # fall back to a paren expression try: - res = self._parse_expression(inTemplate=False) + res = self._parse_expression() self.skip_ws() if not self.skip_string(')'): self.fail("Expected ')' in end of parenthesized expression.") @@ -4799,7 +4825,9 @@ class DefinitionParser(BaseParser): return None def _parse_initializer_list(self, name: str, open: str, close: str - ) -> Tuple[List[ASTExpression], bool]: + ) -> Tuple[List[Union[ASTExpression, + ASTBracedInitList]], + bool]: # Parse open and close with the actual initializer-list inbetween # -> initializer-clause '...'[opt] # | initializer-list ',' initializer-clause '...'[opt] @@ -4809,11 +4837,11 @@ class DefinitionParser(BaseParser): if self.skip_string(close): return [], False - exprs = [] # type: List[ASTExpression] + exprs = [] # type: List[Union[ASTExpression, ASTBracedInitList]] trailingComma = False while True: self.skip_ws() - expr = self._parse_expression(inTemplate=False) + expr = self._parse_initializer_clause() self.skip_ws() if self.skip_string('...'): exprs.append(ASTPackExpansionExpr(expr)) @@ -4843,6 +4871,12 @@ class DefinitionParser(BaseParser): return None return ASTParenExprList(exprs) + def _parse_initializer_clause(self) -> Union[ASTExpression, ASTBracedInitList]: + bracedInitList = self._parse_braced_init_list() + if bracedInitList is not None: + return bracedInitList + return self._parse_assignment_expression(inTemplate=False) + def _parse_braced_init_list(self) -> ASTBracedInitList: # -> '{' initializer-list ','[opt] '}' # | '{' '}' @@ -4902,7 +4936,7 @@ class DefinitionParser(BaseParser): self.fail("Expected '(' in '%s'." % cast) def parser(): - return self._parse_expression(inTemplate=False) + return self._parse_expression() expr = self._parse_expression_fallback([')'], parser) self.skip_ws() if not self.skip_string(")"): @@ -4923,7 +4957,7 @@ class DefinitionParser(BaseParser): try: def parser(): - return self._parse_expression(inTemplate=False) + return self._parse_expression() expr = self._parse_expression_fallback([')'], parser) prefix = ASTTypeId(expr, isType=False) if not self.skip_string(')'): @@ -4970,7 +5004,7 @@ class DefinitionParser(BaseParser): self.skip_ws() if prefixType in ['expr', 'cast', 'typeid']: if self.skip_string_and_ws('['): - expr = self._parse_expression(inTemplate=False) + expr = self._parse_expression() self.skip_ws() if not self.skip_string(']'): self.fail("Expected ']' in end of postfix expression.") @@ -5061,7 +5095,7 @@ class DefinitionParser(BaseParser): if self.skip_word_and_ws('noexcept'): if not self.skip_string_and_ws('('): self.fail("Expecting '(' after 'noexcept'.") - expr = self._parse_expression(inTemplate=False) + expr = self._parse_expression() self.skip_ws() if not self.skip_string(')'): self.fail("Expecting ')' to end 'noexcept'.") @@ -5194,7 +5228,7 @@ class DefinitionParser(BaseParser): # logical-or-expression # | logical-or-expression "?" expression ":" assignment-expression # | logical-or-expression assignment-operator initializer-clause - exprs = [] + exprs = [] # type: List[Union[ASTExpression, ASTBracedInitList]] ops = [] orExpr = self._parse_logical_or_expression(inTemplate=inTemplate) exprs.append(orExpr) @@ -5209,7 +5243,7 @@ class DefinitionParser(BaseParser): else: if not self.skip_string(op): continue - expr = self._parse_logical_or_expression(False) + expr = self._parse_initializer_clause() exprs.append(expr) ops.append(op) oneMore = True @@ -5226,11 +5260,19 @@ class DefinitionParser(BaseParser): # TODO: use _parse_conditional_expression_tail return orExpr - def _parse_expression(self, inTemplate: bool) -> ASTExpression: + def _parse_expression(self) -> ASTExpression: # -> assignment-expression # | expression "," assignment-expresion - # TODO: actually parse the second production - return self._parse_assignment_expression(inTemplate=inTemplate) + exprs = [self._parse_assignment_expression(inTemplate=False)] + while True: + self.skip_ws() + if not self.skip_string(','): + break + exprs.append(self._parse_assignment_expression(inTemplate=False)) + if len(exprs) == 1: + return exprs[0] + else: + return ASTCommaExpr(exprs) def _parse_expression_fallback(self, end: List[str], parser: Callable[[], ASTExpression], @@ -5441,7 +5483,7 @@ class DefinitionParser(BaseParser): if not self.skip_string(')'): self.fail("Expected ')' after 'decltype(auto'.") return ASTTrailingTypeSpecDecltypeAuto() - expr = self._parse_expression(inTemplate=False) + expr = self._parse_expression() self.skip_ws() if not self.skip_string(')'): self.fail("Expected ')' after 'decltype('.") @@ -5685,7 +5727,7 @@ class DefinitionParser(BaseParser): continue def parser(): - return self._parse_expression(inTemplate=False) + return self._parse_expression() value = self._parse_expression_fallback([']'], parser) if not self.skip_string(']'): self.fail("Expected ']' in end of array operator.") @@ -6351,7 +6393,7 @@ class DefinitionParser(BaseParser): def parse_expression(self) -> Union[ASTExpression, ASTType]: pos = self.pos try: - expr = self._parse_expression(False) + expr = self._parse_expression() self.skip_ws() self.assert_end() return expr diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py index c6d0df8aa..38b1d141e 100644 --- a/tests/test_domain_cpp.py +++ b/tests/test_domain_cpp.py @@ -274,6 +274,9 @@ def test_expressions(): exprCheck('a xor_eq 5', 'eO1aL5E') exprCheck('a |= 5', 'oR1aL5E') exprCheck('a or_eq 5', 'oR1aL5E') + exprCheck('a = {1, 2, 3}', 'aS1ailL1EL2EL3EE') + # comma operator + exprCheck('a, 5', 'cm1aL5E') # Additional tests # a < expression that starts with something that could be a template From c9fb5ab9ad6c3512899e279c8162cf76bff85b68 Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Wed, 25 Mar 2020 13:08:09 +0100 Subject: [PATCH 08/13] C++, support pack expansion in template argument lists --- sphinx/domains/cpp.py | 50 ++++++++++++++++++++++++++++++++-------- tests/test_domain_cpp.py | 6 +++++ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 2b4478820..63672985f 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -1713,9 +1713,11 @@ class ASTTemplateArgConstant(ASTBase): class ASTTemplateArgs(ASTBase): - def __init__(self, args: List[Union["ASTType", ASTTemplateArgConstant]]) -> None: + def __init__(self, args: List[Union["ASTType", ASTTemplateArgConstant]], + packExpansion: bool) -> None: assert args is not None self.args = args + self.packExpansion = packExpansion def get_id(self, version: int) -> str: if version == 1: @@ -1727,13 +1729,21 @@ class ASTTemplateArgs(ASTBase): res = [] res.append('I') - for a in self.args: - res.append(a.get_id(version)) + if len(self.args) > 0: + for a in self.args[:-1]: + res.append(a.get_id(version)) + if self.packExpansion: + res.append('J') + res.append(self.args[-1].get_id(version)) + if self.packExpansion: + res.append('E') res.append('E') return ''.join(res) def _stringify(self, transform: StringifyTransform) -> str: res = ', '.join(transform(a) for a in self.args) + if self.packExpansion: + res += '...' return '<' + res + '>' def describe_signature(self, signode: TextElement, mode: str, @@ -1746,6 +1756,8 @@ class ASTTemplateArgs(ASTBase): signode += nodes.Text(', ') first = False a.describe_signature(signode, 'markType', env, symbol=symbol) + if self.packExpansion: + signode += nodes.Text('...') signode += nodes.Text('>') @@ -5351,13 +5363,21 @@ class DefinitionParser(BaseParser): return ASTOperatorType(type) def _parse_template_argument_list(self) -> ASTTemplateArgs: + # template-argument-list: (but we include the < and > here + # template-argument ...[opt] + # template-argument-list, template-argument ...[opt] + # template-argument: + # constant-expression + # type-id + # id-expression self.skip_ws() if not self.skip_string_and_ws('<'): return None if self.skip_string('>'): - return ASTTemplateArgs([]) + return ASTTemplateArgs([], False) prevErrors = [] templateArgs = [] # type: List[Union[ASTType, ASTTemplateArgConstant]] + packExpansion = False while 1: pos = self.pos parsedComma = False @@ -5365,12 +5385,17 @@ class DefinitionParser(BaseParser): try: type = self._parse_type(named=False) self.skip_ws() - if self.skip_string('>'): + if self.skip_string_and_ws('...'): + packExpansion = True + parsedEnd = True + if not self.skip_string('>'): + self.fail('Expected ">" after "..." in template argument list.') + elif self.skip_string('>'): parsedEnd = True elif self.skip_string(','): parsedComma = True else: - self.fail('Expected ">" or "," in template argument list.') + self.fail('Expected "...>", ">" or "," in template argument list.') templateArgs.append(type) except DefinitionError as e: prevErrors.append((e, "If type argument")) @@ -5378,12 +5403,17 @@ class DefinitionParser(BaseParser): try: value = self._parse_constant_expression(inTemplate=True) self.skip_ws() - if self.skip_string('>'): + if self.skip_string_and_ws('...'): + packExpansion = True + parsedEnd = True + if not self.skip_string('>'): + self.fail('Expected ">" after "..." in template argument list.') + elif self.skip_string('>'): parsedEnd = True elif self.skip_string(','): parsedComma = True else: - self.fail('Expected ">" or "," in template argument list.') + self.fail('Expected "...>", ">" or "," in template argument list.') templateArgs.append(ASTTemplateArgConstant(value)) except DefinitionError as e: self.pos = pos @@ -5393,7 +5423,9 @@ class DefinitionParser(BaseParser): if parsedEnd: assert not parsedComma break - return ASTTemplateArgs(templateArgs) + else: + assert not packExpansion + return ASTTemplateArgs(templateArgs, packExpansion) def _parse_nested_name(self, memberPointer: bool = False) -> ASTNestedName: names = [] # type: List[ASTNestedNameElement] diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py index 38b1d141e..2faa3d4ea 100644 --- a/tests/test_domain_cpp.py +++ b/tests/test_domain_cpp.py @@ -629,6 +629,12 @@ def test_class_definitions(): {2: 'I0E7has_varI1TNSt6void_tIDTadN1T3varEEEEE'}) + check('class', 'template T', + {2: 'IDpE1TIJPFi2TsEEE'}) + check('class', 'template T<(Is)...>', + {2: 'I_DpiE1TIJX(Is)EEE', 3: 'I_DpiE1TIJX2IsEEE'}) + + def test_union_definitions(): check('union', 'A', {2: "1A"}) From 6ea9eb687a782e8f8d363642c398907781bda09c Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Wed, 25 Mar 2020 13:16:00 +0100 Subject: [PATCH 09/13] Update changes --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index a75d24e2e..42bae4b82 100644 --- a/CHANGES +++ b/CHANGES @@ -20,6 +20,8 @@ Bugs fixed * #7370: autosummary: raises UnboundLocalError when unknown module given * #7367: C++, alternate operator spellings are now supported. * C, alternate operator spellings are now supported. +* #7368: C++, comma operator in expressions, pack expansion in template + argument lists, and more comprehensive error messages in some cases. Testing -------- From 67e5edde6db57d2b9e1fbc1398d7ff775ac398a3 Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Wed, 25 Mar 2020 20:07:16 +0100 Subject: [PATCH 10/13] C, C++, do not recurse in anon symbols on addition --- sphinx/domains/c.py | 2 +- sphinx/domains/cpp.py | 2 +- tests/roots/test-domain-c/anon-dup-decl.rst | 5 +++++ tests/roots/test-domain-cpp/anon-dup-decl.rst | 4 ++++ tests/test_domain_c.py | 9 +++++++++ tests/test_domain_cpp.py | 9 +++++++++ 6 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 tests/roots/test-domain-c/anon-dup-decl.rst create mode 100644 tests/roots/test-domain-cpp/anon-dup-decl.rst diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index 5360b3a9b..a7b42cfd3 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -1675,7 +1675,7 @@ class Symbol: onMissingQualifiedSymbol, ancestorLookupType=None, matchSelf=False, - recurseInAnon=True, + recurseInAnon=False, searchInSiblings=False) assert lookupResult is not None # we create symbols all the way, so that can't happen symbols = list(lookupResult.symbols) diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 68e9f4d2f..dbb6d1a39 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -4159,7 +4159,7 @@ class Symbol: ancestorLookupType=None, templateShorthand=False, matchSelf=False, - recurseInAnon=True, + recurseInAnon=False, correctPrimaryTemplateArgs=True, searchInSiblings=False) assert lookupResult is not None # we create symbols all the way, so that can't happen diff --git a/tests/roots/test-domain-c/anon-dup-decl.rst b/tests/roots/test-domain-c/anon-dup-decl.rst new file mode 100644 index 000000000..5f6c3bdfe --- /dev/null +++ b/tests/roots/test-domain-c/anon-dup-decl.rst @@ -0,0 +1,5 @@ +.. c:struct:: anon_dup_decl + + .. c:struct:: @a.A + .. c:struct:: @b.A + .. c:struct:: A diff --git a/tests/roots/test-domain-cpp/anon-dup-decl.rst b/tests/roots/test-domain-cpp/anon-dup-decl.rst new file mode 100644 index 000000000..89a9c9553 --- /dev/null +++ b/tests/roots/test-domain-cpp/anon-dup-decl.rst @@ -0,0 +1,4 @@ +.. cpp:namespace:: anon_dup_decl +.. cpp:class:: @a::A +.. cpp:class:: @b::A +.. cpp:class:: A diff --git a/tests/test_domain_c.py b/tests/test_domain_c.py index 9daed2800..3255efc55 100644 --- a/tests/test_domain_c.py +++ b/tests/test_domain_c.py @@ -474,6 +474,15 @@ def test_build_domain_c(app, status, warning): assert len(ws) == 0 +@pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True}) +def test_build_domain_c_anon_dup_decl(app, status, warning): + app.builder.build_all() + ws = filter_warnings(warning, "anon-dup-decl") + assert len(ws) == 2 + assert "WARNING: c:identifier reference target not found: @a" in ws[0] + assert "WARNING: c:identifier reference target not found: @b" in ws[1] + + def test_cfunction(app): text = (".. c:function:: PyObject* " "PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems)") diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py index 2faa3d4ea..53dd4c5a9 100644 --- a/tests/test_domain_cpp.py +++ b/tests/test_domain_cpp.py @@ -913,6 +913,15 @@ def test_build_domain_cpp_backslash_ok(app, status, warning): assert "WARNING: Parsing of expression failed. Using fallback parser." in ws[0] +@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True}) +def test_build_domain_cpp_anon_dup_decl(app, status, warning): + app.builder.build_all() + ws = filter_warnings(warning, "anon-dup-decl") + assert len(ws) == 2 + assert "WARNING: cpp:identifier reference target not found: @a" in ws[0] + assert "WARNING: cpp:identifier reference target not found: @b" in ws[1] + + @pytest.mark.sphinx(testroot='domain-cpp') def test_build_domain_cpp_misuse_of_roles(app, status, warning): app.builder.build_all() From 5be82125f2d462b2d7895539ee234987e3e7c96f Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Wed, 25 Mar 2020 20:27:51 +0100 Subject: [PATCH 11/13] Update changes --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 42bae4b82..f0c19c854 100644 --- a/CHANGES +++ b/CHANGES @@ -22,6 +22,7 @@ Bugs fixed * C, alternate operator spellings are now supported. * #7368: C++, comma operator in expressions, pack expansion in template argument lists, and more comprehensive error messages in some cases. +* C, C++, fix crash and wrong duplicate warnings related to anon symbols. Testing -------- From 5ba69ad83774cbb27a6b0b751aa0cf9fc8912f2f Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 28 Mar 2020 21:53:46 +0900 Subject: [PATCH 12/13] Fix #7225: release package contains .mypy_cache directory --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 42fe22dd8..0f3cb9a08 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ clean-buildfiles: .PHONY: clean-mypyfiles clean-mypyfiles: - rm -rf .mypy_cache/ + rm -rf **/.mypy_cache/ .PHONY: style-check style-check: From d09dce974e03c74cc8642d23700767db02343d6a Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 28 Mar 2020 21:54:26 +0900 Subject: [PATCH 13/13] Fix typo --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0f3cb9a08..eea632143 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ clean-backupfiles: clean-generated: find . -name '.DS_Store' -exec rm -f {} + rm -rf Sphinx.egg-info/ - rm -rf dists/ + rm -rf dist/ rm -rf doc/_build/ rm -f sphinx/pycode/*.pickle rm -f utils/*3.py*