Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doxygenfunction fixes #623

Merged
merged 3 commits into from
Jan 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 53 additions & 52 deletions breathe/directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import re
import subprocess

from typing import Any, List, Type # noqa
from typing import Any, List, Optional, Type # noqa


class NoMatchingFunctionError(BreatheError):
Expand Down Expand Up @@ -76,13 +76,22 @@ def run(self) -> List[Node]:
return warning.warn('doxygenfunction: %s' % e)

# Extract arguments from the function name.
args = self.parse_args(args)
args = self._parse_args(args)

finder_filter = self.filter_factory.create_function_finder_filter(namespace, function_name)
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
matches = [] # type: List[Any]
finder.filter_(finder_filter, matches)
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
Expand All @@ -96,39 +105,26 @@ def run(self) -> List[Node]:
)

try:
node_stack = self.resolve_function(matches, args, project_info)
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 multiple matches for function ' \
'"{namespace}{function}" with arguments ({args}) {tail}.\n' \
message = 'doxygenfunction: Unable to resolve function ' \
'"{namespace}{function}" with arguments {args} {tail}.\n' \
'Potential matches:\n'

# We want to create a string for the console output and a set of docutils nodes
# for rendering into the final output. We handle the final output as a literal string
# with a txt based list of the options.
literal_text = ''

# TODO: We're cheating here with the set() as signatures has repeating entries for some
# reason (failures in the matcher_stack code) so we consolidate them by shoving them in
# a set to remove duplicates. Should be fixed!
signatures = ''
for i, entry in enumerate(sorted(set(error.signatures))):
if i:
literal_text += '\n'
# Replace new lines with a new line & enough spacing to reach the appropriate
# alignment for our simple plain text list
literal_text += '- %s' % entry.replace('\n', '\n ')
signatures += ' - %s\n' % entry.replace('\n', '\n ')
block = nodes.literal_block('', '', nodes.Text(literal_text))
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=signatures)
unformatted_suffix=text)
return result

target_handler = create_target_handler(self.options, project_info, self.state.document)
Expand All @@ -137,9 +133,9 @@ def run(self) -> List[Node]:
return self.render(node_stack, project_info, filter_, target_handler, NullMaskFactory(),
self.directive_args)

def parse_args(self, function_description):
def _parse_args(self, function_description: str) -> Optional[cpp.ASTParametersQualifiers]:
if function_description == '':
function_description = '()'
return None

parser = cpp.DefinitionParser(function_description,
location=self.get_source_info(),
Expand All @@ -152,14 +148,14 @@ def parse_args(self, function_description):
continue
declarator = p.arg.type.decl
while hasattr(declarator, 'next'):
declarator = declarator.next
declarator = declarator.next # type: ignore
assert hasattr(declarator, 'declId')
declarator.declId = None
p.arg.init = None
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):
def _create_function_signature(self, node_stack, project_info, filter_, target_handler,
mask_factory, directive_args) -> str:
"Standard render process used by subclasses"

try:
Expand All @@ -177,7 +173,8 @@ def create_function_signature(self, node_stack, project_info, filter_, target_ha
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)
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]
Expand All @@ -197,16 +194,12 @@ def create_function_signature(self, node_stack, project_info, filter_, target_ha
ast = parser.parse_declaration('function', 'function')
return str(ast)

def resolve_function(self, matches, args, project_info):
def _resolve_function(self, matches, args: Optional[cpp.ASTParametersQualifiers], project_info):
if not matches:
raise NoMatchingFunctionError()

if len(matches) == 1:
return matches[0]

signatures = []

# Iterate over the potential matches
res = []
candSignatures = []
for entry in matches:
text_options = {'no-link': u'', 'outline': u''}

Expand All @@ -220,21 +213,29 @@ def resolve_function(self, matches, args, project_info):
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)
signatures.append(signature)
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)

match = re.match(r"([^(]*)(.*)", signature)
_match_args = match.group(2)
# Parse the text to find the arguments
match_args = self._parse_args(_match_args)

# 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

# Match them against the arg spec
if args == match_args:
return entry
res.append((entry, signature))

raise UnableToResolveFunctionError(signatures)
if len(res) == 1:
return res[0][0]
else:
raise UnableToResolveFunctionError(candSignatures)


class _DoxygenClassLikeDirective(BaseDirective):
Expand Down
5 changes: 3 additions & 2 deletions breathe/renderer/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1026,16 +1026,17 @@ def create_member_finder_filter(self, namespace: str, name: str, kind: str) -> F
return (parent_is_compound & parent_is_file & node_matches) \
| (parent_is_compound & parent_is_not_file & node_matches)

def create_function_finder_filter(self, namespace: str, name: str) -> Filter:
def create_function_and_all_friend_finder_filter(self, namespace: str, name: str) -> Filter:
parent = Parent()
parent_is_compound = parent.node_type == 'compound'
parent_is_group = parent.kind == 'group'

function_filter = self.create_member_finder_filter(namespace, name, 'function')
friend_filter = self.create_member_finder_filter(namespace, name, 'friend')
# Get matching functions but only ones where the parent is not a group. We want to skip
# function entries in groups as we'll find the same functions in a file's xml output
# elsewhere and having more than one match is confusing for our logic later on.
return function_filter & ~(parent_is_compound & parent_is_group)
return (function_filter | friend_filter) & ~(parent_is_compound & parent_is_group)

def create_enumvalue_finder_filter(self, name: str) -> Filter:
"""Returns a filter which looks for an enumvalue with the specified name."""
Expand Down
14 changes: 8 additions & 6 deletions breathe/renderer/sphinxrenderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1606,12 +1606,14 @@ def visit_function(self, node) -> List[Node]:
if dom == 'py':
declaration = name + node.get_argsstring()
else:
declaration = ' '.join([
self.create_template_prefix(node),
''.join(n.astext() for n in self.render(node.get_type())), # type: ignore
name,
node.get_argsstring()
])
elements = [self.create_template_prefix(node)]
if node.kind == 'friend':
elements.append('friend')
elements.append(''.join(n.astext()
for n in self.render(node.get_type()))) # type: ignore
elements.append(name)
elements.append(node.get_argsstring())
declaration = ' '.join(elements)
nodes = self.handle_declaration(node, declaration)
return nodes
else:
Expand Down
8 changes: 4 additions & 4 deletions tests/test_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,14 +457,14 @@ def test_resolve_overrides(app):

# Verify that the exact arguments returns one override
for args in argsstrings:
ast_param = cls.parse_args(args)
ret = cls.resolve_function(matches, ast_param, None)
ast_param = cls._parse_args(args)
ret = cls._resolve_function(matches, ast_param, None)

def test_ellipsis(app):
argsstrings, matches = get_matches('ellipsis.xml')
cls = get_directive(app)

# Verify that parsing an ellipsis works
ast_param = cls.parse_args(argsstrings[0])
ret = cls.resolve_function(matches, ast_param, None)
ast_param = cls._parse_args(argsstrings[0])
ret = cls._resolve_function(matches, ast_param, None)