From 951530df2b0dd66ec7c2a38ef515c91f0b7bacfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20=C4=8Cerm=C3=A1k?= Date: Wed, 11 Aug 2021 21:59:47 +0200 Subject: [PATCH] Add patch to fix parsing errors, fixes rhbz#1992426 --- 0001-Handle-parsing-errors.patch | 1774 ++++++++++++++++++++++++++++++ python-breathe.spec | 1 + 2 files changed, 1775 insertions(+) create mode 100644 0001-Handle-parsing-errors.patch diff --git a/0001-Handle-parsing-errors.patch b/0001-Handle-parsing-errors.patch new file mode 100644 index 0000000..5163a70 --- /dev/null +++ b/0001-Handle-parsing-errors.patch @@ -0,0 +1,1774 @@ +From bbf329963dea00a82fef4a7aa511fbfe216336c4 Mon Sep 17 00:00:00 2001 +From: Jakob Lykke Andersen +Date: Sun, 2 May 2021 11:26:07 +0200 +Subject: [PATCH] Handle parsing errors + +In a few places the C++ domain in Sphinx is used to parse fragments, but potential errors are not handled. This tries to fix that. +One case is the parsing of the function parameters in doxygenfunction, so .. doxygenfunction:: f( would crash Breathe. +The second case is parsing the function parameters in the XML, I don't know how to trigger that directly, but it should be handled now. +The third case is in the hax to insert the name in template parameters (#681). For parameters typename ...Args it will trigger a parsing error (observed in the pybind11 project). + +Make WarningHandler private + +Directives, make create_warning a method + +Move BaseDirective from directive.base to directive + +Make more base directive classes private + +Move directive setup to separate file + +Make function directive helpers private + +Move function directive to separate file + +Move class-like directives to separate file + +Move content block directives to separate file + +Move item directives to directive module + +Rename directive module to directives + +Handle parsing errors from the C++ domain + +Fix typo + +Co-authored-by: Bruce Merry <1963944+bmerry@users.noreply.github.com> +--- + Makefile | 2 +- + breathe/__init__.py | 12 +- + breathe/directive/__init__.py | 0 + breathe/directives.py | 619 ------------------ + .../base.py => directives/__init__.py} | 35 +- + breathe/directives/class_like.py | 74 +++ + breathe/directives/content_block.py | 117 ++++ + breathe/{directive => directives}/file.py | 28 +- + breathe/directives/function.py | 249 +++++++ + breathe/{directive => directives}/index.py | 22 +- + breathe/directives/item.py | 102 +++ + breathe/directives/setup.py | 133 ++++ + breathe/renderer/sphinxrenderer.py | 32 +- + tests/test_renderer.py | 2 +- + 14 files changed, 738 insertions(+), 689 deletions(-) + delete mode 100644 breathe/directive/__init__.py + delete mode 100644 breathe/directives.py + rename breathe/{directive/base.py => directives/__init__.py} (85%) + create mode 100644 breathe/directives/class_like.py + create mode 100644 breathe/directives/content_block.py + rename breathe/{directive => directives}/file.py (83%) + create mode 100644 breathe/directives/function.py + rename breathe/{directive => directives}/index.py (89%) + create mode 100644 breathe/directives/item.py + create mode 100644 breathe/directives/setup.py + +diff --git a/Makefile b/Makefile +index e34a282..129a441 100644 +--- a/Makefile ++++ b/Makefile +@@ -37,7 +37,7 @@ dev-test: + .PHONY: flake8 + flake8: + flake8 breathe/*.py \ +- breathe/directive/*.py \ ++ breathe/directives/*.py \ + breathe/finder/*.py \ + breathe/renderer/sphinxrenderer.py \ + breathe/renderer/filter.py \ +diff --git a/breathe/__init__.py b/breathe/__init__.py +index 311e958..1821c89 100644 +--- a/breathe/__init__.py ++++ b/breathe/__init__.py +@@ -1,6 +1,6 @@ +-from . import directives +-from . import file_state_cache +-from .renderer import sphinxrenderer ++from breathe.directives.setup import setup as directive_setup ++from breathe.file_state_cache import setup as file_state_cache_setup ++from breathe.renderer.sphinxrenderer import setup as renderer_setup + + from sphinx.application import Sphinx + +@@ -8,9 +8,9 @@ __version__ = '4.30.0' + + + def setup(app: Sphinx): +- directives.setup(app) +- file_state_cache.setup(app) +- sphinxrenderer.setup(app) ++ directive_setup(app) ++ file_state_cache_setup(app) ++ renderer_setup(app) + + return { + 'version': __version__, +diff --git a/breathe/directive/__init__.py b/breathe/directive/__init__.py +deleted file mode 100644 +index e69de29..0000000 +diff --git a/breathe/directives.py b/breathe/directives.py +deleted file mode 100644 +index a57e075..0000000 +--- a/breathe/directives.py ++++ /dev/null +@@ -1,619 +0,0 @@ +-from breathe.directive.base import create_warning +-from breathe.directive.file import DoxygenFileDirective, AutoDoxygenFileDirective +-from breathe.directive.index import DoxygenIndexDirective, AutoDoxygenIndexDirective +-from breathe.exception import BreatheError +-from breathe.finder.factory import FinderFactory +-from breathe.directive.base import BaseDirective +-from breathe.file_state_cache import MTimeError +-from breathe.parser import DoxygenParserFactory, ParserError, FileIOError +-from breathe.process import AutoDoxygenProcessHandle +-from breathe.project import ProjectInfoFactory, ProjectError +-from breathe.renderer import format_parser_error, RenderContext +-from breathe.renderer.sphinxrenderer import WithContext +-from breathe.renderer.filter import Filter +-from breathe.renderer.mask import ( +- MaskFactory, NullMaskFactory, NoParameterNamesMask +-) +-from breathe.renderer.sphinxrenderer import SphinxRenderer +-from breathe.renderer.target import create_target_handler +- +-from sphinx.application import Sphinx +-from sphinx.domains import cpp +- +-from docutils import nodes +-from docutils.nodes import Node +-from docutils.parsers.rst.directives import unchanged_required, unchanged, flag +- +-import os +-import re +-import subprocess +- +-from typing import Any, List, Optional, Type # noqa +- +- +-class NoMatchingFunctionError(BreatheError): +- pass +- +- +-class UnableToResolveFunctionError(BreatheError): +- def __init__(self, signatures: List[str]) -> None: +- self.signatures = signatures +- +- +-class DoxygenFunctionDirective(BaseDirective): +- required_arguments = 1 +- option_spec = { +- "path": unchanged_required, +- "project": unchanged_required, +- "outline": flag, +- "no-link": flag, +- } +- has_content = False +- final_argument_whitespace = True +- +- def run(self) -> List[Node]: +- # Separate possible arguments (delimited by a "(") from the namespace::name +- match = re.match(r"([^(]*)(.*)", self.arguments[0]) +- assert match is not None # TODO: this is probably not appropriate, for now it fixes typing +- namespaced_function, args = match.group(1), match.group(2) +- +- # Split the namespace and the function name +- try: +- (namespace, function_name) = namespaced_function.rsplit("::", 1) +- except ValueError: +- (namespace, function_name) = "", namespaced_function +- namespace = namespace.strip() +- function_name = function_name.strip() +- +- try: +- project_info = self.project_info_factory.create_project_info(self.options) +- except ProjectError as e: +- warning = create_warning(None, self.state, self.lineno) +- return warning.warn('doxygenfunction: %s' % e) +- +- try: +- finder = self.finder_factory.create_finder(project_info) +- except MTimeError as e: +- warning = create_warning(None, self.state, self.lineno) +- return warning.warn('doxygenfunction: %s' % e) +- +- # Extract arguments from the function name. +- args = self._parse_args(args) +- +- finder_filter = self.filter_factory.create_function_and_all_friend_finder_filter( +- namespace, function_name) +- +- # TODO: find a more specific type for the Doxygen nodes +- matchesAll = [] # type: List[Any] +- finder.filter_(finder_filter, matchesAll) +- matches = [] +- for m in matchesAll: +- # only take functions and friend functions +- # ignore friend classes +- node = m[0] +- if node.kind == 'friend' and not node.argsstring: +- continue +- matches.append(m) +- +- # Create it ahead of time as it is cheap and it is ugly to declare it for both exception +- # clauses below +- warning = create_warning( +- project_info, +- self.state, +- self.lineno, +- namespace='%s::' % namespace if namespace else '', +- function=function_name, +- args=str(args) +- ) +- +- try: +- node_stack = self._resolve_function(matches, args, project_info) +- except NoMatchingFunctionError: +- return warning.warn('doxygenfunction: Cannot find function "{namespace}{function}" ' +- '{tail}') +- except UnableToResolveFunctionError as error: +- message = 'doxygenfunction: Unable to resolve function ' \ +- '"{namespace}{function}" with arguments {args} {tail}.\n' \ +- 'Potential matches:\n' +- +- text = '' +- for i, entry in enumerate(sorted(error.signatures)): +- text += '- %s\n' % entry +- block = nodes.literal_block('', '', nodes.Text(text)) +- formatted_message = warning.format(message) +- warning_nodes = [ +- nodes.paragraph("", "", nodes.Text(formatted_message)), +- block +- ] +- result = warning.warn(message, rendered_nodes=warning_nodes, +- unformatted_suffix=text) +- return result +- +- target_handler = create_target_handler(self.options, project_info, self.state.document) +- filter_ = self.filter_factory.create_outline_filter(self.options) +- +- return self.render(node_stack, project_info, filter_, target_handler, NullMaskFactory(), +- self.directive_args) +- +- def _parse_args(self, function_description: str) -> Optional[cpp.ASTParametersQualifiers]: +- if function_description == '': +- return None +- +- parser = cpp.DefinitionParser(function_description, +- location=self.get_source_info(), +- config=self.config) +- paramQual = parser._parse_parameters_and_qualifiers(paramMode='function') +- # now erase the parameter names +- for p in paramQual.args: +- if p.arg is None: +- assert p.ellipsis +- continue +- declarator = p.arg.type.decl +- while hasattr(declarator, 'next'): +- declarator = declarator.next # type: ignore +- assert hasattr(declarator, 'declId') +- declarator.declId = None # type: ignore +- p.arg.init = None # type: ignore +- return paramQual +- +- def _create_function_signature(self, node_stack, project_info, filter_, target_handler, +- mask_factory, directive_args) -> str: +- "Standard render process used by subclasses" +- +- try: +- object_renderer = SphinxRenderer( +- self.parser_factory.app, +- project_info, +- node_stack, +- self.state, +- self.state.document, +- target_handler, +- self.parser_factory.create_compound_parser(project_info), +- filter_, +- ) +- except ParserError as e: +- return format_parser_error("doxygenclass", e.error, e.filename, self.state, +- self.lineno, True) +- except FileIOError as e: +- return format_parser_error("doxygenclass", e.error, e.filename, self.state, +- self.lineno, False) +- +- context = RenderContext(node_stack, mask_factory, directive_args) +- node = node_stack[0] +- with WithContext(object_renderer, context): +- # this part should be kept in sync with visit_function in sphinxrenderer +- name = node.get_name() +- # assume we are only doing this for C++ declarations +- declaration = ' '.join([ +- object_renderer.create_template_prefix(node), +- ''.join(n.astext() for n in object_renderer.render(node.get_type())), +- name, +- node.get_argsstring() +- ]) +- parser = cpp.DefinitionParser(declaration, +- location=self.get_source_info(), +- config=self.config) +- ast = parser.parse_declaration('function', 'function') +- return str(ast) +- +- def _resolve_function(self, matches, args: Optional[cpp.ASTParametersQualifiers], project_info): +- if not matches: +- raise NoMatchingFunctionError() +- +- res = [] +- candSignatures = [] +- for entry in matches: +- text_options = {'no-link': u'', 'outline': u''} +- +- # Render the matches to docutils nodes +- target_handler = create_target_handler({'no-link': u''}, +- project_info, self.state.document) +- filter_ = self.filter_factory.create_outline_filter(text_options) +- mask_factory = MaskFactory({'param': NoParameterNamesMask}) +- +- # Override the directive args for this render +- directive_args = self.directive_args[:] +- directive_args[2] = text_options +- +- signature = self._create_function_signature(entry, project_info, filter_, +- target_handler, +- mask_factory, directive_args) +- candSignatures.append(signature) +- +- if args is not None: +- match = re.match(r"([^(]*)(.*)", signature) +- assert match +- _match_args = match.group(2) +- +- # Parse the text to find the arguments +- match_args = self._parse_args(_match_args) +- +- # Match them against the arg spec +- if args != match_args: +- continue +- +- res.append((entry, signature)) +- +- if len(res) == 1: +- return res[0][0] +- else: +- raise UnableToResolveFunctionError(candSignatures) +- +- +-class _DoxygenClassLikeDirective(BaseDirective): +- required_arguments = 1 +- optional_arguments = 0 +- final_argument_whitespace = True +- option_spec = { +- "path": unchanged_required, +- "project": unchanged_required, +- "members": unchanged, +- "membergroups": unchanged_required, +- "members-only": flag, +- "protected-members": flag, +- "private-members": flag, +- "undoc-members": flag, +- "show": unchanged_required, +- "outline": flag, +- "no-link": flag, +- } +- has_content = False +- +- def run(self) -> List[Node]: +- name = self.arguments[0] +- +- try: +- project_info = self.project_info_factory.create_project_info(self.options) +- except ProjectError as e: +- warning = create_warning(None, self.state, self.lineno, kind=self.kind) +- return warning.warn('doxygen{kind}: %s' % e) +- +- try: +- finder = self.finder_factory.create_finder(project_info) +- except MTimeError as e: +- warning = create_warning(None, self.state, self.lineno, kind=self.kind) +- return warning.warn('doxygen{kind}: %s' % e) +- +- finder_filter = self.filter_factory.create_compound_finder_filter(name, self.kind) +- +- # TODO: find a more specific type for the Doxygen nodes +- matches = [] # type: List[Any] +- finder.filter_(finder_filter, matches) +- +- if len(matches) == 0: +- warning = create_warning(project_info, self.state, self.lineno, name=name, +- kind=self.kind) +- return warning.warn('doxygen{kind}: Cannot find class "{name}" {tail}') +- +- target_handler = create_target_handler(self.options, project_info, self.state.document) +- filter_ = self.filter_factory.create_class_filter(name, self.options) +- +- mask_factory = NullMaskFactory() +- return self.render(matches[0], project_info, filter_, target_handler, mask_factory, +- self.directive_args) +- +- +-class DoxygenClassDirective(_DoxygenClassLikeDirective): +- kind = "class" +- +- +-class DoxygenStructDirective(_DoxygenClassLikeDirective): +- kind = "struct" +- +- +-class DoxygenInterfaceDirective(_DoxygenClassLikeDirective): +- kind = "interface" +- +- +-class _DoxygenContentBlockDirective(BaseDirective): +- """Base class for namespace and group directives which have very similar behaviours""" +- +- required_arguments = 1 +- optional_arguments = 1 +- option_spec = { +- "path": unchanged_required, +- "project": unchanged_required, +- "content-only": flag, +- "outline": flag, +- "members": flag, +- "protected-members": flag, +- "private-members": flag, +- "undoc-members": flag, +- "no-link": flag +- } +- has_content = False +- +- def run(self) -> List[Node]: +- name = self.arguments[0] +- +- try: +- project_info = self.project_info_factory.create_project_info(self.options) +- except ProjectError as e: +- warning = create_warning(None, self.state, self.lineno, kind=self.kind) +- return warning.warn('doxygen{kind}: %s' % e) +- +- try: +- finder = self.finder_factory.create_finder(project_info) +- except MTimeError as e: +- warning = create_warning(None, self.state, self.lineno, kind=self.kind) +- return warning.warn('doxygen{kind}: %s' % e) +- +- finder_filter = self.filter_factory.create_finder_filter(self.kind, name) +- +- # TODO: find a more specific type for the Doxygen nodes +- matches = [] # type: List[Any] +- finder.filter_(finder_filter, matches) +- +- # It shouldn't be possible to have too many matches as namespaces & groups in their nature +- # are merged together if there are multiple declarations, so we only check for no matches +- if not matches: +- warning = create_warning(project_info, self.state, self.lineno, name=name, +- kind=self.kind) +- return warning.warn('doxygen{kind}: Cannot find {kind} "{name}" {tail}') +- +- if 'content-only' in self.options and self.kind != "page": +- # Unpack the single entry in the matches list +- (node_stack,) = matches +- +- filter_ = self.filter_factory.create_content_filter(self.kind, self.options) +- # Having found the compound node for the namespace or group in the index we want to grab +- # the contents of it which match the filter +- contents_finder = self.finder_factory.create_finder_from_root(node_stack[0], +- project_info) +- # TODO: find a more specific type for the Doxygen nodes +- contents = [] # type: List[Any] +- contents_finder.filter_(filter_, contents) +- +- # Replaces matches with our new starting points +- matches = contents +- +- target_handler = create_target_handler(self.options, project_info, self.state.document) +- filter_ = self.filter_factory.create_render_filter(self.kind, self.options) +- +- node_list = [] +- for node_stack in matches: +- object_renderer = SphinxRenderer( +- self.parser_factory.app, +- project_info, +- node_stack, +- self.state, +- self.state.document, +- target_handler, +- self.parser_factory.create_compound_parser(project_info), +- filter_ +- ) +- +- mask_factory = NullMaskFactory() +- context = RenderContext(node_stack, mask_factory, self.directive_args) +- node_list.extend(object_renderer.render(context.node_stack[0], context)) +- +- return node_list +- +- +-class DoxygenNamespaceDirective(_DoxygenContentBlockDirective): +- kind = "namespace" +- +- +-class DoxygenGroupDirective(_DoxygenContentBlockDirective): +- kind = "group" +- option_spec = { +- **_DoxygenContentBlockDirective.option_spec, +- "inner": flag, +- } +- +- +-class DoxygenPageDirective(_DoxygenContentBlockDirective): +- kind = "page" +- option_spec = { +- "path": unchanged_required, +- "project": unchanged_required, +- "content-only": flag, +- } +- +- +-# TODO: is this comment still relevant? +-# This class was the same as the DoxygenBaseDirective above, except that it +-# wraps the output in a definition_list before passing it back. This should be +-# abstracted in a far nicer way to avoid repeating so much code +-# +-# Now we've removed the definition_list wrap so we really need to refactor this! +-class _DoxygenBaseItemDirective(BaseDirective): +- required_arguments = 1 +- optional_arguments = 1 +- option_spec = { +- "path": unchanged_required, +- "project": unchanged_required, +- "outline": flag, +- "no-link": flag, +- } +- has_content = False +- +- def create_finder_filter(self, namespace: str, name: str) -> Filter: +- """Creates a filter to find the node corresponding to this item.""" +- +- return self.filter_factory.create_member_finder_filter( +- namespace, name, self.kind) +- +- def run(self) -> List[Node]: +- try: +- namespace, name = self.arguments[0].rsplit("::", 1) +- except ValueError: +- namespace, name = "", self.arguments[0] +- +- try: +- project_info = self.project_info_factory.create_project_info(self.options) +- except ProjectError as e: +- warning = create_warning(None, self.state, self.lineno, kind=self.kind) +- return warning.warn('doxygen{kind}: %s' % e) +- +- try: +- finder = self.finder_factory.create_finder(project_info) +- except MTimeError as e: +- warning = create_warning(None, self.state, self.lineno, kind=self.kind) +- return warning.warn('doxygen{kind}: %s' % e) +- +- finder_filter = self.create_finder_filter(namespace, name) +- +- # TODO: find a more specific type for the Doxygen nodes +- matches = [] # type: List[Any] +- finder.filter_(finder_filter, matches) +- +- if len(matches) == 0: +- display_name = "%s::%s" % (namespace, name) if namespace else name +- warning = create_warning(project_info, self.state, self.lineno, kind=self.kind, +- display_name=display_name) +- return warning.warn('doxygen{kind}: Cannot find {kind} "{display_name}" {tail}') +- +- target_handler = create_target_handler(self.options, project_info, self.state.document) +- filter_ = self.filter_factory.create_outline_filter(self.options) +- +- node_stack = matches[0] +- mask_factory = NullMaskFactory() +- return self.render(node_stack, project_info, filter_, target_handler, mask_factory, +- self.directive_args) +- +- +-class DoxygenVariableDirective(_DoxygenBaseItemDirective): +- kind = "variable" +- +- +-class DoxygenDefineDirective(_DoxygenBaseItemDirective): +- kind = "define" +- +- +-class DoxygenEnumDirective(_DoxygenBaseItemDirective): +- kind = "enum" +- +- +-class DoxygenEnumValueDirective(_DoxygenBaseItemDirective): +- kind = "enumvalue" +- +- def create_finder_filter(self, namespace: str, name: str) -> Filter: +- return self.filter_factory.create_enumvalue_finder_filter(name) +- +- +-class DoxygenTypedefDirective(_DoxygenBaseItemDirective): +- kind = "typedef" +- +- +-class DoxygenUnionDirective(_DoxygenBaseItemDirective): +- kind = "union" +- +- def create_finder_filter(self, namespace: str, name: str) -> Filter: +- # Unions are stored in the xml file with their fully namespaced name +- # We're using C++ namespaces here, it might be best to make this file +- # type dependent +- # +- xml_name = "%s::%s" % (namespace, name) if namespace else name +- return self.filter_factory.create_compound_finder_filter(xml_name, 'union') +- +- +-# Setup Administration +-# -------------------- +- +-class DirectiveContainer: +- def __init__(self, app: Sphinx, directive: Type[BaseDirective], +- finder_factory: FinderFactory, project_info_factory: ProjectInfoFactory, +- parser_factory: DoxygenParserFactory): +- self.app = app +- self.directive = directive +- self.finder_factory = finder_factory +- self.project_info_factory = project_info_factory +- self.parser_factory = parser_factory +- +- # Required for sphinx to inspect +- self.required_arguments = directive.required_arguments +- self.optional_arguments = directive.optional_arguments +- self.option_spec = directive.option_spec +- self.has_content = directive.has_content +- self.final_argument_whitespace = directive.final_argument_whitespace +- +- def __call__(self, *args): +- call_args = [ +- self.finder_factory, +- self.project_info_factory, +- self.parser_factory +- ] +- call_args.extend(args) +- return self.directive(*call_args) +- +- +-def setup(app: Sphinx) -> None: +- directives = { +- "doxygenindex": DoxygenIndexDirective, +- "autodoxygenindex": AutoDoxygenIndexDirective, +- "doxygenfunction": DoxygenFunctionDirective, +- "doxygenstruct": DoxygenStructDirective, +- "doxygenclass": DoxygenClassDirective, +- "doxygeninterface": DoxygenInterfaceDirective, +- "doxygenvariable": DoxygenVariableDirective, +- "doxygendefine": DoxygenDefineDirective, +- "doxygenenum": DoxygenEnumDirective, +- "doxygenenumvalue": DoxygenEnumValueDirective, +- "doxygentypedef": DoxygenTypedefDirective, +- "doxygenunion": DoxygenUnionDirective, +- "doxygennamespace": DoxygenNamespaceDirective, +- "doxygengroup": DoxygenGroupDirective, +- "doxygenfile": DoxygenFileDirective, +- "autodoxygenfile": AutoDoxygenFileDirective, +- "doxygenpage": DoxygenPageDirective, +- } +- +- # note: the parser factory contains a cache of the parsed XML +- # note: the project_info_factory also contains some caching stuff +- # TODO: is that actually safe for when reading in parallel? +- project_info_factory = ProjectInfoFactory(app) +- parser_factory = DoxygenParserFactory(app) +- finder_factory = FinderFactory(app, parser_factory) +- for name, directive in directives.items(): +- # ordinarily app.add_directive takes a class it self, but we need to inject extra arguments +- # so we give a DirectiveContainer object which has an overloaded __call__ operator. +- app.add_directive(name, DirectiveContainer( # type: ignore +- app, +- directive, +- finder_factory, +- project_info_factory, +- parser_factory +- )) +- +- app.add_config_value("breathe_projects", {}, True) # Dict[str, str] +- app.add_config_value("breathe_default_project", "", True) # str +- # Provide reasonable defaults for domain_by_extension mapping. Can be overridden by users. +- app.add_config_value("breathe_domain_by_extension", +- {'py': 'py', 'cs': 'cs'}, True) # Dict[str, str] +- app.add_config_value("breathe_domain_by_file_pattern", {}, True) # Dict[str, str] +- app.add_config_value("breathe_projects_source", {}, True) +- app.add_config_value("breathe_build_directory", '', True) +- app.add_config_value("breathe_default_members", (), True) +- app.add_config_value("breathe_show_define_initializer", False, 'env') +- app.add_config_value("breathe_show_enumvalue_initializer", False, 'env') +- app.add_config_value("breathe_implementation_filename_extensions", ['.c', '.cc', '.cpp'], True) +- app.add_config_value("breathe_doxygen_config_options", {}, True) +- app.add_config_value("breathe_use_project_refids", False, "env") +- app.add_config_value("breathe_order_parameters_first", False, 'env') +- app.add_config_value("breathe_separate_member_pages", False, 'env') +- +- breathe_css = "breathe.css" +- if (os.path.exists(os.path.join(app.confdir, "_static", breathe_css))): # type: ignore +- app.add_css_file(breathe_css) +- +- def write_file(directory, filename, content): +- # Check the directory exists +- if not os.path.exists(directory): +- os.makedirs(directory) +- +- # Write the file with the provided contents +- with open(os.path.join(directory, filename), "w") as f: +- f.write(content) +- +- doxygen_handle = AutoDoxygenProcessHandle( +- subprocess.check_call, +- write_file, +- project_info_factory) +- +- def doxygen_hook(app): +- doxygen_handle.generate_xml( +- app.config.breathe_projects_source, +- app.config.breathe_doxygen_config_options +- ) +- app.connect("builder-inited", doxygen_hook) +diff --git a/breathe/directive/base.py b/breathe/directives/__init__.py +similarity index 85% +rename from breathe/directive/base.py +rename to breathe/directives/__init__.py +index f808306..db27fe2 100644 +--- a/breathe/directive/base.py ++++ b/breathe/directives/__init__.py +@@ -16,7 +16,7 @@ from docutils.nodes import Node + from typing import Any, Dict, List, Optional, Sequence + + +-class WarningHandler: ++class _WarningHandler: + def __init__(self, state, context: Dict[str, Any]) -> None: + self.state = state + self.context = context +@@ -35,23 +35,6 @@ class WarningHandler: + return text.format(**self.context) + + +-def create_warning(project_info: Optional[ProjectInfo], state, lineno: int, +- **kwargs) -> WarningHandler: +- tail = '' +- if project_info: +- tail = 'in doxygen xml output for project "{project}" from directory: {path}'.format( +- project=project_info.name(), +- path=project_info.project_path() +- ) +- +- context = dict( +- lineno=lineno, +- tail=tail, +- **kwargs +- ) +- return WarningHandler(state, context) +- +- + class BaseDirective(SphinxDirective): + def __init__(self, finder_factory: FinderFactory, + project_info_factory: ProjectInfoFactory, +@@ -69,6 +52,22 @@ class BaseDirective(SphinxDirective): + def kind(self) -> str: + raise NotImplementedError + ++ def create_warning(self, project_info: Optional[ProjectInfo], **kwargs) -> _WarningHandler: ++ if project_info: ++ tail = 'in doxygen xml output for project "{project}" from directory: {path}'.format( ++ project=project_info.name(), ++ path=project_info.project_path() ++ ) ++ else: ++ tail = '' ++ ++ context = dict( ++ lineno=self.lineno, ++ tail=tail, ++ **kwargs ++ ) ++ return _WarningHandler(self.state, context) ++ + def render(self, node_stack, project_info: ProjectInfo, filter_: Filter, + target_handler: TargetHandler, mask_factory: MaskFactoryBase, + directive_args) -> List[Node]: +diff --git a/breathe/directives/class_like.py b/breathe/directives/class_like.py +new file mode 100644 +index 0000000..325251d +--- /dev/null ++++ b/breathe/directives/class_like.py +@@ -0,0 +1,74 @@ ++from breathe.directives import BaseDirective ++from breathe.file_state_cache import MTimeError ++from breathe.project import ProjectError ++from breathe.renderer.mask import NullMaskFactory ++from breathe.renderer.target import create_target_handler ++ ++from docutils.nodes import Node ++from docutils.parsers.rst.directives import unchanged_required, unchanged, flag ++ ++from typing import Any, List, Optional, Type # noqa ++ ++ ++class _DoxygenClassLikeDirective(BaseDirective): ++ required_arguments = 1 ++ optional_arguments = 0 ++ final_argument_whitespace = True ++ option_spec = { ++ "path": unchanged_required, ++ "project": unchanged_required, ++ "members": unchanged, ++ "membergroups": unchanged_required, ++ "members-only": flag, ++ "protected-members": flag, ++ "private-members": flag, ++ "undoc-members": flag, ++ "show": unchanged_required, ++ "outline": flag, ++ "no-link": flag, ++ } ++ has_content = False ++ ++ def run(self) -> List[Node]: ++ name = self.arguments[0] ++ ++ try: ++ project_info = self.project_info_factory.create_project_info(self.options) ++ except ProjectError as e: ++ warning = self.create_warning(None, kind=self.kind) ++ return warning.warn('doxygen{kind}: %s' % e) ++ ++ try: ++ finder = self.finder_factory.create_finder(project_info) ++ except MTimeError as e: ++ warning = self.create_warning(None, kind=self.kind) ++ return warning.warn('doxygen{kind}: %s' % e) ++ ++ finder_filter = self.filter_factory.create_compound_finder_filter(name, self.kind) ++ ++ # TODO: find a more specific type for the Doxygen nodes ++ matches = [] # type: List[Any] ++ finder.filter_(finder_filter, matches) ++ ++ if len(matches) == 0: ++ warning = self.create_warning(project_info, name=name, kind=self.kind) ++ return warning.warn('doxygen{kind}: Cannot find class "{name}" {tail}') ++ ++ target_handler = create_target_handler(self.options, project_info, self.state.document) ++ filter_ = self.filter_factory.create_class_filter(name, self.options) ++ ++ mask_factory = NullMaskFactory() ++ return self.render(matches[0], project_info, filter_, target_handler, mask_factory, ++ self.directive_args) ++ ++ ++class DoxygenClassDirective(_DoxygenClassLikeDirective): ++ kind = "class" ++ ++ ++class DoxygenStructDirective(_DoxygenClassLikeDirective): ++ kind = "struct" ++ ++ ++class DoxygenInterfaceDirective(_DoxygenClassLikeDirective): ++ kind = "interface" +diff --git a/breathe/directives/content_block.py b/breathe/directives/content_block.py +new file mode 100644 +index 0000000..f50d686 +--- /dev/null ++++ b/breathe/directives/content_block.py +@@ -0,0 +1,117 @@ ++from breathe.directives import BaseDirective ++from breathe.file_state_cache import MTimeError ++from breathe.project import ProjectError ++from breathe.renderer import RenderContext ++from breathe.renderer.mask import NullMaskFactory ++from breathe.renderer.sphinxrenderer import SphinxRenderer ++from breathe.renderer.target import create_target_handler ++ ++from docutils.nodes import Node ++from docutils.parsers.rst.directives import unchanged_required, flag ++ ++from typing import Any, List, Optional, Type # noqa ++ ++ ++class _DoxygenContentBlockDirective(BaseDirective): ++ """Base class for namespace and group directives which have very similar behaviours""" ++ ++ required_arguments = 1 ++ optional_arguments = 1 ++ option_spec = { ++ "path": unchanged_required, ++ "project": unchanged_required, ++ "content-only": flag, ++ "outline": flag, ++ "members": flag, ++ "protected-members": flag, ++ "private-members": flag, ++ "undoc-members": flag, ++ "no-link": flag ++ } ++ has_content = False ++ ++ def run(self) -> List[Node]: ++ name = self.arguments[0] ++ ++ try: ++ project_info = self.project_info_factory.create_project_info(self.options) ++ except ProjectError as e: ++ warning = self.create_warning(None, kind=self.kind) ++ return warning.warn('doxygen{kind}: %s' % e) ++ ++ try: ++ finder = self.finder_factory.create_finder(project_info) ++ except MTimeError as e: ++ warning = self.create_warning(None, kind=self.kind) ++ return warning.warn('doxygen{kind}: %s' % e) ++ ++ finder_filter = self.filter_factory.create_finder_filter(self.kind, name) ++ ++ # TODO: find a more specific type for the Doxygen nodes ++ matches = [] # type: List[Any] ++ finder.filter_(finder_filter, matches) ++ ++ # It shouldn't be possible to have too many matches as namespaces & groups in their nature ++ # are merged together if there are multiple declarations, so we only check for no matches ++ if not matches: ++ warning = self.create_warning(project_info, name=name, kind=self.kind) ++ return warning.warn('doxygen{kind}: Cannot find {kind} "{name}" {tail}') ++ ++ if 'content-only' in self.options and self.kind != "page": ++ # Unpack the single entry in the matches list ++ (node_stack,) = matches ++ ++ filter_ = self.filter_factory.create_content_filter(self.kind, self.options) ++ # Having found the compound node for the namespace or group in the index we want to grab ++ # the contents of it which match the filter ++ contents_finder = self.finder_factory.create_finder_from_root(node_stack[0], ++ project_info) ++ # TODO: find a more specific type for the Doxygen nodes ++ contents = [] # type: List[Any] ++ contents_finder.filter_(filter_, contents) ++ ++ # Replaces matches with our new starting points ++ matches = contents ++ ++ target_handler = create_target_handler(self.options, project_info, self.state.document) ++ filter_ = self.filter_factory.create_render_filter(self.kind, self.options) ++ ++ node_list = [] ++ for node_stack in matches: ++ object_renderer = SphinxRenderer( ++ self.parser_factory.app, ++ project_info, ++ node_stack, ++ self.state, ++ self.state.document, ++ target_handler, ++ self.parser_factory.create_compound_parser(project_info), ++ filter_ ++ ) ++ ++ mask_factory = NullMaskFactory() ++ context = RenderContext(node_stack, mask_factory, self.directive_args) ++ node_list.extend(object_renderer.render(context.node_stack[0], context)) ++ ++ return node_list ++ ++ ++class DoxygenNamespaceDirective(_DoxygenContentBlockDirective): ++ kind = "namespace" ++ ++ ++class DoxygenGroupDirective(_DoxygenContentBlockDirective): ++ kind = "group" ++ option_spec = { ++ **_DoxygenContentBlockDirective.option_spec, ++ "inner": flag, ++ } ++ ++ ++class DoxygenPageDirective(_DoxygenContentBlockDirective): ++ kind = "page" ++ option_spec = { ++ "path": unchanged_required, ++ "project": unchanged_required, ++ "content-only": flag, ++ } +diff --git a/breathe/directive/file.py b/breathe/directives/file.py +similarity index 83% +rename from breathe/directive/file.py +rename to breathe/directives/file.py +index 923c728..ccdf4aa 100644 +--- a/breathe/directive/file.py ++++ b/breathe/directives/file.py +@@ -1,7 +1,6 @@ + from ..renderer.mask import NullMaskFactory +-from ..directive.base import BaseDirective ++from ..directives import BaseDirective + from ..project import ProjectError +-from .base import create_warning + + from breathe.renderer import RenderContext + from breathe.renderer.sphinxrenderer import SphinxRenderer +@@ -10,7 +9,7 @@ from breathe.renderer.target import create_target_handler + from docutils.parsers.rst.directives import unchanged_required, flag + + +-class BaseFileDirective(BaseDirective): ++class _BaseFileDirective(BaseDirective): + """Base class handle the main work when given the appropriate file and project info to work + from. + """ +@@ -27,13 +26,10 @@ class BaseFileDirective(BaseDirective): + finder.filter_(finder_filter, matches) + + if len(matches) > 1: +- warning = create_warning(None, self.state, self.lineno, file=file_, +- directivename=self.directive_name) ++ warning = self.create_warning(None, file=file_, directivename=self.directive_name) + return warning.warn('{directivename}: Found multiple matches for file "{file} {tail}') +- + elif not matches: +- warning = create_warning(None, self.state, self.lineno, file=file_, +- directivename=self.directive_name) ++ warning = self.create_warning(None, file=file_, directivename=self.directive_name) + return warning.warn('{directivename}: Cannot find file "{file} {tail}') + + target_handler = create_target_handler(self.options, project_info, self.state.document) +@@ -59,8 +55,7 @@ class BaseFileDirective(BaseDirective): + return node_list + + +-class DoxygenFileDirective(BaseFileDirective): +- ++class DoxygenFileDirective(_BaseFileDirective): + directive_name = 'doxygenfile' + + required_arguments = 0 +@@ -71,25 +66,23 @@ class DoxygenFileDirective(BaseFileDirective): + "outline": flag, + "no-link": flag, + "sections": unchanged_required, +- } ++ } + has_content = False + + def run(self): + """Get the file from the argument and the project info from the factory.""" + + file_ = self.arguments[0] +- + try: + project_info = self.project_info_factory.create_project_info(self.options) + except ProjectError as e: +- warning = create_warning(None, self.state, self.lineno) ++ warning = self.create_warning(None) + return warning.warn('doxygenfile: %s' % e) + + return self.handle_contents(file_, project_info) + + +-class AutoDoxygenFileDirective(BaseFileDirective): +- ++class AutoDoxygenFileDirective(_BaseFileDirective): + directive_name = 'autodoxygenfile' + + required_arguments = 1 +@@ -98,7 +91,7 @@ class AutoDoxygenFileDirective(BaseFileDirective): + "outline": flag, + "no-link": flag, + "sections": unchanged_required, +- } ++ } + has_content = False + + def run(self): +@@ -107,11 +100,10 @@ class AutoDoxygenFileDirective(BaseFileDirective): + """ + + file_ = self.arguments[0] +- + try: + project_info = self.project_info_factory.retrieve_project_info_for_auto(self.options) + except ProjectError as e: +- warning = create_warning(None, self.state, self.lineno) ++ warning = self.create_warning(None) + return warning.warn('autodoxygenfile: %s' % e) + + return self.handle_contents(file_, project_info) +diff --git a/breathe/directives/function.py b/breathe/directives/function.py +new file mode 100644 +index 0000000..ea89f53 +--- /dev/null ++++ b/breathe/directives/function.py +@@ -0,0 +1,249 @@ ++from breathe.directives import BaseDirective ++from breathe.exception import BreatheError ++from breathe.file_state_cache import MTimeError ++from breathe.parser import ParserError, FileIOError ++from breathe.project import ProjectError ++from breathe.renderer import format_parser_error, RenderContext ++from breathe.renderer.sphinxrenderer import WithContext ++from breathe.renderer.mask import ( ++ MaskFactory, NullMaskFactory, NoParameterNamesMask ++) ++from breathe.renderer.sphinxrenderer import SphinxRenderer ++from breathe.renderer.target import create_target_handler ++ ++from docutils.nodes import Node ++from docutils.parsers.rst.directives import unchanged_required, flag ++ ++from sphinx.domains import cpp ++ ++from docutils import nodes ++ ++import re ++ ++from typing import Any, List, Optional, Type # noqa ++ ++ ++class _NoMatchingFunctionError(BreatheError): ++ pass ++ ++ ++class _UnableToResolveFunctionError(BreatheError): ++ def __init__(self, signatures: List[str]) -> None: ++ self.signatures = signatures ++ ++ ++class DoxygenFunctionDirective(BaseDirective): ++ required_arguments = 1 ++ option_spec = { ++ "path": unchanged_required, ++ "project": unchanged_required, ++ "outline": flag, ++ "no-link": flag, ++ } ++ has_content = False ++ final_argument_whitespace = True ++ ++ def run(self) -> List[Node]: ++ # Separate possible arguments (delimited by a "(") from the namespace::name ++ match = re.match(r"([^(]*)(.*)", self.arguments[0]) ++ assert match is not None # TODO: this is probably not appropriate, for now it fixes typing ++ namespaced_function, args = match.group(1), match.group(2) ++ ++ # Split the namespace and the function name ++ try: ++ (namespace, function_name) = namespaced_function.rsplit("::", 1) ++ except ValueError: ++ (namespace, function_name) = "", namespaced_function ++ namespace = namespace.strip() ++ function_name = function_name.strip() ++ ++ try: ++ project_info = self.project_info_factory.create_project_info(self.options) ++ except ProjectError as e: ++ warning = self.create_warning(None) ++ return warning.warn('doxygenfunction: %s' % e) ++ ++ try: ++ finder = self.finder_factory.create_finder(project_info) ++ except MTimeError as e: ++ warning = self.create_warning(None) ++ return warning.warn('doxygenfunction: %s' % e) ++ ++ # Extract arguments from the function name. ++ try: ++ args = self._parse_args(args) ++ except cpp.DefinitionError as e: ++ return self.create_warning( ++ project_info, ++ namespace='%s::' % namespace if namespace else '', ++ function=function_name, ++ args=str(args), ++ cpperror=str(e) ++ ).warn('doxygenfunction: Unable to resolve function ' ++ '"{namespace}{function}" with arguments "{args}".\n' ++ 'Could not parse arguments. Parsing eror is\n{cpperror}') ++ ++ finder_filter = self.filter_factory.create_function_and_all_friend_finder_filter( ++ namespace, function_name) ++ ++ # TODO: find a more specific type for the Doxygen nodes ++ matchesAll = [] # type: List[Any] ++ finder.filter_(finder_filter, matchesAll) ++ matches = [] ++ for m in matchesAll: ++ # only take functions and friend functions ++ # ignore friend classes ++ node = m[0] ++ if node.kind == 'friend' and not node.argsstring: ++ continue ++ matches.append(m) ++ ++ # Create it ahead of time as it is cheap and it is ugly to declare it for both exception ++ # clauses below ++ warning = self.create_warning( ++ project_info, ++ namespace='%s::' % namespace if namespace else '', ++ function=function_name, ++ args=str(args) ++ ) ++ ++ try: ++ node_stack = self._resolve_function(matches, args, project_info) ++ except _NoMatchingFunctionError: ++ return warning.warn('doxygenfunction: Cannot find function "{namespace}{function}" ' ++ '{tail}') ++ except _UnableToResolveFunctionError as error: ++ message = 'doxygenfunction: Unable to resolve function ' \ ++ '"{namespace}{function}" with arguments {args} {tail}.\n' \ ++ 'Potential matches:\n' ++ ++ text = '' ++ for i, entry in enumerate(sorted(error.signatures)): ++ text += '- %s\n' % entry ++ block = nodes.literal_block('', '', nodes.Text(text)) ++ formatted_message = warning.format(message) ++ warning_nodes = [ ++ nodes.paragraph("", "", nodes.Text(formatted_message)), ++ block ++ ] ++ result = warning.warn(message, rendered_nodes=warning_nodes, ++ unformatted_suffix=text) ++ return result ++ except cpp.DefinitionError as error: ++ warning.context['cpperror'] = str(error) ++ return warning.warn( ++ 'doxygenfunction: Unable to resolve function ' ++ '"{namespace}{function}" with arguments "{args}".\n' ++ 'Candidate function could not be parsed. Parsing error is\n{cpperror}') ++ ++ target_handler = create_target_handler(self.options, project_info, self.state.document) ++ filter_ = self.filter_factory.create_outline_filter(self.options) ++ ++ return self.render(node_stack, project_info, filter_, target_handler, NullMaskFactory(), ++ self.directive_args) ++ ++ def _parse_args(self, function_description: str) -> Optional[cpp.ASTParametersQualifiers]: ++ # Note: the caller must catch cpp.DefinitionError ++ if function_description == '': ++ return None ++ ++ parser = cpp.DefinitionParser(function_description, ++ location=self.get_source_info(), ++ config=self.config) ++ paramQual = parser._parse_parameters_and_qualifiers(paramMode='function') ++ # now erase the parameter names ++ for p in paramQual.args: ++ if p.arg is None: ++ assert p.ellipsis ++ continue ++ declarator = p.arg.type.decl ++ while hasattr(declarator, 'next'): ++ declarator = declarator.next # type: ignore ++ assert hasattr(declarator, 'declId') ++ declarator.declId = None # type: ignore ++ p.arg.init = None # type: ignore ++ return paramQual ++ ++ def _create_function_signature(self, node_stack, project_info, filter_, target_handler, ++ mask_factory, directive_args) -> str: ++ "Standard render process used by subclasses" ++ ++ try: ++ object_renderer = SphinxRenderer( ++ self.parser_factory.app, ++ project_info, ++ node_stack, ++ self.state, ++ self.state.document, ++ target_handler, ++ self.parser_factory.create_compound_parser(project_info), ++ filter_, ++ ) ++ except ParserError as e: ++ return format_parser_error("doxygenclass", e.error, e.filename, self.state, ++ self.lineno, True) ++ except FileIOError as e: ++ return format_parser_error("doxygenclass", e.error, e.filename, self.state, ++ self.lineno, False) ++ ++ context = RenderContext(node_stack, mask_factory, directive_args) ++ node = node_stack[0] ++ with WithContext(object_renderer, context): ++ # this part should be kept in sync with visit_function in sphinxrenderer ++ name = node.get_name() ++ # assume we are only doing this for C++ declarations ++ declaration = ' '.join([ ++ object_renderer.create_template_prefix(node), ++ ''.join(n.astext() for n in object_renderer.render(node.get_type())), ++ name, ++ node.get_argsstring() ++ ]) ++ parser = cpp.DefinitionParser(declaration, ++ location=self.get_source_info(), ++ config=self.config) ++ ast = parser.parse_declaration('function', 'function') ++ return str(ast) ++ ++ def _resolve_function(self, matches, args: Optional[cpp.ASTParametersQualifiers], project_info): ++ if not matches: ++ raise _NoMatchingFunctionError() ++ ++ res = [] ++ candSignatures = [] ++ for entry in matches: ++ text_options = {'no-link': u'', 'outline': u''} ++ ++ # Render the matches to docutils nodes ++ target_handler = create_target_handler({'no-link': u''}, ++ project_info, self.state.document) ++ filter_ = self.filter_factory.create_outline_filter(text_options) ++ mask_factory = MaskFactory({'param': NoParameterNamesMask}) ++ ++ # Override the directive args for this render ++ directive_args = self.directive_args[:] ++ directive_args[2] = text_options ++ ++ signature = self._create_function_signature(entry, project_info, filter_, ++ target_handler, ++ mask_factory, directive_args) ++ candSignatures.append(signature) ++ ++ if args is not None: ++ match = re.match(r"([^(]*)(.*)", signature) ++ assert match ++ _match_args = match.group(2) ++ ++ # Parse the text to find the arguments ++ # This one should succeed as it came from _create_function_signature ++ match_args = self._parse_args(_match_args) ++ ++ # Match them against the arg spec ++ if args != match_args: ++ continue ++ ++ res.append((entry, signature)) ++ ++ if len(res) == 1: ++ return res[0][0] ++ else: ++ raise _UnableToResolveFunctionError(candSignatures) +diff --git a/breathe/directive/index.py b/breathe/directives/index.py +similarity index 89% +rename from breathe/directive/index.py +rename to breathe/directives/index.py +index 80e7384..d460f74 100644 +--- a/breathe/directive/index.py ++++ b/breathe/directives/index.py +@@ -1,8 +1,7 @@ + from ..renderer.mask import NullMaskFactory +-from ..directive.base import BaseDirective ++from ..directives import BaseDirective + from ..project import ProjectError + from ..parser import ParserError, FileIOError +-from .base import create_warning + + from breathe.renderer import format_parser_error, RenderContext + from breathe.renderer.sphinxrenderer import SphinxRenderer +@@ -11,12 +10,11 @@ from breathe.renderer.target import create_target_handler + from docutils.parsers.rst.directives import unchanged_required, flag + + +-class RootDataObject(object): +- ++class RootDataObject: + node_type = "root" + + +-class BaseIndexDirective(BaseDirective): ++class _BaseIndexDirective(BaseDirective): + """Base class handle the main work when given the appropriate project info to work from. + """ + +@@ -25,7 +23,6 @@ class BaseIndexDirective(BaseDirective): + # pass way too much stuff to a helper object to be reasonable. + + def handle_contents(self, project_info): +- + try: + finder = self.finder_factory.create_finder(project_info) + except ParserError as e: +@@ -65,7 +62,7 @@ class BaseIndexDirective(BaseDirective): + return node_list + + +-class DoxygenIndexDirective(BaseIndexDirective): ++class DoxygenIndexDirective(_BaseIndexDirective): + required_arguments = 0 + optional_arguments = 2 + option_spec = { +@@ -73,7 +70,7 @@ class DoxygenIndexDirective(BaseIndexDirective): + "project": unchanged_required, + "outline": flag, + "no-link": flag, +- } ++ } + has_content = False + + def run(self): +@@ -82,21 +79,20 @@ class DoxygenIndexDirective(BaseIndexDirective): + try: + project_info = self.project_info_factory.create_project_info(self.options) + except ProjectError as e: +- warning = create_warning(None, self.state, self.lineno) ++ warning = self.create_warning(None) + return warning.warn('doxygenindex: %s' % e) + + return self.handle_contents(project_info) + + +-class AutoDoxygenIndexDirective(BaseIndexDirective): +- ++class AutoDoxygenIndexDirective(_BaseIndexDirective): + required_arguments = 0 + final_argument_whitespace = True + option_spec = { + "project": unchanged_required, + "outline": flag, + "no-link": flag, +- } ++ } + has_content = False + + def run(self): +@@ -107,7 +103,7 @@ class AutoDoxygenIndexDirective(BaseIndexDirective): + try: + project_info = self.project_info_factory.retrieve_project_info_for_auto(self.options) + except ProjectError as e: +- warning = create_warning(None, self.state, self.lineno) ++ warning = self.create_warning(None) + return warning.warn('autodoxygenindex: %s' % e) + + return self.handle_contents(project_info) +diff --git a/breathe/directives/item.py b/breathe/directives/item.py +new file mode 100644 +index 0000000..26f901c +--- /dev/null ++++ b/breathe/directives/item.py +@@ -0,0 +1,102 @@ ++from breathe.directives import BaseDirective ++from breathe.file_state_cache import MTimeError ++from breathe.project import ProjectError ++from breathe.renderer.filter import Filter ++from breathe.renderer.mask import NullMaskFactory ++from breathe.renderer.target import create_target_handler ++ ++from docutils.nodes import Node ++from docutils.parsers.rst.directives import unchanged_required, flag ++ ++from typing import Any, List, Optional, Type # noqa ++ ++ ++class _DoxygenBaseItemDirective(BaseDirective): ++ required_arguments = 1 ++ optional_arguments = 1 ++ option_spec = { ++ "path": unchanged_required, ++ "project": unchanged_required, ++ "outline": flag, ++ "no-link": flag, ++ } ++ has_content = False ++ ++ def create_finder_filter(self, namespace: str, name: str) -> Filter: ++ """Creates a filter to find the node corresponding to this item.""" ++ ++ return self.filter_factory.create_member_finder_filter( ++ namespace, name, self.kind) ++ ++ def run(self) -> List[Node]: ++ try: ++ namespace, name = self.arguments[0].rsplit("::", 1) ++ except ValueError: ++ namespace, name = "", self.arguments[0] ++ ++ try: ++ project_info = self.project_info_factory.create_project_info(self.options) ++ except ProjectError as e: ++ warning = self.create_warning(None, kind=self.kind) ++ return warning.warn('doxygen{kind}: %s' % e) ++ ++ try: ++ finder = self.finder_factory.create_finder(project_info) ++ except MTimeError as e: ++ warning = self.create_warning(None, kind=self.kind) ++ return warning.warn('doxygen{kind}: %s' % e) ++ ++ finder_filter = self.create_finder_filter(namespace, name) ++ ++ # TODO: find a more specific type for the Doxygen nodes ++ matches = [] # type: List[Any] ++ finder.filter_(finder_filter, matches) ++ ++ if len(matches) == 0: ++ display_name = "%s::%s" % (namespace, name) if namespace else name ++ warning = self.create_warning(project_info, kind=self.kind, ++ display_name=display_name) ++ return warning.warn('doxygen{kind}: Cannot find {kind} "{display_name}" {tail}') ++ ++ target_handler = create_target_handler(self.options, project_info, self.state.document) ++ filter_ = self.filter_factory.create_outline_filter(self.options) ++ ++ node_stack = matches[0] ++ mask_factory = NullMaskFactory() ++ return self.render(node_stack, project_info, filter_, target_handler, mask_factory, ++ self.directive_args) ++ ++ ++class DoxygenVariableDirective(_DoxygenBaseItemDirective): ++ kind = "variable" ++ ++ ++class DoxygenDefineDirective(_DoxygenBaseItemDirective): ++ kind = "define" ++ ++ ++class DoxygenEnumDirective(_DoxygenBaseItemDirective): ++ kind = "enum" ++ ++ ++class DoxygenEnumValueDirective(_DoxygenBaseItemDirective): ++ kind = "enumvalue" ++ ++ def create_finder_filter(self, namespace: str, name: str) -> Filter: ++ return self.filter_factory.create_enumvalue_finder_filter(name) ++ ++ ++class DoxygenTypedefDirective(_DoxygenBaseItemDirective): ++ kind = "typedef" ++ ++ ++class DoxygenUnionDirective(_DoxygenBaseItemDirective): ++ kind = "union" ++ ++ def create_finder_filter(self, namespace: str, name: str) -> Filter: ++ # Unions are stored in the xml file with their fully namespaced name ++ # We're using C++ namespaces here, it might be best to make this file ++ # type dependent ++ # ++ xml_name = "%s::%s" % (namespace, name) if namespace else name ++ return self.filter_factory.create_compound_finder_filter(xml_name, 'union') +diff --git a/breathe/directives/setup.py b/breathe/directives/setup.py +new file mode 100644 +index 0000000..fff6136 +--- /dev/null ++++ b/breathe/directives/setup.py +@@ -0,0 +1,133 @@ ++from breathe.directives import BaseDirective ++from breathe.directives.class_like import ( ++ DoxygenStructDirective, DoxygenClassDirective, DoxygenInterfaceDirective, ++) ++from breathe.directives.content_block import ( ++ DoxygenNamespaceDirective, DoxygenGroupDirective, DoxygenPageDirective, ++) ++from breathe.directives.file import DoxygenFileDirective, AutoDoxygenFileDirective ++from breathe.directives.function import DoxygenFunctionDirective ++from breathe.directives.index import DoxygenIndexDirective, AutoDoxygenIndexDirective ++from breathe.directives.item import ( ++ DoxygenVariableDirective, DoxygenDefineDirective, DoxygenUnionDirective, ++ DoxygenEnumDirective, DoxygenEnumValueDirective, DoxygenTypedefDirective, ++) ++from breathe.finder.factory import FinderFactory ++from breathe.parser import DoxygenParserFactory ++from breathe.project import ProjectInfoFactory ++from breathe.process import AutoDoxygenProcessHandle ++ ++from sphinx.application import Sphinx ++ ++import os ++import subprocess ++ ++from typing import Any, List, Optional, Type # noqa ++ ++ ++class DirectiveContainer: ++ def __init__(self, app: Sphinx, directive: Type[BaseDirective], ++ finder_factory: FinderFactory, project_info_factory: ProjectInfoFactory, ++ parser_factory: DoxygenParserFactory): ++ self.app = app ++ self.directive = directive ++ self.finder_factory = finder_factory ++ self.project_info_factory = project_info_factory ++ self.parser_factory = parser_factory ++ ++ # Required for sphinx to inspect ++ self.required_arguments = directive.required_arguments ++ self.optional_arguments = directive.optional_arguments ++ self.option_spec = directive.option_spec ++ self.has_content = directive.has_content ++ self.final_argument_whitespace = directive.final_argument_whitespace ++ ++ def __call__(self, *args): ++ call_args = [ ++ self.finder_factory, ++ self.project_info_factory, ++ self.parser_factory ++ ] ++ call_args.extend(args) ++ return self.directive(*call_args) ++ ++ ++def setup(app: Sphinx) -> None: ++ directives = { ++ "doxygenindex": DoxygenIndexDirective, ++ "autodoxygenindex": AutoDoxygenIndexDirective, ++ "doxygenfunction": DoxygenFunctionDirective, ++ "doxygenstruct": DoxygenStructDirective, ++ "doxygenclass": DoxygenClassDirective, ++ "doxygeninterface": DoxygenInterfaceDirective, ++ "doxygenvariable": DoxygenVariableDirective, ++ "doxygendefine": DoxygenDefineDirective, ++ "doxygenenum": DoxygenEnumDirective, ++ "doxygenenumvalue": DoxygenEnumValueDirective, ++ "doxygentypedef": DoxygenTypedefDirective, ++ "doxygenunion": DoxygenUnionDirective, ++ "doxygennamespace": DoxygenNamespaceDirective, ++ "doxygengroup": DoxygenGroupDirective, ++ "doxygenfile": DoxygenFileDirective, ++ "autodoxygenfile": AutoDoxygenFileDirective, ++ "doxygenpage": DoxygenPageDirective, ++ } ++ ++ # note: the parser factory contains a cache of the parsed XML ++ # note: the project_info_factory also contains some caching stuff ++ # TODO: is that actually safe for when reading in parallel? ++ project_info_factory = ProjectInfoFactory(app) ++ parser_factory = DoxygenParserFactory(app) ++ finder_factory = FinderFactory(app, parser_factory) ++ for name, directive in directives.items(): ++ # ordinarily app.add_directive takes a class it self, but we need to inject extra arguments ++ # so we give a DirectiveContainer object which has an overloaded __call__ operator. ++ app.add_directive(name, DirectiveContainer( # type: ignore ++ app, ++ directive, ++ finder_factory, ++ project_info_factory, ++ parser_factory ++ )) ++ ++ app.add_config_value("breathe_projects", {}, True) # Dict[str, str] ++ app.add_config_value("breathe_default_project", "", True) # str ++ # Provide reasonable defaults for domain_by_extension mapping. Can be overridden by users. ++ app.add_config_value("breathe_domain_by_extension", ++ {'py': 'py', 'cs': 'cs'}, True) # Dict[str, str] ++ app.add_config_value("breathe_domain_by_file_pattern", {}, True) # Dict[str, str] ++ app.add_config_value("breathe_projects_source", {}, True) ++ app.add_config_value("breathe_build_directory", '', True) ++ app.add_config_value("breathe_default_members", (), True) ++ app.add_config_value("breathe_show_define_initializer", False, 'env') ++ app.add_config_value("breathe_show_enumvalue_initializer", False, 'env') ++ app.add_config_value("breathe_implementation_filename_extensions", ['.c', '.cc', '.cpp'], True) ++ app.add_config_value("breathe_doxygen_config_options", {}, True) ++ app.add_config_value("breathe_use_project_refids", False, "env") ++ app.add_config_value("breathe_order_parameters_first", False, 'env') ++ app.add_config_value("breathe_separate_member_pages", False, 'env') ++ ++ breathe_css = "breathe.css" ++ if (os.path.exists(os.path.join(app.confdir, "_static", breathe_css))): # type: ignore ++ app.add_css_file(breathe_css) ++ ++ def write_file(directory, filename, content): ++ # Check the directory exists ++ if not os.path.exists(directory): ++ os.makedirs(directory) ++ ++ # Write the file with the provided contents ++ with open(os.path.join(directory, filename), "w") as f: ++ f.write(content) ++ ++ doxygen_handle = AutoDoxygenProcessHandle( ++ subprocess.check_call, ++ write_file, ++ project_info_factory) ++ ++ def doxygen_hook(app): ++ doxygen_handle.generate_xml( ++ app.config.breathe_projects_source, ++ app.config.breathe_doxygen_config_options ++ ) ++ app.connect("builder-inited", doxygen_hook) +diff --git a/breathe/renderer/sphinxrenderer.py b/breathe/renderer/sphinxrenderer.py +index 8c16d45..07ae46c 100644 +--- a/breathe/renderer/sphinxrenderer.py ++++ b/breathe/renderer/sphinxrenderer.py +@@ -2019,8 +2019,8 @@ class SphinxRenderer: + signode += nodes.Text(node.name) + return [desc] + +- def visit_param(self, node: compound.paramTypeSub, *, +- insertDeclNameByParsing: bool = False) -> List[Node]: ++ def visit_templateparam(self, node: compound.paramTypeSub, *, ++ insertDeclNameByParsing: bool = False) -> List[Node]: + nodelist = [] + + # Parameter type +@@ -2048,15 +2048,22 @@ class SphinxRenderer: + ''.join(n.astext() for n in nodelist), + location=self.state.state_machine.get_source_and_line(), + config=self.app.config) +- ast = parser._parse_type(named='single', outer='templateParam') +- assert ast.name is None +- nn = cpp.ASTNestedName( +- names=[cpp.ASTNestedNameElement(cpp.ASTIdentifier(node.declname), None)], +- templates=[False], rooted=False) +- ast.name = nn +- # the actual nodes don't matter, as it is astext()-ed later +- nodelist = [nodes.Text(str(ast))] +- appendDeclName = False ++ try: ++ # we really should use _parse_template_parameter() ++ # but setting a name there is non-trivial, so we use type ++ ast = parser._parse_type(named='single', outer='templateParam') ++ assert ast.name is None ++ nn = cpp.ASTNestedName( ++ names=[cpp.ASTNestedNameElement( ++ cpp.ASTIdentifier(node.declname), None)], ++ templates=[False], rooted=False) ++ ast.name = nn ++ # the actual nodes don't matter, as it is astext()-ed later ++ nodelist = [nodes.Text(str(ast))] ++ appendDeclName = False ++ except cpp.DefinitionError: ++ # happens with "typename ...Args", so for now, just append ++ pass + + if appendDeclName: + if nodelist: +@@ -2086,7 +2093,7 @@ class SphinxRenderer: + for i, item in enumerate(node.param): + if i: + nodelist.append(nodes.Text(", ")) +- nodelist.extend(self.visit_param(item, insertDeclNameByParsing=True)) ++ nodelist.extend(self.visit_templateparam(item, insertDeclNameByParsing=True)) + self.output_defname = True + return nodelist + +@@ -2225,7 +2232,6 @@ class SphinxRenderer: + "compoundref": visit_compoundref, + "mixedcontainer": visit_mixedcontainer, + "description": visit_description, +- "param": visit_param, + "templateparamlist": visit_templateparamlist, + "docparamlist": visit_docparamlist, + "docxrefsect": visit_docxrefsect, +diff --git a/tests/test_renderer.py b/tests/test_renderer.py +index c2c8423..cf6ab7f 100644 +--- a/tests/test_renderer.py ++++ b/tests/test_renderer.py +@@ -422,7 +422,7 @@ def test_render_innergroup(app): + options=['inner'])) + + def get_directive(app): +- from breathe.directives import DoxygenFunctionDirective ++ from breathe.directives.function import DoxygenFunctionDirective + from breathe.project import ProjectInfoFactory + from breathe.parser import DoxygenParserFactory + from breathe.finder.factory import FinderFactory +-- +2.31.1 + diff --git a/python-breathe.spec b/python-breathe.spec index 1b0c0fc..97ffc8d 100644 --- a/python-breathe.spec +++ b/python-breathe.spec @@ -14,6 +14,7 @@ URL: https://github.com/%{owner}/%{srcname} Source0: %{URL}/archive/v%{version}.tar.gz Source1: %{URL}/releases/download/v%{version}/%{srcname}-%{version}.tar.gz.sig Source2: https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x8aed58021feacdd5f27ba0e6a72f627716ea9d96#./vermware.key +Patch0: 0001-Handle-parsing-errors.patch BuildArch: noarch