You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
293 lines
11 KiB
293 lines
11 KiB
8 months ago
|
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
|
||
|
|
||
|
|
||
|
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]:
|
||
|
# Extract namespace, function name, and parameters
|
||
|
# Regex explanation:
|
||
|
# 1. (?:<something>::)?
|
||
|
# Optional namespace prefix, including template arguments if a specialization.
|
||
|
# The <something> is group 1:
|
||
|
# 1. [^:(<]+, basically an identifier
|
||
|
# definitely not a scope operator, ::, or template argument list, <
|
||
|
# 2. (?:::[^:(<]+)*, (?:<stuff>) for anon match group,
|
||
|
# so a namespace delimiter and then another identifier
|
||
|
# 3. ::, another namespace delimiter before the function name
|
||
|
# 2. ([^(]+), group 2, the function name, whatever remains after the optional prefix,
|
||
|
# until a (.
|
||
|
# 3. (.*), group 3, the parameters.
|
||
|
# Note: for template argument lists, the spacing is important for the Doxygen lookup.
|
||
|
# TODO: we should really do this parsing differently, e.g., using the Sphinx C++ domain.
|
||
|
# TODO: the Doxygen lookup should not be whitespace sensitive.
|
||
|
match = re.match(r"(?:([^:(<]+(?:::[^:(<]+)*)::)?([^(]+)(.*)", self.arguments[0])
|
||
|
assert match is not None # TODO: this is probably not appropriate, for now it fixes typing
|
||
|
namespace = (match.group(1) or "").strip()
|
||
|
function_name = match.group(2).strip()
|
||
|
argsStr = match.group(3)
|
||
|
|
||
|
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(argsStr)
|
||
|
except cpp.DefinitionError as e:
|
||
|
return self.create_warning(
|
||
|
project_info,
|
||
|
namespace="%s::" % namespace if namespace else "",
|
||
|
function=function_name,
|
||
|
args=argsStr,
|
||
|
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: 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")
|
||
|
# strip everything that doesn't contribute to overloading
|
||
|
|
||
|
def stripParamQual(paramQual):
|
||
|
paramQual.exceptionSpec = None
|
||
|
paramQual.final = None
|
||
|
paramQual.override = None
|
||
|
# TODO: strip attrs when Doxygen handles them
|
||
|
paramQual.initializer = None
|
||
|
paramQual.trailingReturn = None
|
||
|
for p in paramQual.args:
|
||
|
if p.arg is None:
|
||
|
assert p.ellipsis
|
||
|
continue
|
||
|
p.arg.init = None
|
||
|
declarator = p.arg.type.decl
|
||
|
|
||
|
def stripDeclarator(declarator):
|
||
|
if hasattr(declarator, "next"):
|
||
|
stripDeclarator(declarator.next)
|
||
|
if isinstance(declarator, cpp.ASTDeclaratorParen):
|
||
|
assert hasattr(declarator, "inner")
|
||
|
stripDeclarator(declarator.inner)
|
||
|
else:
|
||
|
assert isinstance(declarator, cpp.ASTDeclaratorNameParamQual)
|
||
|
assert hasattr(declarator, "declId")
|
||
|
declarator.declId = None
|
||
|
if declarator.paramQual is not None:
|
||
|
stripParamQual(declarator.paramQual)
|
||
|
|
||
|
stripDeclarator(declarator)
|
||
|
|
||
|
stripParamQual(paramQual)
|
||
|
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": "", "outline": ""}
|
||
|
|
||
|
# Render the matches to docutils nodes
|
||
|
target_handler = create_target_handler(
|
||
|
{"no-link": ""}, 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)
|