From 62346bf27de8d84e12f85a66c1ecb7b3aba9f3ac Mon Sep 17 00:00:00 2001 From: Mark Gillard Date: Wed, 12 Oct 2022 14:23:55 +0300 Subject: [PATCH] XML v2 work - added support for `` - added support for all exposition-only markup - added additional validation - started work on the solve for #15 --- poxy/doxygen.py | 863 +++++++++++++----- poxy/graph.py | 630 +++++++++---- poxy/run.py | 34 +- poxy/utils.py | 11 + tests/regenerate_tests.py | 8 +- .../expected_html/Test!.tagfile.xml | 150 ++- .../expected_html/classtest_1_1class__1.html | 114 ++- tests/test_project/expected_html/code_8h.html | 46 +- .../dir_68267d1309a1af8e8297ef4c3efbcdba.html | 8 + .../dir_ed64655242b001a1b5d7ddadcfdd4bf2.html | 134 +++ tests/test_project/expected_html/files.html | 6 + .../expected_html/namespacetest.html | 144 ++- .../expected_html/searchdata-v2.js | 2 +- .../expected_html/subfolder_2code_8h.html | 117 +++ .../expected_xml/Test!.tagfile.xml | 150 ++- .../expected_xml/classtest_1_1class__1.xml | 212 ++++- .../classtest_1_1template__class__1.xml | 2 +- tests/test_project/expected_xml/code_8h.xml | 162 +++- .../concepttest_1_1concept__1.xml | 2 +- .../concepttest_1_1nested_1_1concept__2.xml | 2 +- .../dir_68267d1309a1af8e8297ef4c3efbcdba.xml | 1 + .../dir_ed64655242b001a1b5d7ddadcfdd4bf2.xml | 13 + tests/test_project/expected_xml/index.xml | 45 +- .../expected_xml/namespacetest.xml | 162 +++- .../expected_xml/namespacetest_1_1empty.xml | 2 +- .../expected_xml/namespacetest_1_1nested.xml | 2 +- .../expected_xml/structtest_1_1struct__1.xml | 2 + .../expected_xml/subfolder_2code_8h.xml | 13 + tests/test_project/src/code.h | 73 +- .../src/empty_subfolder/folder.dox | 3 + tests/test_project/src/subfolder/code.h | 4 + 31 files changed, 2631 insertions(+), 486 deletions(-) create mode 100644 tests/test_project/expected_html/dir_ed64655242b001a1b5d7ddadcfdd4bf2.html create mode 100644 tests/test_project/expected_html/subfolder_2code_8h.html create mode 100644 tests/test_project/expected_xml/dir_ed64655242b001a1b5d7ddadcfdd4bf2.xml create mode 100644 tests/test_project/expected_xml/subfolder_2code_8h.xml create mode 100644 tests/test_project/src/empty_subfolder/folder.dox create mode 100644 tests/test_project/src/subfolder/code.h diff --git a/poxy/doxygen.py b/poxy/doxygen.py index 9b02d13..ed1f7dd 100644 --- a/poxy/doxygen.py +++ b/poxy/doxygen.py @@ -235,6 +235,16 @@ def __str__(self) -> str: +class Virt(object): + + def __init__(self, value: bool): + self.__value = bool(value) + + def __str__(self) -> str: + return r'virtual' if self.__value else r'non-virtual' + + + #======================================================================================================================= # XML <=> Graph #======================================================================================================================= @@ -256,7 +266,7 @@ def __str__(self) -> str: r'variable': graph.Variable, r'function': graph.Function, r'define': graph.Define, - r'page': graph.Article + r'page': graph.Page } NODE_TYPES_TO_KINDS = {t: k for k, t in KINDS_TO_NODE_TYPES.items()} COMPOUND_NODE_TYPES = {KINDS_TO_NODE_TYPES[c] for c in COMPOUNDS} @@ -264,21 +274,13 @@ def __str__(self) -> str: -def _to_kind(node_type) -> str: - if node_type is None: - return None - global NODE_TYPES_TO_KINDS - assert node_type in NODE_TYPES_TO_KINDS - return NODE_TYPES_TO_KINDS[node_type] - - - -def _to_node_type(kind: str): - if kind is None: - return None - global KINDS_TO_NODE_TYPES - assert kind in KINDS_TO_NODE_TYPES - return KINDS_TO_NODE_TYPES[kind] +def _ordered(*types) -> list: + assert types is not None + assert types + types = [*types] + types.sort(key=lambda t: t.__name__) + types = tuple(types) + return types @@ -316,25 +318,36 @@ def parse_structured_text(node: graph.Node, elem): if elem.text: text = g.get_or_create_node(type=graph.Text) text.text = elem.text - node.connect_to(text) + node.add(text) # child for child_elem in elem: if child_elem.tag == r'para': para = g.get_or_create_node(type=graph.Paragraph) parse_structured_text(para, child_elem) - node.connect_to(para) + node.add(para) elif child_elem.tag == r'ref': - ref = g.get_or_create_node(type=graph.Text) + ref = g.get_or_create_node(type=graph.Reference) ref.text = child_elem.text - ref.reference_id = child_elem.get(r'refid') - node.connect_to(ref) + ref.kind = child_elem.get(r'kindref') + resource = g.get_or_create_node(id=child_elem.get(r'refid')) + if child_elem.get(r'external'): + resource.type = graph.ExternalResource + resource.file = child_elem.get(r'external') + ref.add(resource) + node.add(ref) else: - raise Error(rf'Unknown <{elem.tag}> child element <{child_elem.tag}>') + markup = g.get_or_create_node(type=graph.ExpositionMarkup) + markup.tag = child_elem.tag + attrs = [(k, v) for k, v in child_elem.attrib.items()] + attrs.sort(key=lambda kvp: kvp[0]) + markup.extra_attributes = tuple(attrs) + parse_structured_text(markup, child_elem) + node.add(markup) # text that came after the child if child_elem.tail: text = g.get_or_create_node(type=graph.Text) text.text = child_elem.tail - node.connect_to(text) + node.add(text) def parse_text_subnode(node: graph.Node, subnode_type, elem, subelem_tag: str): assert node is not None @@ -348,127 +361,222 @@ def parse_text_subnode(node: graph.Node, subnode_type, elem, subelem_tag: str): return nonlocal g subnode = g.get_or_create_node(type=subnode_type) - node.connect_to(subnode) + node.add(subnode) parse_structured_text(subnode, subelem) - def parse_brief(node, elem): + def parse_brief(node: graph.Node, elem): parse_text_subnode(node, graph.BriefDescription, elem, r'briefdescription') - def parse_detail(node, elem): + def parse_detail(node: graph.Node, elem): parse_text_subnode(node, graph.DetailedDescription, elem, r'detaileddescription') - def parse_initializer(node, elem): + def parse_initializer(node: graph.Node, elem): parse_text_subnode(node, graph.Initializer, elem, r'initializer') + def parse_type(node: graph.Node, elem, resolve_auto_as=None): + assert node is not None + assert elem is not None + if graph.Type in node: + return + type_elem = elem.find(r'type') + if type_elem is None: + return + # extract constexpr, constinit, static, mutable etc out of the type of doxygen has leaked it + while type_elem.text: + text = rf' {type_elem.text} ' + match = re.search(r'\s(?:(?:const(?:expr|init|eval)|static|mutable|explicit|virtual|inline)\s)+', text) + if match is None: + break + type_elem.text = (text[:match.start()] + r' ' + text[match.end():]).strip() + if match[0].find(r'constexpr') != -1: + node.constexpr = True + if match[0].find(r'constinit') != -1: + node.constinit = True + if match[0].find(r'consteval') != -1: + node.consteval = True + if match[0].find(r'static') != -1: + node.static = True + if match[0].find(r'mutable') != -1: + node.mutable = True + if match[0].find(r'explicit') != -1: + node.explicit = True + if match[0].find(r'virtual') != -1: + node.virtual = True + if match[0].find(r'inline') != -1: + node.inline = True + if type_elem.text == r'auto' and resolve_auto_as is not None: + type_elem.text = resolve_auto_as + parse_text_subnode(node, graph.Type, elem, r'type') + + def parse_location(node: graph.Node, elem): + location = elem.find(r'location') + if location is None: + return + node.file = location.get(r'file') + try: + node.line = location.get(r'line') + except: + pass + node.column = location.get(r'column') + attrs = [] + for k, v in location.attrib.items(): + if k not in (r'file', r'line', r'column'): + attrs.append((k, v)) + attrs.sort(key=lambda kvp: kvp[0]) + node.extra_attributes = tuple(attrs) + # - # (these are doxygen's version of 'forward declarations') + # (these are doxygen's version of 'forward declarations', typically found in index.xml) for compound in root.findall(r'compound'): - node = g.get_or_create_node(id=compound.get(r'refid'), type=_to_node_type(compound.get(r'kind'))) - node.qualified_name = extract_qualified_name(compound) + node = g.get_or_create_node(id=compound.get(r'refid'), type=KINDS_TO_NODE_TYPES[compound.get(r'kind')]) + + if node.type is graph.File: # files use their local name?? doxygen is so fucking weird + node.local_name = tail( + extract_subelement_text(compound, r'name').strip().replace('\\', r'/').rstrip(r'/'), # + r'/' + ) + else: + node.qualified_name = extract_subelement_text(compound, r'name') # for member_elem in compound.findall(rf'member'): member_kind = member_elem.get(r'kind') if member_kind == r'enumvalue': continue - member = g.get_or_create_node(id=member_elem.get(r'refid'), type=_to_node_type(member_kind)) - node.connect_to(member) - # manually rebuild the fully-qualified name + member = g.get_or_create_node(id=member_elem.get(r'refid'), type=KINDS_TO_NODE_TYPES[member_kind]) + node.add(member) name = extract_subelement_text(member_elem, r'name') if name: - if node.node_type is graph.Directory and member.node_type in (graph.Directory, graph.File): - member.qualified_name = rf'{node.qualified_name}/{name}' - elif ( - node.node_type not in (graph.Directory, graph.File) # - and member.node_type not in (graph.Directory, graph.File) - ): - member.qualified_name = rf'{node.qualified_name}::{name}' + if member.type is graph.Define: + member.local_name = name + member.qualified_name = name + elif node.type not in (graph.Directory, graph.File): + member.local_name = name + if node.qualified_name: + member.qualified_name = rf'{node.qualified_name}::{name}' # for compounddef in root.findall(r'compounddef'): - node = g.get_or_create_node(id=compounddef.get(r'id'), type=_to_node_type(compounddef.get(r'kind'))) - node.qualified_name = extract_qualified_name(compounddef) + node = g.get_or_create_node(id=compounddef.get(r'id'), type=KINDS_TO_NODE_TYPES[compounddef.get(r'kind')]) node.access_level = compounddef.get(r'prot') - - def get_all_memberdefs(kind: str, *sectiondef_kinds): - nonlocal compounddef - memberdefs = [ - s for s in compounddef.findall(r'sectiondef') if (s.get(r'kind') in {kind, *sectiondef_kinds}) - ] - memberdefs = [s.findall(r'memberdef') for s in memberdefs] # list of lists of memberdefs - memberdefs = list(itertools.chain.from_iterable(memberdefs)) # list of memberdefs - return [m for m in memberdefs if m.get(r'kind') == kind] # matching memberdefs - - def get_all_type_memberdefs(kind: str): - return get_all_memberdefs(kind, r'public-type', r'protected-type', r'private-type') - - def get_all_attrib_memberdefs(kind: str): - return get_all_memberdefs( - kind, # - r'var', - r'public-static-attrib', - r'protected-static-attrib', - r'private-static-attrib', - r'public-attrib', - r'protected-attrib', - r'private-attrib' - ) + parse_brief(node, compounddef) + parse_detail(node, compounddef) + parse_initializer(node, compounddef) + parse_location(node, compounddef) + parse_type(node, compounddef) + + # qualified name + qualified_name = extract_subelement_text(compounddef, r'qualifiedname') + qualified_name = qualified_name.strip() if qualified_name is not None else r'' + if not qualified_name and node.type in (graph.Directory, graph.File): + qualified_name = compounddef.find(r'location') + qualified_name = qualified_name.get(r'file') if qualified_name is not None else r'' + qualified_name = qualified_name.rstrip(r'/') + if not qualified_name: + qualified_name = extract_qualified_name(compounddef) + node.qualified_name = qualified_name + + # get all memberdefs in one flat list + memberdefs = [compounddef] + memberdefs += [s for s in compounddef.findall(r'sectiondef')] + memberdefs = [s.findall(r'memberdef') for s in memberdefs] # list of lists of memberdefs + memberdefs = list(itertools.chain.from_iterable(memberdefs)) # list of memberdefs + + def get_memberdefs(kind: str): + nonlocal memberdefs + return [m for m in memberdefs if m.get(r'kind') == kind] + + # all + for elem in memberdefs: + member = g.get_or_create_node(id=elem.get(r'id')) + parse_brief(member, elem) + parse_detail(member, elem) + parse_initializer(member, elem) + parse_location(member, elem) + member.access_level = elem.get(r'prot') + member.static = elem.get(r'static') + member.const = elem.get(r'const') + member.constexpr = elem.get(r'constexpr') + member.consteval = elem.get(r'consteval') + member.inline = elem.get(r'inline') + member.explicit = elem.get(r'explicit') + member.virtual = True if elem.get(r'virtual') == r'virtual' else None + member.strong = elem.get(r'strong') + member.definition = extract_subelement_text(elem, r'definition') + node.add(member) + + # fix trailing return types in some situations (https://github.com/mosra/m.css/issues/94) + trailing_return_type = None + if elem.get(r'kind') == r'function': + type_elem = elem.find(r'type') + args_elem = elem.find(r'argsstring') + if type_elem is not None and type_elem.text and args_elem is not None and args_elem.text: + match = re.search(r'^(.*?)\s*->\s*([a-zA-Z][a-zA-Z0-9_::*& <>]+)\s*$', args_elem.text) + if match: + args_elem.text = str(match[1]) + trailing_return_type = str(match[2]).strip() + + parse_type(member, elem, resolve_auto_as=trailing_return_type) # enums - for enum_elem in get_all_type_memberdefs(r'enum'): - enum = g.get_or_create_node(id=enum_elem.get(r'id'), type=graph.Enum) - enum.access_level = enum_elem.get(r'prot') - enum.strong = enum_elem.get(r'strong') - enum.static = enum_elem.get(r'static') - enum.local_name = extract_subelement_text(enum_elem, r'name') - enum.qualified_name = extract_qualified_name(enum_elem) - parse_brief(enum, enum_elem) - parse_detail(enum, enum_elem) - node.connect_to(enum) - for value_elem in enum_elem.findall(r'enumvalue'): + for elem in get_memberdefs(r'enum'): + member = g.get_or_create_node(id=elem.get(r'id'), type=graph.Enum) + member.local_name = extract_subelement_text(elem, r'name') + member.qualified_name = extract_qualified_name(elem) + node.add(member) + for value_elem in elem.findall(r'enumvalue'): value = g.get_or_create_node(id=value_elem.get(r'id'), type=graph.EnumValue) value.access_level = value_elem.get(r'prot') value.local_name = extract_subelement_text(value_elem, r'name') parse_brief(value, value_elem) parse_detail(value, value_elem) parse_initializer(value, value_elem) - enum.connect_to(value) + parse_location(value, value_elem) + member.add(value) + + # typedefs + for elem in get_memberdefs(r'typedef'): + member = g.get_or_create_node(id=elem.get(r'id'), type=graph.Typedef) + member.local_name = extract_subelement_text(elem, r'name') + member.qualified_name = extract_qualified_name(elem) + node.add(member) # vars - for var_elem in get_all_attrib_memberdefs(r'variable'): - var = g.get_or_create_node(id=var_elem.get(r'id'), type=graph.Variable) - var.access_level = var_elem.get(r'prot') - var.static = var_elem.get(r'static') - var.constexpr = var_elem.get(r'constexpr') - var.constinit = var_elem.get(r'constinit') - var.mutable = var_elem.get(r'mutable') - var.type = extract_subelement_text(var_elem, r'type') - var.definition = extract_subelement_text(var_elem, r'definition') - var.local_name = extract_subelement_text(var_elem, r'name') - var.qualified_name = extract_qualified_name(var_elem) - parse_brief(var, var_elem) - parse_detail(var, var_elem) - parse_initializer(var, var_elem) - node.connect_to(var) - - # - for inner_suffix in (r'namespace', r'class', r'concept', r'dir', r'file'): + for elem in get_memberdefs(r'variable'): + member = g.get_or_create_node(id=elem.get(r'id'), type=graph.Variable) + member.local_name = extract_subelement_text(elem, r'name') + member.qualified_name = extract_qualified_name(elem) + node.add(member) + + # functions + for elem in get_memberdefs(r'function'): + member = g.get_or_create_node(id=elem.get(r'id'), type=graph.Function) + node.add(member) + + # + for inner_suffix in (r'dir', r'file', r'class', r'namespace', r'page', r'group', r'concept'): for inner_elem in compounddef.findall(rf'inner{inner_suffix}'): inner = g.get_or_create_node(id=inner_elem.get(r'refid')) if inner_suffix == r'class': if inner.id.startswith(r'class'): - inner.node_type = graph.Class + inner.type = graph.Class elif inner.id.startswith(r'struct'): - inner.node_type = graph.Struct + inner.type = graph.Struct elif inner.id.startswith(r'union'): - inner.node_type = graph.Union + inner.type = graph.Union + elif node.type in (graph.Class, graph.Struct, graph.Union) and inner_suffix == r'group': + inner.type = graph.MemberGroup else: - inner.node_type = _to_node_type(inner_suffix) - inner.qualified_name = inner_elem.text - node.connect_to(inner) - - # deduce any missing qualified_names - # for node in g(graph.Namespace, graph.Class, graph.Struct, graph.Union): + inner.type = KINDS_TO_NODE_TYPES[inner_suffix] + if node.type is graph.Directory: + if inner.type is graph.Directory: + inner.qualified_name = inner_elem.text + else: + assert inner.type is graph.File + inner.qualified_name = rf'{node.qualified_name}/{inner_elem.text}' + elif node.type in graph.CPP_TYPES and inner.type in graph.CPP_TYPES: + inner.qualified_name = inner_elem.text + node.add(inner) @@ -483,8 +591,106 @@ def read_graph_from_xml(folder, log_func=None) -> graph.Graph: encoding=r'utf-8' ) g = graph.Graph() + + # parse files for path in get_all_files(folder, all=r"*.xml"): - _parse_xml_file(g=g, path=path, parser=parser, log_func=log_func) + try: + _parse_xml_file(g=g, path=path, parser=parser, log_func=log_func) + except KeyError: + raise + except Exception as ex: + raise Error(rf'Parsing {path.name} failed: {ex}') + + # deduce any missing qualified_names for C++ constructs + again = True + while again: + again = False + for namespace in g(graph.Namespace, graph.Class, graph.Struct, graph.Union, graph.Enum): + if not namespace.qualified_name: + continue + for member in namespace( + graph.Namespace, graph.Class, graph.Struct, graph.Union, graph.Variable, graph.Concept, graph.Enum, + graph.EnumValue, graph.Function, graph.Typedef + ): + if member.local_name and not member.qualified_name: + member.qualified_name = rf'{namespace.qualified_name}::{member.local_name}' + again = True + + # deduce any missing qualified_names for files and folders + again = True + while again: + again = False + for dir in g(graph.Directory): + if not dir.qualified_name: + continue + for member in dir(graph.Directory, graph.File): + if member.local_name and not member.qualified_name: + member.qualified_name = rf'{dir.qualified_name}/{member.local_name}' + again = True + + # add missing dir nodes + link file hierarchy + for node in list(g(graph.Directory, graph.File)): + sep = node.qualified_name.rstrip(r'/').rfind(r'/') + if sep == -1: + continue + parent_path = node.qualified_name[:sep] + if not parent_path: + continue + parent = None + for dir in g(graph.Directory): + if dir.qualified_name == parent_path: + parent = dir + break + if parent is None: + parent = g.get_or_create_node(type=graph.Directory) + parent.qualified_name = parent_path + parent.add(node) + + # resolve file links + for node in g: + if not node or node.type in (graph.Directory, graph.File, graph.ExternalResource) or not node.file: + continue + for file in g(graph.File): + if file.qualified_name == node.file: + file.add(node) + + g.validate() + + # replace doxygen's stupid nondeterministic IDs with something more robust + id_remap = dict() + + def fix_ids(node: graph.Node) -> str: + nonlocal id_remap + assert node is not None + assert node.type is not None + + # enum values are special - their ids always begin with the ID of their owning enum + if node.type is graph.EnumValue: + assert node.has_parent(graph.Enum) + assert node.local_name + parent = list(node(graph.Enum, parents=True))[0] + id = re.sub(r'[/+!@#$%&*()+=.,{}<>;:?\[\]\^\-\\]+', r'_', node.local_name).rstrip(r'_') + id = rf'{fix_ids(parent)}_{id}' + id_remap[node.id] = id + return id + + # if we don't have a qualified name then there's no meaningful transformation to do + # we also don't transform functions because overloading makes them ambiguous (todo: handle functions) + if not node.qualified_name or node.type is graph.Function: + id_remap[node.id] = node.id + return node.id + + id = re.sub(r'[/+!@#$%&*()+=.,{}<>;:?\[\]\^\-\\]+', r'_', node.qualified_name).rstrip(r'_') + if len(id) > 128: + id = sha1(id) + id = rf'{node.type_name.lower()}_{id}' + id_remap[node.id] = id + return id + + g = g.copy(id_transform=fix_ids) + + g.validate() + return g @@ -499,80 +705,108 @@ def write_graph_to_xml(g: graph.Graph, folder: Path, log_func=None): ns_clean=True ) - global COMPOUNDS - global COMPOUND_NODE_TYPES - global KINDS_TO_NODE_TYPES - global NODE_TYPES_TO_KINDS - global VERSION + def make_structured_text(elem, nodes): + assert elem is not None + assert nodes is not None + + # all the ones at the start that are just plain text get + # concatenated and set as the main text of the root subelement + if elem.text is None: + elem.text = r'' + while nodes and nodes[0].type is graph.Text: + elem.text = elem.text + nodes[0].text + nodes.pop(0) + + # paragraphs/references/other exposition markup + prev = None + while nodes: + if nodes[0].type is graph.Paragraph: + para = etree.SubElement(elem, rf'para') + para.text = nodes[0].text + make_structured_text(para, [n for n in nodes[0]]) + prev = para + elif nodes[0].type is graph.ExpositionMarkup: + assert nodes[0].tag + markup = etree.SubElement(elem, nodes[0].tag) + for k, v in nodes[0].extra_attributes: + markup.set(k, v) + markup.text = nodes[0].text + make_structured_text(markup, [n for n in nodes[0]]) + prev = markup + elif nodes[0].type is graph.Reference and nodes[0].is_parent: + ref = etree.SubElement(elem, rf'ref', attrib={r'refid': nodes[0][0].id}) + ref.text = nodes[0].text + if nodes[0].kind: + ref.set(r'kindref', nodes[0].kind) + if nodes[0][0].type is graph.ExternalResource: + ref.set(r'external', nodes[0][0].file) + prev = ref + else: + assert nodes[0].type in (graph.Text, graph.Reference) + assert prev is not None + if prev.tail is None: + prev.tail = r'' + prev.tail = prev.tail + nodes[0].text + nodes.pop(0) def make_text_subnode(elem, subelem_tag: str, node: graph.Node, subnode_type): - assert node is not None assert elem is not None assert subelem_tag is not None + assert node is not None assert subnode_type is not None subelem = etree.SubElement(elem, subelem_tag) subelem.text = r'' if subnode_type not in node: return text = [n for n in node(subnode_type)] # list of BriefDescription - text = [[i for i in n(graph.Paragraph, graph.Text)] for n in text] # list of lists of Text/Paragraph - text = list(itertools.chain.from_iterable(text)) # list of Text/Paragraph + text = [[i for i in n(graph.Paragraph, graph.Text, graph.Reference, graph.ExpositionMarkup)] + for n in text] # list of lists + text = list(itertools.chain.from_iterable(text)) # flattened list of Text/Paragraph/Reference if not text: return - # all the ones at the start that are just plain text get - # concatenated and set as the main text of the root subelement - while text and text[0].node_type is graph.Text and not text[0].reference_id: - subelem.text = subelem.text + text[0].text - text.pop(0) - # otherwise we need to loop through and make paragraphs/references - prev = None - while text: - if text[0].node_type is graph.Paragraph: - para = etree.SubElement(subelem, rf'para') - para.text = text[0].text - para_children = [n for n in text[0](graph.Text)] - text.pop(0) - prev = para - while para_children and not para_children[0].reference_id: - para.text = para.text + para_children[0].text - para_children.pop(0) - para_prev = None - while para_children: - if para_children[0].reference_id: - para_prev = etree.SubElement(subelem, rf'ref', attrib={r'refid': para_children[0].reference_id}) - para_prev.text = para_children[0].text - para_children.pop(0) - else: - assert para_prev is not None - while para_children and not para_children[0].reference_id: - para_prev.tail = para_prev.tail + para_children[0].text - para_children.pop(0) - elif text[0].reference_id: - prev = etree.SubElement(subelem, rf'ref', attrib={r'refid': text[0].reference_id}) - prev.text = text[0].text - text.pop(0) - else: - assert prev is not None - while text and text[0].node_type is graph.Text and not text[0].reference_id: - prev.tail = prev.tail + text[0].text - text.pop(0) + make_structured_text(subelem, text) - def make_brief(elem, node): + def make_brief(elem, node: graph.Node): make_text_subnode(elem, r'briefdescription', node, graph.BriefDescription) - def make_detail(elem, node): + def make_detail(elem, node: graph.Node): make_text_subnode(elem, r'detaileddescription', node, graph.DetailedDescription) - def make_initializer(elem, node): + def make_initializer(elem, node: graph.Node): make_text_subnode(elem, r'initializer', node, graph.Initializer) + def make_type(elem, node: graph.Node): + make_text_subnode(elem, r'type', node, graph.Type) + + def make_location(elem, node: graph.Node): + subelem = None + if node.type is graph.Directory: + subelem = etree.SubElement(elem, rf'location', attrib={r'file': rf'{node.qualified_name}/'}) + elif node.type is graph.File: + subelem = etree.SubElement(elem, rf'location', attrib={r'file': rf'{node.qualified_name}'}) + else: + subelem = etree.SubElement( + elem, rf'location', attrib={ + r'line': rf'{node.line}', + r'column': rf'{node.column}' + } + ) + if node.file: + subelem.set(r'file', node.file) + else: + files = [f for f in node(graph.File, parents=True)] + if files and files[0].qualified_name: + subelem.set(r'file', files[0].qualified_name) + for k, v in node.extra_attributes: + subelem.set(k, v) + # serialize the compound nodes for node in g(*COMPOUND_NODE_TYPES): if not node: continue assert node.qualified_name - kind = _to_kind(node.node_type) + kind = NODE_TYPES_TO_KINDS[node.type] assert kind in COMPOUNDS path = Path(folder, rf'{node.id}.xml') @@ -585,7 +819,7 @@ def make_initializer(elem, node): xml:lang="en-US"> - {node.qualified_name} + {node.local_name if node.type is graph.File else node.qualified_name} ''', parser=parser @@ -594,123 +828,309 @@ def make_initializer(elem, node): # create the root compounddef = xml.getroot().find(r'compounddef') - if node.node_type not in (graph.Namespace, graph.Directory, graph.File): + if node.type not in (graph.Namespace, graph.Directory, graph.File, graph.Concept): compounddef.set(r'prot', str(Prot(node.access_level))) # - if node.node_type in (graph.Class, graph.Struct, graph.Union): + if node.type in (graph.Class, graph.Struct, graph.Union, graph.Concept): files = [f for f in g(graph.File) if (f and f is not node and node in f)] for f in files: - assert f.qualified_name - elem = etree.SubElement(compounddef, rf'includes', attrib={r'local': r'no'}) - elem.text = f.qualified_name - - # add the inners - for inner_node in node( - graph.Namespace, graph.Class, graph.Struct, graph.Union, graph.Concept, graph.Directory, graph.File - ): - if not inner_node: - continue - assert inner_node.qualified_name - - kind = NODE_TYPES_TO_KINDS[inner_node.node_type] - if inner_node.node_type in (graph.Struct, graph.Union): - kind = r'class' - inner_elem = etree.SubElement(compounddef, rf'inner{kind}', attrib={r'refid': inner_node.id}) - if node.node_type not in (graph.Namespace, graph.Directory, graph.File): - inner_elem.set(r'prot', str(Prot(inner_node.access_level))) - inner_elem.text = inner_node.qualified_name + assert f.local_name + etree.SubElement(compounddef, rf'includes', attrib={r'local': r'no'}).text = f.local_name # create all the # (empty ones will be deleted at the end) sectiondefs = ( # namespace/file sections: r'enum', + r'typedef', r'var', + r'func', # class/struct/union sections: r'public-type', r'protected-type', r'private-type', + r'public-static-func', + r'protected-static-func', + r'private-static-func', + r'public-func', + r'protected-func', + r'private-func', r'public-static-attrib', r'protected-static-attrib', r'private-static-attrib', r'public-attrib', r'protected-attrib', - r'private-attrib' + r'private-attrib', + r'friend' ) sectiondefs = {k: etree.SubElement(compounddef, r'sectiondef', attrib={r'kind': k}) for k in sectiondefs} # enums - for enum in node(graph.Enum): + enums = list(node(graph.Enum)) + enums.sort(key=lambda n: n.qualified_name) + for member in enums: section = r'enum' - if node.node_type in (graph.Class, graph.Struct, graph.Union): - section = rf'{Prot(enum.access_level)}-type' - enum_elem = etree.SubElement( + if node.type in (graph.Class, graph.Struct, graph.Union): + section = rf'{Prot(member.access_level)}-type' + elem = etree.SubElement( sectiondefs[section], rf'memberdef', attrib={ - r'id': enum.id, + r'id': member.id, r'kind': r'enum', - r'static': str(Bool(enum.static)), - r'strong': str(Bool(enum.strong)), - r'prot': str(Prot(enum.access_level)) + r'static': str(Bool(member.static)), + r'strong': str(Bool(member.strong)), + r'prot': str(Prot(member.access_level)) } ) - etree.SubElement(enum_elem, r'type').text = enum.type - etree.SubElement(enum_elem, r'name').text = enum.local_name - etree.SubElement(enum_elem, r'qualified_name').text = enum.qualified_name - make_brief(enum_elem, enum) - make_detail(enum_elem, enum) - etree.SubElement(enum_elem, r'inbodydescription').text = r'' # todo - for value in enum(graph.EnumValue): + make_type(elem, member) + etree.SubElement(elem, r'name').text = member.local_name + etree.SubElement(elem, r'qualifiedname').text = member.qualified_name + for value in member(graph.EnumValue): value_elem = etree.SubElement( - enum_elem, rf'enumvalue', attrib={ + elem, rf'enumvalue', attrib={ r'id': value.id, r'prot': str(Prot(value.access_level)) } ) etree.SubElement(value_elem, r'name').text = value.local_name + if graph.Initializer in value: + make_initializer(value_elem, value) make_brief(value_elem, value) make_detail(value_elem, value) - make_initializer(value_elem, value) + make_brief(elem, member) + make_detail(elem, member) + etree.SubElement(elem, r'inbodydescription').text = r'' # todo + make_location(elem, member) + + # typedefs + typedefs = list(node(graph.Typedef)) + typedefs.sort(key=lambda n: n.qualified_name) + for member in typedefs: + section = r'typedef' + if node.type in (graph.Class, graph.Struct, graph.Union): + section = rf'{Prot(member.access_level)}-type' + elem = etree.SubElement( + sectiondefs[section], + rf'memberdef', + attrib={ + r'id': member.id, + r'kind': r'typedef', + r'static': str(Bool(member.static)), + r'prot': str(Prot(member.access_level)) + } + ) + make_type(elem, member) + etree.SubElement(elem, r'definition').text = member.definition + etree.SubElement(elem, r'argsstring') + etree.SubElement(elem, r'name').text = member.local_name + etree.SubElement(elem, r'qualifiedname').text = member.qualified_name + make_brief(elem, member) + make_detail(elem, member) + etree.SubElement(elem, r'inbodydescription').text = r'' # todo + make_location(elem, member) # variables - for var in node(graph.Variable): + variables = list(node(graph.Variable)) + if node.type in (graph.Class, graph.Struct, graph.Union): + static_vars = [v for v in variables if v.static] + static_vars.sort(key=lambda n: n.qualified_name) + variables = static_vars + [v for v in variables if not v.static] + else: + variables.sort(key=lambda n: n.qualified_name) + for member in variables: section = r'var' - if node.node_type in (graph.Class, graph.Struct, graph.Union): - section = rf'{Prot(var.access_level)}-{"static-" if var.static else ""}attrib' - var_elem = etree.SubElement( + if node.type in (graph.Class, graph.Struct, graph.Union): + section = rf'{Prot(member.access_level)}-{"static-" if member.static else ""}attrib' + elem = etree.SubElement( sectiondefs[section], rf'memberdef', attrib={ - r'id': var.id, + r'id': member.id, r'kind': r'variable', - r'prot': str(Prot(var.access_level)), - r'static': str(Bool(var.static)), - r'constexpr': str(Bool(var.constexpr)), - r'constinit': str(Bool(var.constinit)), - r'mutable': str(Bool(var.strong)), + r'prot': str(Prot(member.access_level)), + r'static': str(Bool(member.static)), + r'constexpr': str(Bool(member.constexpr)), + r'constinit': str(Bool(member.constinit)), + r'mutable': str(Bool(member.strong)), + } + ) + make_type(elem, member) + etree.SubElement(elem, r'definition').text = member.definition + etree.SubElement(elem, r'argsstring') + etree.SubElement(elem, r'name').text = member.local_name + etree.SubElement(elem, r'qualifiedname').text = member.qualified_name + make_brief(elem, member) + make_detail(elem, member) + make_initializer(elem, member) + etree.SubElement(elem, r'inbodydescription').text = r'' # todo + make_location(elem, member) + + # functions + functions = list(node(graph.Function)) + functions.sort(key=lambda n: n.qualified_name) + for member in functions: + section = r'func' + if node.type in (graph.Class, graph.Struct, graph.Union): + section = rf'{Prot(member.access_level)}-{"static-" if member.static else ""}func' + elem = etree.SubElement( + sectiondefs[section], + rf'memberdef', + attrib={ + r'id': member.id, + r'kind': r'function', + r'prot': str(Prot(member.access_level)), + r'static': str(Bool(member.static)), + r'const': str(Bool(member.const)), + r'constexpr': str(Bool(member.constexpr)), + r'consteval': str(Bool(member.consteval)), + r'explicit': str(Bool(member.explicit)), + r'inline': str(Bool(member.inline)), + r'noexcept': str(Bool(member.noexcept)), + r'virtual': str(Virt(member.virtual)), } ) - etree.SubElement(var_elem, r'type').text = var.type - etree.SubElement(var_elem, r'definition').text = var.definition - etree.SubElement(var_elem, r'argsstring') - etree.SubElement(var_elem, r'name').text = var.local_name - etree.SubElement(var_elem, r'qualified_name').text = var.qualified_name - make_brief(var_elem, var) - make_detail(var_elem, var) - make_initializer(var_elem, var) - etree.SubElement(var_elem, r'inbodydescription').text = r'' # todo - etree.SubElement(var_elem, r'location') + make_type(elem, member) + etree.SubElement(elem, r'name').text = member.local_name + etree.SubElement(elem, r'qualifiedname').text = member.qualified_name + make_brief(elem, member) + make_detail(elem, member) + etree.SubElement(elem, r'inbodydescription').text = r'' # todo + make_location(elem, member) + + # for concepts + if node.type is graph.Concept: + make_initializer(compounddef, node) + + # , , + make_brief(compounddef, node) + make_detail(compounddef, node) + make_location(compounddef, node) # - if node.node_type in (graph.Class, graph.Struct, graph.Union): + if node.type in (graph.Class, graph.Struct, graph.Union): listofallmembers = etree.SubElement(compounddef, rf'listofallmembers') + listofallmembers.text = r'' + for member_type in _ordered(graph.Function, graph.Variable): + for member in node(member_type): + member_elem = etree.SubElement( + listofallmembers, + rf'member', + attrib={ + r'refid': member.id, + r'prot': str(Prot(member.access_level)), + r'virtual': str(Virt(member.virtual)), + } + ) + etree.SubElement(member_elem, r'scope').text = node.qualified_name + etree.SubElement(member_elem, r'name').text = member.local_name + + # add the inners + for inner_type in _ordered( + graph.Directory, # + graph.File, + graph.Namespace, + graph.Class, + graph.Struct, + graph.Union, + graph.Concept, + graph.Page, + graph.Group, + graph.MemberGroup + ): + for inner_node in node(inner_type): + if not inner_node: + continue + assert inner_node.qualified_name + + kind = None + if inner_node.type in (graph.Class, graph.Struct, graph.Union): + kind = r'class' + elif inner_node.type is graph.MemberGroup: + kind = r'group' + else: + kind = NODE_TYPES_TO_KINDS[inner_node.type] + inner_elem = etree.SubElement(compounddef, rf'inner{kind}', attrib={r'refid': inner_node.id}) + if node.type not in (graph.Namespace, graph.Directory, graph.File, graph.Group, graph.Page): + inner_elem.set(r'prot', str(Prot(inner_node.access_level))) + inner_elem.text = inner_node.qualified_name + + # prune empty etc + for tag_name in (r'sectiondef', ): + for elem in list(compounddef.findall(tag_name)): + if not len(elem): + elem.getparent().remove(elem) + + if log_func: + log_func(rf'Writing {path}') + xml.write( + str(path), # + encoding=r'utf-8', + pretty_print=True, + xml_declaration=True + ) - # prune empty - for _, elem in sectiondefs.items(): - if not len(elem): - elem.getparent().remove(elem) + # serialize index.xml + if 1: + path = Path(folder, rf'index.xml') + xml = etree.ElementTree( + etree.XML( + rf''' + + ''', + parser=parser + ) + ) + root = xml.getroot() + for node_type in _ordered(*COMPOUND_NODE_TYPES): + for node in g(node_type): + compound = etree.SubElement( + root, + r'compound', + attrib={ + r'refid': node.id, # + r'kind': NODE_TYPES_TO_KINDS[node.type], + } + ) + etree.SubElement(compound, r'name').text = node.qualified_name + if node.type is graph.Directory: + continue + for child_type in _ordered(graph.Define, graph.Function, graph.Variable, graph.Enum): + children = list(node(child_type)) + if child_type is graph.Variable and node.type in (graph.Class, graph.Struct, graph.Union): + static_vars = [c for c in children if c.static] + static_vars.sort(key=lambda n: n.qualified_name) + children = static_vars + [c for c in children if not c.static] + else: + children.sort(key=lambda n: n.qualified_name) + for child in children: + assert child.local_name + member = etree.SubElement( + compound, + r'member', + attrib={ + r'refid': child.id, # + r'kind': NODE_TYPES_TO_KINDS[child.type], + } + ) + etree.SubElement(member, r'name').text = child.local_name + if child_type is graph.Enum: + for enumvalue in child(graph.EnumValue): + assert enumvalue.local_name + elem = etree.SubElement( + compound, + r'member', + attrib={ + r'refid': enumvalue.id, # + r'kind': NODE_TYPES_TO_KINDS[enumvalue.type], + } + ) + etree.SubElement(elem, r'name').text = enumvalue.local_name if log_func: log_func(rf'Writing {path}') @@ -718,6 +1138,5 @@ def make_initializer(elem, node): str(path), # encoding=r'utf-8', pretty_print=True, - xml_declaration=True, - standalone=False + xml_declaration=True ) diff --git a/poxy/graph.py b/poxy/graph.py index 9b2ba60..75d6d37 100644 --- a/poxy/graph.py +++ b/poxy/graph.py @@ -8,6 +8,7 @@ """ import enum as _enum +from platform import node from .utils import * #======================================================================================================================= @@ -52,6 +53,12 @@ class Function(object): +class Type(object): + '''The type of a variable/enum/function return.''' + pass + + + class Variable(object): '''Variables.''' pass @@ -94,12 +101,6 @@ class MemberGroup(object): -class Article(object): - '''A documentation article (e.g. Doxygen's `@page`).''' - pass - - - class Directory(object): '''A directory in the filesystem.''' pass @@ -112,6 +113,12 @@ class File(object): +class Page(object): + '''A documentation page (e.g. Doxygen's `@page`).''' + pass + + + class BriefDescription(object): '''A brief description of an element.''' pass @@ -137,42 +144,68 @@ class Paragraph(object): class Text(object): - '''Plain text, optionally with reference semantics.''' + '''Plain text.''' + pass + + + +class Reference(object): + '''A reference to another node.''' + pass + + + +class ExternalResource(object): + '''A reference to some resource outside the project (e.g. something in a tagfile).''' + pass + + + +class ExpositionMarkup(object): + '''A 'leftover' node for representing miscellaneous markup in expository contexts.''' pass -DESCRIPTION_NODE_TYPES = {BriefDescription, DetailedDescription} NODE_TYPES = { Namespace, Class, Struct, Union, Concept, Function, Variable, Enum, EnumValue, Typedef, Define, Group, MemberGroup, - Article, Directory, File, *DESCRIPTION_NODE_TYPES, Initializer, Paragraph, Text + Directory, File, BriefDescription, DetailedDescription, Page, Initializer, Paragraph, Text, Reference, + ExternalResource, Type, ExpositionMarkup +} +DESCRIPTION_NODE_TYPES = {BriefDescription, DetailedDescription} +EXPOSITION_NODE_TYPES = { + *DESCRIPTION_NODE_TYPES, Page, Initializer, Paragraph, Text, Reference, ExternalResource, Type, ExpositionMarkup } -Namespace.CAN_CONNECT_TO = { +CPP_TYPES = {Namespace, Class, Struct, Union, Concept, Function, Variable, Enum, EnumValue, Typedef, Define} +Namespace.CAN_CONTAIN = { Function, Class, Struct, Union, Variable, Typedef, Namespace, Concept, Enum, *DESCRIPTION_NODE_TYPES } -Class.CAN_CONNECT_TO = {Class, Struct, Union, Function, Variable, Typedef, Enum, MemberGroup, *DESCRIPTION_NODE_TYPES} -Struct.CAN_CONNECT_TO = Class.CAN_CONNECT_TO -Union.CAN_CONNECT_TO = Class.CAN_CONNECT_TO -Concept.CAN_CONNECT_TO = {Initializer, *DESCRIPTION_NODE_TYPES} -Function.CAN_CONNECT_TO = DESCRIPTION_NODE_TYPES -Variable.CAN_CONNECT_TO = {Initializer, *DESCRIPTION_NODE_TYPES} -Enum.CAN_CONNECT_TO = {EnumValue, *DESCRIPTION_NODE_TYPES} -EnumValue.CAN_CONNECT_TO = {Initializer, *DESCRIPTION_NODE_TYPES} -Typedef.CAN_CONNECT_TO = DESCRIPTION_NODE_TYPES -Define.CAN_CONNECT_TO = {Initializer, *DESCRIPTION_NODE_TYPES} -Group.CAN_CONNECT_TO = {t for t in NODE_TYPES if t not in (Article, )} -MemberGroup.CAN_CONNECT_TO = {t for t in Class.CAN_CONNECT_TO if t not in (MemberGroup, )} -Article.CAN_CONNECT_TO = DESCRIPTION_NODE_TYPES -File.CAN_CONNECT_TO = { - Namespace, Class, Struct, Union, Concept, Function, Variable, Enum, Typedef, Define, Article, - *DESCRIPTION_NODE_TYPES +Class.CAN_CONTAIN = {Class, Struct, Union, Function, Variable, Typedef, Enum, MemberGroup, *DESCRIPTION_NODE_TYPES} +Struct.CAN_CONTAIN = Class.CAN_CONTAIN +Union.CAN_CONTAIN = Class.CAN_CONTAIN +Concept.CAN_CONTAIN = {Initializer, *DESCRIPTION_NODE_TYPES} +Function.CAN_CONTAIN = {Type, *DESCRIPTION_NODE_TYPES} +Variable.CAN_CONTAIN = {Type, Initializer, *DESCRIPTION_NODE_TYPES} +Enum.CAN_CONTAIN = {Type, EnumValue, *DESCRIPTION_NODE_TYPES} +EnumValue.CAN_CONTAIN = {Initializer, *DESCRIPTION_NODE_TYPES} +Typedef.CAN_CONTAIN = {Type, *DESCRIPTION_NODE_TYPES} +Define.CAN_CONTAIN = {Initializer, *DESCRIPTION_NODE_TYPES} +Group.CAN_CONTAIN = {t for t in NODE_TYPES if t not in (Page, )} +MemberGroup.CAN_CONTAIN = {t for t in Class.CAN_CONTAIN if t not in (MemberGroup, )} +Directory.CAN_CONTAIN = {Directory, File, *DESCRIPTION_NODE_TYPES} +File.CAN_CONTAIN = { + Namespace, Class, Struct, Union, Concept, Function, Variable, Enum, Typedef, Define, Page, *DESCRIPTION_NODE_TYPES } -Directory.CAN_CONNECT_TO = {Directory, File, *DESCRIPTION_NODE_TYPES} -BriefDescription.CAN_CONNECT_TO = {Paragraph, Text} -DetailedDescription.CAN_CONNECT_TO = {Paragraph, Text} -Initializer.CAN_CONNECT_TO = {Text} -Paragraph.CAN_CONNECT_TO = {Text} -Text.CAN_CONNECT_TO = set() +Page.CAN_CONTAIN = {Paragraph, Text, Reference, *DESCRIPTION_NODE_TYPES, ExpositionMarkup} +BriefDescription.CAN_CONTAIN = {Paragraph, Text, Reference, ExpositionMarkup} +DetailedDescription.CAN_CONTAIN = {Paragraph, Text, Reference, ExpositionMarkup} +Initializer.CAN_CONTAIN = {Text, Reference, ExpositionMarkup} +Paragraph.CAN_CONTAIN = {Text, Reference, ExpositionMarkup} +Text.CAN_CONTAIN = set() +ExpositionMarkup.CAN_CONTAIN = {Paragraph, Text, Reference, ExpositionMarkup} +Type.CAN_CONTAIN = {Text, Reference} +Reference.CAN_CONTAIN = {*CPP_TYPES, Page, Group, MemberGroup, Directory, File, ExternalResource} +ExternalResource.CAN_CONTAIN = set() @@ -190,17 +223,67 @@ class AccessLevel(_enum.Enum): +def _make_node_iterator(nodes, *types): + assert types is not None + + def permissive_generator(): + nonlocal nodes + for node in nodes: + yield node + + if not types: + return permissive_generator() + + for t in types: + assert t is None or isinstance(t, bool) or t in NODE_TYPES + + def selective_generator(): + nonlocal nodes + nonlocal types + yield_with_no_type = False in types or None in types + yield_with_any_type = True in types + for node in nodes: + if ((node.type is None and yield_with_no_type) + or (node.type is not None and (yield_with_any_type or node.type in types))): + yield node + + return selective_generator() + + + +class _NullNodeIterator(object): + + def __iter__(self): + return self + + def __next__(self): + raise StopIteration + + + +class NodePropertyChanged(Error): + """Raised when an an attempt is made to change an already-set property in a graph node.""" + pass + + + class Node(object): + """A single node in a C++ project graph.""" class _Props(object): pass + def __make_hierarchy_containers(self): + if hasattr(self, r'_Node__children'): + return + self.__parents = [] + self.__parents_by_id = dict() + self.__children = [] + self.__children_by_id = dict() + def __init__(self, id: str): assert id is not None self.__id = id - self.__connections = [] - self.__connections_by_id = dict() - self.__props = Node._Props() #============== # getters @@ -208,7 +291,9 @@ def __init__(self, id: str): def __property_get(self, name: str, out_type=None, default=None): assert name is not None - value = getattr(self.__props, str(name), None) + value = None + if hasattr(self, r'_Node__props'): + value = getattr(self.__props, str(name), None) if value is None: value = default if value is not None and out_type is not None and not isinstance(value, out_type): @@ -220,16 +305,12 @@ def id(self) -> str: return self.__id @property - def node_type(self): - return self.__property_get(r'node_type') - - @property - def has_node_type(self) -> bool: - return self.node_type is not None + def type(self): + return self.__property_get(r'type') @property - def node_type_name(self) -> str: - t = self.node_type + def type_name(self) -> str: + t = self.type if t is None: return '' return t.__name__ @@ -242,10 +323,6 @@ def qualified_name(self) -> str: def local_name(self) -> str: return self.__property_get(r'local_name', str, r'') - @property - def type(self) -> str: - return self.__property_get(r'type', str, r'') - @property def definition(self) -> str: return self.__property_get(r'definition', str, r'') @@ -301,32 +378,48 @@ def strong(self) -> bool: @property def access_level(self) -> AccessLevel: return self.__property_get( - r'access_level', AccessLevel, AccessLevel.PRIVATE if self.node_type is Class else AccessLevel.PUBLIC + r'access_level', AccessLevel, AccessLevel.PRIVATE if self.type is Class else AccessLevel.PUBLIC ) @property def text(self) -> str: return self.__property_get(r'text', str, r'') - @property - def reference_id(self) -> str: - return self.__property_get(r'reference_id', str, r'') - @property def is_paragraph(self) -> bool: return self.__property_get(r'is_paragraph', bool, False) + @property + def file(self) -> str: + return self.__property_get(r'file', str, r'') + + @property + def line(self) -> int: + return self.__property_get(r'line', int, 0) + + @property + def column(self) -> int: + return self.__property_get(r'column', int, 0) + + @property + def kind(self) -> str: + return self.__property_get(r'kind', str, r'') + + @property + def tag(self) -> str: + return self.__property_get(r'tag', str, r'') + + @property + def extra_attributes(self) -> typing.Sequence[typing.Tuple[str, str]]: + return self.__property_get(r'extra_attributes', None, tuple()) + def __bool__(self) -> bool: - return self.has_node_type and bool(self.id) + return self.type is not None and bool(self.id) #============== # setters #============== - def __property_is_set(self, name: str) -> bool: - assert name is not None - return hasattr(self.__props, str(name)) - def __property_set(self, name: str, out_type, value, strip_strings=False): assert name is not None # known types that have a sensible __bool__ operator can convert to None if false @@ -340,7 +433,7 @@ def __property_set(self, name: str, out_type, value, strip_strings=False): elif value.lower() in (r'yes', r'true', r'enabled'): value = True else: - raise Error(rf"C++ node '{self.id}' property '{name}' could not parse a boolean from '{value}'") + raise Error(rf"Node '{self.id}' property '{name}' could not parse a boolean from '{value}'") elif out_type is AccessLevel: if value.lower() in (r'pub', r'public'): value = AccessLevel.PUBLIC @@ -349,7 +442,7 @@ def __property_set(self, name: str, out_type, value, strip_strings=False): elif value.lower() in (r'priv', r'private'): value = AccessLevel.PRIVATE else: - raise Error(rf"C++ node '{self.id}' property '{name}' could not parse access level from '{value}'") + raise Error(rf"Node '{self.id}' property '{name}' could not parse access level from '{value}'") assert isinstance(value, AccessLevel) # None == keep whatever the current value is (no-op) # (None is never a valid value for a real graph attribute) @@ -359,56 +452,57 @@ def __property_set(self, name: str, out_type, value, strip_strings=False): value = out_type(value) if strip_strings and isinstance(value, str): value = value.strip() - current = getattr(self.__props, str(name), None) + current = None + has_props = hasattr(self, r'_Node__props') + if has_props: + current = getattr(self.__props, str(name), None) # it's OK if there's already a value as long as it's identical to the new one, # otherwise we throw so that we can detect when the source data is bad or the adapter is faulty # (since if a property _can_ be defined in multiple places it should be identical in all of them) if current is not None: if type(current) != type(value): - raise Error( - rf"C++ node '{self.id}' property '{name}' first seen with type {type(current)}, now seen with type {type(value)}" + raise NodePropertyChanged( + rf"Node '{self.id}' property '{name}' first seen with type {type(current)}, now seen with type {type(value)}" ) if current != value: - raise Error( - rf"C++ node '{self.id}' property '{name}' first seen with value {current}, now seen with value {value}" + raise NodePropertyChanged( + rf"Node '{self.id}' property '{name}' first seen with value '{current}', now seen with value '{value}'" ) return + if not has_props: + self.__props = Node._Props() setattr(self.__props, str(name), value) - @node_type.setter - def node_type(self, value): - global NODE_TYPES + @type.setter + def type(self, value): if value is None: return if value not in NODE_TYPES: raise Error(rf"Unknown C++ node type '{value}'") - had_node_type = self.has_node_type - self.__property_set(r'node_type', None, value) - if had_node_type != self.has_node_type: + had_type = self.type is not None + self.__property_set(r'type', None, value) + if had_type != (self.type is not None): self.__deduce_local_name() - for node in self.__connections: - Node._check_connection(self, node) + if hasattr(self, r'_Node__children'): + for child in self.__children: + Node._check_connection(self, child) def __deduce_local_name(self): - if not self.qualified_name or self.local_name or not self.has_node_type: + if not self.qualified_name or self.local_name or self.type is None: return - if self.node_type in (Namespace, Class, Struct, Union, Concept, Function, Variable, Enum, Typedef): - ln = self.qualified_name - if ln.find(r'<') != -1: # templates might have template args with '::' so ignore them + if self.type in (Namespace, Class, Struct, Union, Concept, Function, Variable, Enum, EnumValue, Typedef): + if self.qualified_name.find(r'<') != -1: # templates might have template args with '::' so ignore them return - pos = ln.rfind(r'::') - if pos != -1: - ln = ln[pos + 2:] - self.local_name = ln - elif self.node_type in (Directory, File): - ln = self.qualified_name - pos = ln.rfind(r'/') - if pos != -1: - ln = ln[pos + 1:] - self.local_name = ln + self.local_name = tail(self.qualified_name, r'::') + elif self.type in (Directory, File): + self.local_name = tail(self.qualified_name, r'/') + elif self.type is Define: + self.local_name = self.qualified_name @qualified_name.setter def qualified_name(self, value: str): + if value is not None and self.type in (Directory, File): + value = str(value).strip().replace('\\', r'/').rstrip(r'/') self.__property_set(r'qualified_name', str, value, strip_strings=True) self.__deduce_local_name() @@ -416,26 +510,6 @@ def qualified_name(self, value: str): def local_name(self, value: str): self.__property_set(r'local_name', str, value, strip_strings=True) - @type.setter - def type(self, value: str): - if value is not None: - value = str(value).strip() - # extract constexpr, constinit, static, mutable etc out of the type if possible - attrs = re.fullmatch(r'^((?:(?:const(?:expr|init|eval)|static|mutable)\s)+).*?$', value) - if attrs: - value = value[len(attrs[1]):].strip() - if attrs[1].find(r'constexpr') != -1: - self.constexpr = True - if attrs[1].find(r'constinit') != -1: - self.constinit = True - if attrs[1].find(r'consteval') != -1: - self.consteval = True - if attrs[1].find(r'static') != -1: - self.static = True - if attrs[1].find(r'mutable') != -1: - self.mutable = True - self.__property_set(r'type', str, value, strip_strings=True) - @definition.setter def definition(self, value: str): self.__property_set(r'definition', str, value) @@ -496,36 +570,91 @@ def access_level(self, value: AccessLevel): def text(self, value: str): self.__property_set(r'text', str, value) - @reference_id.setter - def reference_id(self, value: str): - self.__property_set(r'reference_id', str, value, strip_strings=True) - @is_paragraph.setter def is_paragraph(self, value: bool): self.__property_set(r'is_paragraph', bool, value) + @file.setter + def file(self, value: str): + if value is not None: + value = str(value).strip().replace('\\', r'/').rstrip(r'/') + self.__property_set(r'file', str, value) + + @line.setter + def line(self, value: int): + self.__property_set(r'line', int, value) + + @column.setter + def column(self, value: int): + self.__property_set(r'column', int, value) + + @kind.setter + def kind(self, value: str): + self.__property_set(r'kind', str, value, strip_strings=True) + + @tag.setter + def tag(self, value: str): + self.__property_set(r'tag', str, value, strip_strings=True) + + @extra_attributes.setter + def extra_attributes(self, value: typing.Sequence[typing.Tuple[str, str]]): + self.__property_set(r'extra_attributes', None, value) + #============== - # connections + # children #============== @property - def is_leaf(self) -> bool: - return not bool(self.__connections) + def is_child(self) -> bool: + return hasattr(self, r'_Node__children') and bool(self.__parents) + + @property + def is_parent(self) -> bool: + return hasattr(self, r'_Node__children') and bool(self.__children) def __contains__(self, node_or_id) -> bool: - global NODE_TYPES assert node_or_id is not None assert isinstance(node_or_id, (str, Node)) or node_or_id in NODE_TYPES + if not hasattr(self, r'_Node__children'): + return False + if isinstance(node_or_id, Node): + node_or_id = node_or_id.id if isinstance(node_or_id, str): - return node_or_id in self.__connections_by_id - elif isinstance(node_or_id, Node): - return node_or_id in self.__connections + return node_or_id in self.__children_by_id else: - for c in self.__connections: - if c.node_type is node_or_id: + for c in self.__children: + if c.type is node_or_id: return True return False + def has_parent(self, node_or_id) -> bool: + assert node_or_id is not None + assert isinstance(node_or_id, (str, Node)) or node_or_id in NODE_TYPES + if not hasattr(self, r'_Node__parents'): + return False + if isinstance(node_or_id, Node): + node_or_id = node_or_id.id + if isinstance(node_or_id, str): + return node_or_id in self.__parents_by_id + else: + for c in self.__parents: + if c.type is node_or_id: + return True + return False + + def __getitem__(self, id_or_index: typing.Union[str, int]): + assert id_or_index is not None + if not hasattr(self, r'_Node__children'): + return None + if isinstance(id_or_index, str): + try: + return self.__children_by_id[id_or_index] + except: + return None + else: + assert isinstance(id_or_index, int) + return self.__children[id_or_index] + @classmethod def _check_connection(cls, source, dest): assert source is not None @@ -533,60 +662,110 @@ def _check_connection(cls, source, dest): assert dest is not None assert isinstance(dest, Node) - # self-connection is always illegal, regardless of node_type information + # self-connection is always illegal, regardless of type information if id(source) == id(dest): raise Error(rf"C++ node '{source.id}' may not connect to itself") - # otherwise if we don't have node_type information the connection is 'OK' + # otherwise if we don't have type information the connection is 'OK' # (really this just means we defer the check until later) - if not source.has_node_type or not dest.has_node_type: + if source.type is None or dest.type is None: return - if dest.node_type not in source.node_type.CAN_CONNECT_TO: + # check basic connection rules + if dest.type not in source.type.CAN_CONTAIN: raise Error( - rf"C++ node '{source.id}' with type {source.node_type_name} is not allowed to connect to nodes of type {dest.node_type_name}" + rf"{source.type_name} node '{source.id}' is not allowed to connect to nodes of type {dest.type_name}" ) - def connect_to(self, dest): - assert dest is not None - assert isinstance(dest, Node) + # check situations where a node must only belong to one parent of a particular set of classes + def check_single_parentage(source_types, dest_types): + nonlocal source + nonlocal dest + source_types = coerce_collection(source_types) + dest_types = coerce_collection(dest_types) + if source.type not in source_types or dest.type not in dest_types: + return + sum = 0 + for parent in dest(*source_types, parents=True): + sum += 1 + if dest not in source: + sum += 1 + if sum > 1: + raise Error(rf"{dest.type_name} node '{dest.id}' is not allowed to be a member of more than one parent") + + check_single_parentage(Enum, EnumValue) + check_single_parentage((Variable, Function, Enum, Typedef), Type) + + # check some specific cases + if source.type is Reference and source.is_parent: + raise Error(rf"{source.type_name} node '{source.id}' is not allowed to reference more than one node") - Node._check_connection(self, dest) + def add(self, child): + assert child is not None + assert isinstance(child, Node) # connecting to the same node twice is fine (no-op) - if dest.id in self.__connections_by_id: - existing_dest = self.__connections_by_id[dest.id] + if child in self: + existing_child = self.__children_by_id[child.id] # check that identity is unique - if id(dest) != id(existing_dest): - raise Error(rf"Two different C++ nodes seen with the same ID ('{dest.id}')") + if id(child) != id(existing_child): + raise Error(rf"Two different nodes seen with the same ID ('{child.id}')") return - self.__connections.append(dest) - self.__connections_by_id[dest.id] = dest + Node._check_connection(self, child) + + self.__make_hierarchy_containers() + self.__children.append(child) + self.__children_by_id[child.id] = child + + child.__make_hierarchy_containers() + child.__parents.append(self) + child.__parents_by_id[self.id] = self def __iter__(self): - for node in self.__connections: - yield node + if not hasattr(self, r'_Node__children'): + return _NullNodeIterator() + return _make_node_iterator(self.__children) - def __call__(self, *node_types): - assert node_types is not None - if not node_types: - return self.__iter__() + def __call__(self, *types, parents=False): + if not hasattr(self, r'_Node__children'): + return _NullNodeIterator() + return _make_node_iterator(self.__parents if parents else self.__children, *types) - global NODE_TYPES - for t in node_types: - assert t is None or isinstance(t, bool) or t in NODE_TYPES + def remove(self, child): + assert child is not None + assert isinstance(child, Node) - def make_generator(nodes): - nonlocal node_types - yield_with_no_node_type = False in node_types or None in node_types - yield_with_any_node_type = True in node_types - for node in nodes: - if ((node.node_type is None and yield_with_no_node_type) - or (node.node_type is not None and (yield_with_any_node_type or node.node_type in node_types))): - yield node + if not hasattr(self, r'_Node__children') or child not in self or child is self: + return - return make_generator(self.__connections) + self.__children.remove(child) + del self.__children_by_id[child.id] + + child.__parents.remove(self) + del child.__parents_by_id[self.id] + + def clear(self): + if not hasattr(self, r'_Node__children'): + return + + for child in self.__children: + child.__parents.remove(self) + del child.__parents_by_id[self.id] + + self.__children.clear() + self.__children_by_id.clear() + + def copy(self, id=None, transform=None): + node = Node(self.id if id is None else id) + if transform is not None: + transform(self, node) + if hasattr(self, r'_Node__props'): + node.__props = Node._Props() + for key, val in self.__props.__dict__.items(): + if not hasattr(node.__props, key): + setattr(node.__props, key, val) + return node @@ -597,16 +776,21 @@ def make_generator(nodes): class Graph(object): + """A C++ project graph.""" def __init__(self): self.__nodes: typing.Dict[str, Node] self.__nodes = dict() self.__next_unique_id = 0 + def __get_unique_id(self) -> str: + id = rf'__graph_unique_id_{self.__next_unique_id}' + self.__next_unique_id += 1 + return id + def get_or_create_node(self, id: str = None, type=None) -> Node: if id is None: - id = rf'__graph_unique_id_{self.__next_unique_id}' - self.__next_unique_id += 1 + id = self.__get_unique_id() assert id node = None if id not in self.__nodes: @@ -614,29 +798,119 @@ def get_or_create_node(self, id: str = None, type=None) -> Node: self.__nodes[id] = node else: node = self.__nodes[id] - node.node_type = type + node.type = type return node def __iter__(self): - for id, node in self.__nodes.items(): - yield (id, node) - - def __call__(self, *node_types): - assert node_types is not None - if not node_types: - return self.__iter__() - - global NODE_TYPES - for t in node_types: - assert t is None or isinstance(t, bool) or t in NODE_TYPES - - def make_generator(nodes): - nonlocal node_types - yield_with_no_node_type = False in node_types or None in node_types - yield_with_any_node_type = True in node_types - for _, node in nodes: - if ((node.node_type is None and yield_with_no_node_type) - or (node.node_type is not None and (yield_with_any_node_type or node.node_type in node_types))): - yield node - - return make_generator(self.__nodes.items()) + return _make_node_iterator(self.__nodes.values()) + + def __call__(self, *types): + return _make_node_iterator(self.__nodes.values(), *types) + + def __contains__(self, node_or_id) -> bool: + assert node_or_id is not None + assert isinstance(node_or_id, (str, Node)) or node_or_id in NODE_TYPES + if isinstance(node_or_id, Node): + node_or_id = node_or_id.id + if isinstance(node_or_id, str): + return node_or_id in self.__nodes + else: + for _, n in self.__nodes: + if n.type is node_or_id: + return True + return False + + def __getitem__(self, id: str) -> Node: + assert id is not None + assert isinstance(id, str) + try: + return self.__nodes[id] + except: + return None + + def remove(self, *nodes: typing.Sequence[Node], filter=None): + if filter is not None and not nodes: + nodes = self.__nodes.values() + prune = [] + for node in nodes: + if node is None or node not in self: + continue + if filter is not None and not filter(node): + continue + for _, other in self.__nodes.items(): + if node is not other: + other.remove(node) + node.clear() + prune.append(node) + for node in prune: + del self.__nodes[node.id] + + def validate(self): + for node in self: + if node.type is None: + raise Error(rf"Node '{node.id}' is untyped") + if node.type not in EXPOSITION_NODE_TYPES: + if not node.qualified_name: + raise Error(rf"{node.type_name} node '{node.id}' missing attribute 'qualified_name'") + if not node.local_name: + raise Error(rf"{node.type_name} node '{node.id}' missing attribute 'local_name'") + + if node.file.find('\\') != -1: + raise Error(rf"{node.type_name} node '{node.id}' attribute 'file' contains back-slashes") + if node.file.endswith(r'/'): + raise Error(rf"{node.type_name} node '{node.id}' attribute 'file' ends with a forward-slash") + if node.line < 0: + raise Error(rf"{node.type_name} node '{node.id}' attribute 'line' is negative") + if node.column < 0: + raise Error(rf"{node.type_name} node '{node.id}' attribute 'column' is negative") + + if node.type in (Directory, File): + if node.qualified_name.find('\\') != -1: + raise Error(rf"{node.type_name} node '{node.id}' attribute 'qualified_name' contains back-slashes") + if node.qualified_name.endswith(r'/'): + raise Error( + rf"{node.type_name} node '{node.id}' attribute 'qualified_name' ends with a forward-slash" + ) + if node.type in CPP_TYPES: + if node.qualified_name.startswith(r'::'): + raise Error(rf"{node.type_name} node '{node.id}' attribute 'qualified_name' starts with ::") + if node.qualified_name.endswith(r'::'): + raise Error(rf"{node.type_name} node '{node.id}' attribute 'qualified_name' ends with ::") + if node.type is not EnumValue and not node.file: + raise Error(rf"{node.type_name} node '{node.id}' missing attribute 'file'") + + if node.type in (EnumValue, Type): + if not node.is_child: + raise Error(rf"{node.type_name} node '{node.id}' is an orphan") + + if node.type in (Function, Variable, Typedef): + if Type not in node: + raise Error(rf"{node.type_name} node '{node.id}' is missing a Type") + + def copy(self, filter=None, id_transform=None, transform=None): + g = Graph() + id_remap = dict() + # first pass to copy + for src in self: + if filter is not None and not filter(src): + continue + id = src.id + if id_transform is not None: + id = id_transform(src) + if id is None: + id = g.__get_unique_id() + else: + id = str(id) + if id in g: + raise Error(rf"A node with id '{id}' already exists in the destination graph") + id_remap[src.id] = id + g.__nodes[id] = src.copy(id=id, transform=transform) + # second pass to link hierarchy + for src in self: + if src.id not in id_remap: + continue + for child in src: + if child.id not in id_remap: + continue + g[id_remap[src.id]].add(g[id_remap[child.id]]) + return g diff --git a/poxy/run.py b/poxy/run.py index eb956b8..938b94f 100644 --- a/poxy/run.py +++ b/poxy/run.py @@ -18,6 +18,7 @@ from . import doxygen from . import soup from . import fixers +from . import graph from .svg import SVG from distutils.dir_util import copy_tree @@ -134,6 +135,7 @@ def make_temp_file(): (r'SORT_MEMBERS_CTORS_1ST', True), (r'SOURCE_BROWSER', False), (r'STRICT_PROTO_MATCHING', False), + (r'STRIP_FROM_INC_PATH', None), # we handle this (r'SUBGROUPING', True), (r'TAB_SIZE', 4), (r'TOC_INCLUDE_HEADINGS', 3), @@ -142,6 +144,7 @@ def make_temp_file(): (r'USE_HTAGS', False), (r'USE_MDFILE_AS_MAINPAGE', None), (r'VERBATIM_HEADERS', False), + (r'WARN_AS_ERROR', False), # we handle this (r'WARN_IF_DOC_ERROR', True), (r'WARN_IF_INCOMPLETE_DOC', True), (r'WARN_LOGFILE', None), @@ -213,7 +216,6 @@ def preprocess_doxyfile(context: Context): df.append(r'# context.warnings', end='\n\n') # --------------------------------------------------- df.set_value(r'WARNINGS', context.warnings.enabled) - df.set_value(r'WARN_AS_ERROR', False) # we do this ourself df.set_value(r'WARN_IF_UNDOCUMENTED', context.warnings.undocumented) df.append() @@ -572,10 +574,15 @@ def postprocess_xml(context: Context): members = [ m for m in section.findall(r'memberdef') if m.get(r'kind') in (r'friend', r'function') ] - attribute_keywords = ((r'constexpr', r'constexpr', - r'yes'), (r'consteval', r'consteval', r'yes'), (r'explicit', r'explicit', r'yes'), - (r'static', r'static', r'yes'), (r'friend', None, None), (r'inline', r'inline', - r'yes'), (r'virtual', r'virt', r'virtual')) + attribute_keywords = ( + (r'constexpr', r'constexpr', r'yes'), # + (r'consteval', r'consteval', r'yes'), + (r'explicit', r'explicit', r'yes'), + (r'static', r'static', r'yes'), + (r'friend', None, None), + (r'inline', r'inline', r'yes'), + (r'virtual', r'virt', r'virtual') + ) for member in members: type = member.find(r'type') if type is None or type.text is None: @@ -864,7 +871,22 @@ def postprocess_xml_v2(context: Context): log_func = lambda m: context.verbose(m) g = doxygen.read_graph_from_xml(context.temp_xml_dir, log_func=log_func) - delete_directory(context.temp_xml_dir, logger=log_func) + + # delete 'file' nodes for markdown and dox files + g.remove(filter=lambda n: n.type is graph.File and re.search(r'[.](?:md|dox)$', n.local_name, flags=re.I)) + + # delete empty 'dir' nodes + g.remove(filter=lambda n: n.type is graph.Directory and not len(list(n(graph.File, graph.Directory)))) + + # todo: + # - extract namespaces, types and enum values for syntax highlighting + # - enumerate all compound pages and their types for later (e.g. HTML post-process) + # - merge user-defined sections with the same name + # - sort user-defined sections based on their name + # - implementation headers + + for f in enumerate_files(context.temp_xml_dir, any=r'*.xml'): + delete_file(f, logger=log_func) doxygen.write_graph_to_xml(g, context.temp_xml_dir, log_func=log_func) diff --git a/poxy/utils.py b/poxy/utils.py index 7536333..add27fa 100644 --- a/poxy/utils.py +++ b/poxy/utils.py @@ -107,6 +107,17 @@ def download_binary(uri: str, timeout=10) -> bytes: +def tail(s: str, split: str) -> str: + assert s is not None + assert split is not None + assert split + idx = s.rfind(split) + if idx == -1: + return s + return s[idx + len(split):] + + + #======================================================================================================================= # REGEX REPLACER #======================================================================================================================= diff --git a/tests/regenerate_tests.py b/tests/regenerate_tests.py index a4ae550..1c2bd4b 100644 --- a/tests/regenerate_tests.py +++ b/tests/regenerate_tests.py @@ -77,8 +77,12 @@ def regenerate_expected_outputs(): # delete garbage garbage = ( - r'*.xslt', r'*.xsd', r'favicon*', r'search-v2.js', r'Doxyfile*', - *(coerce_collection([r'garbage']) if r'garbage' in config else []) + r'*.xslt', + r'*.xsd', + r'favicon*', + r'search-v2.js', + r'Doxyfile*', + *(coerce_collection([r'garbage']) if r'garbage' in config else []), ) garbage = ( *(enumerate_files(html_dir, any=garbage) if output_html else []), diff --git a/tests/test_project/expected_html/Test!.tagfile.xml b/tests/test_project/expected_html/Test!.tagfile.xml index c59f1ee..f3e4522 100644 --- a/tests/test_project/expected_html/Test!.tagfile.xml +++ b/tests/test_project/expected_html/Test!.tagfile.xml @@ -19,6 +19,27 @@ a867e1e9e6c924393462cf7c1a78c6c3e + + int + a_shit_typedef + namespacetest.html + a51ed0e00636746f407a20697efc8518e + + + + int + a_typedef + namespacetest.html + a011ffb640026d2f49f9c73fe09d88cfb + + + + T + a_typedef_template + namespacetest.html + ab80f438f7d1d6d6e3b184b7ce32df687 + + unscoped_enum @@ -48,11 +69,32 @@ scoped_enum namespacetest.html - a3a2b94881b2cd4942667975879b992b9 + a393facd551443a4aaea74c533c2bce6f - val_0 - val_1 - val_2 + val_0 + val_1 + val_2 + + + std::uint8_t + do_the_thing + namespacetest.html + a011c6f27e90f228cd68be9def15bc076 + () + + + constexpr T + do_the_other_thing + namespacetest.html + ae66b7e2e94f84934cff5de3e512ce0ee + (U u) noexcept + + + auto + do_the_thing_automatically + namespacetest.html + af2e6008ff8ee50f6cda80da681d44348 + () -> int constexpr bool @@ -62,9 +104,34 @@ + + code.h + subfolder_2code_8h.html + test::class_1 classtest_1_1class__1.html + + int + public_typedef + classtest_1_1class__1.html + a52150fb93a5946c0d2fca381f99f426f + + + + bool + public_function + classtest_1_1class__1.html + a6b372d2af2e8e874869f0ddf50a306fb + () + + + static constexpr struct_1 + public_static_function + classtest_1_1class__1.html + af2caa02646ff203339faf41a0efc4b20 + () + bool public_variable @@ -73,12 +140,33 @@ - static constexpr bool + static constexpr std::byte public_static_variable classtest_1_1class__1.html - a27966148032ee64e169e8e24ad6c7449 + ae05cfedbf34e9f128e87410c09ad3780 + + + + int + protected_typedef + classtest_1_1class__1.html + aa57cecf6a75b7d9a7343b06706f3863c + + bool + protected_function + classtest_1_1class__1.html + ab61d354f51a90bb49d38d22e7a7d7f48 + () + + + static constexpr bool + protected_static_function + classtest_1_1class__1.html + ac4b22b5c1e50ba1a06f0319566252bb8 + () + bool protected_variable @@ -142,6 +230,27 @@ test::struct_1 test::template_class_1 test::concept_1 + + int + a_shit_typedef + namespacetest.html + a51ed0e00636746f407a20697efc8518e + + + + int + a_typedef + namespacetest.html + a011ffb640026d2f49f9c73fe09d88cfb + + + + T + a_typedef_template + namespacetest.html + ab80f438f7d1d6d6e3b184b7ce32df687 + + unscoped_enum @@ -171,11 +280,32 @@ scoped_enum namespacetest.html - a3a2b94881b2cd4942667975879b992b9 + a393facd551443a4aaea74c533c2bce6f - val_0 - val_1 - val_2 + val_0 + val_1 + val_2 + + + std::uint8_t + do_the_thing + namespacetest.html + a011c6f27e90f228cd68be9def15bc076 + () + + + constexpr T + do_the_other_thing + namespacetest.html + ae66b7e2e94f84934cff5de3e512ce0ee + (U u) noexcept + + + auto + do_the_thing_automatically + namespacetest.html + af2e6008ff8ee50f6cda80da681d44348 + () -> int constexpr bool diff --git a/tests/test_project/expected_html/classtest_1_1class__1.html b/tests/test_project/expected_html/classtest_1_1class__1.html index 9758550..126efc3 100644 --- a/tests/test_project/expected_html/classtest_1_1class__1.html +++ b/tests/test_project/expected_html/classtest_1_1class__1.html @@ -65,8 +65,14 @@

Contents

  • Reference @@ -74,15 +80,42 @@

    Contents

    More info.

    +
    +

    Public types

    +
    +
    + using public_typedef = int +
    +
    A public typedef.
    +
    +

    Public static variables

    - static bool public_static_variable constexpr + static std::byte public_static_variable constexpr
    A public static variable.
    +
    +

    Public static functions

    +
    +
    + static auto public_static_function() -> struct_1 constexpr +
    +
    A public static function.
    +
    +
    +
    +

    Public functions

    +
    +
    + auto public_function() -> bool +
    +
    A public function.
    +
    +

    Public variables

    @@ -92,6 +125,33 @@

    Public variables

    A public variable.
    +
    +

    Protected types

    +
    +
    + using protected_typedef = int +
    +
    A protected typedef.
    +
    +
    +
    +

    Protected static functions

    +
    +
    + static auto protected_static_function() -> bool constexpr +
    +
    A protected static function.
    +
    +
    +
    +

    Protected functions

    +
    +
    + auto protected_function() -> bool +
    +
    A protected function.
    +
    +

    Protected static variables

    @@ -110,11 +170,59 @@

    Protected variables

    A protected variable.
    +
    +

    Typedef documentation

    +
    +

    + using test::class_1::public_typedef = int +

    +

    A public typedef.

    +

    More info.

    +
    +
    +

    + using test::class_1::protected_typedef = int protected +

    +

    A protected typedef.

    +

    More info.

    +
    +
    +
    +

    Function documentation

    +
    +

    + static struct_1 test::class_1::public_static_function() constexpr +

    +

    A public static function.

    +

    More info.

    +
    +
    +

    + bool test::class_1::public_function() +

    +

    A public function.

    +

    More info.

    +
    +
    +

    + static bool test::class_1::protected_static_function() protected constexpr +

    +

    A protected static function.

    +

    More info.

    +
    +
    +

    + bool test::class_1::protected_function() protected +

    +

    A protected function.

    +

    More info.

    +
    +

    Variable documentation

    -
    +

    - static bool test::class_1::public_static_variable constexpr + static std::byte test::class_1::public_static_variable constexpr

    A public static variable.

    More info.

    diff --git a/tests/test_project/expected_html/code_8h.html b/tests/test_project/expected_html/code_8h.html index 101c191..849a905 100644 --- a/tests/test_project/expected_html/code_8h.html +++ b/tests/test_project/expected_html/code_8h.html @@ -67,6 +67,8 @@

    Contents

  • Namespaces
  • Classes
  • Enums
  • +
  • Typedefs
  • +
  • Functions
  • Variables
  • Defines
  • @@ -110,10 +112,10 @@

    Classes

    Enums

    -
    - enum class scoped_enum { val_0, - val_1 = 1, - val_2 = 2 } +
    + enum class scoped_enum: unsigned { val_0, + val_1 = 1, + val_2 = 2 }
    A C++11 scoped enum.
    @@ -124,6 +126,42 @@

    Enums

    A pre-C++11 unscoped enum.
    +
    +

    Typedefs

    +
    +
    + using a_shit_typedef = int +
    +
    An old-school typedef.
    +
    + using a_typedef = int +
    +
    A C++11 'using' typedef.
    +
    +
    template<typename T>
    + using a_typedef_template = T +
    +
    A C++11 'using' typedef template.
    +
    +
    +
    +

    Functions

    +
    +
    +
    template<typename T, typename U>
    + auto do_the_other_thing(U u) -> T constexpr noexcept +
    +
    A function template.
    +
    + auto do_the_thing() -> std::uint8_t +
    +
    A function.
    +
    + auto do_the_thing_automatically() -> int -> auto +
    +
    A function with a trailing return type.
    +
    +

    Variables

    diff --git a/tests/test_project/expected_html/dir_68267d1309a1af8e8297ef4c3efbcdba.html b/tests/test_project/expected_html/dir_68267d1309a1af8e8297ef4c3efbcdba.html index 1511e59..7b9e8aa 100644 --- a/tests/test_project/expected_html/dir_68267d1309a1af8e8297ef4c3efbcdba.html +++ b/tests/test_project/expected_html/dir_68267d1309a1af8e8297ef4c3efbcdba.html @@ -64,11 +64,19 @@

    Contents

  • Reference
  • +
    +

    Directories

    +
    +
    directory subfolder/
    +
    A subfolder.
    +
    +

    Files

    diff --git a/tests/test_project/expected_html/dir_ed64655242b001a1b5d7ddadcfdd4bf2.html b/tests/test_project/expected_html/dir_ed64655242b001a1b5d7ddadcfdd4bf2.html new file mode 100644 index 0000000..f35284e --- /dev/null +++ b/tests/test_project/expected_html/dir_ed64655242b001a1b5d7ddadcfdd4bf2.html @@ -0,0 +1,134 @@ + + + + src/subfolder/ directory | Test! A C++ Project + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +

    + src/subfolder/ directory +

    +

    A subfolder.

    + +
    +

    Files

    +
    +
    file code.h
    +
    More code, yo.
    +
    +
    +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/tests/test_project/expected_html/files.html b/tests/test_project/expected_html/files.html index 4f6369c..3e0379e 100644 --- a/tests/test_project/expected_html/files.html +++ b/tests/test_project/expected_html/files.html @@ -59,6 +59,12 @@

    Files

  • dir src A folder.
  • diff --git a/tests/test_project/expected_html/namespacetest.html b/tests/test_project/expected_html/namespacetest.html index 1f506d7..fd858a5 100644 --- a/tests/test_project/expected_html/namespacetest.html +++ b/tests/test_project/expected_html/namespacetest.html @@ -68,6 +68,8 @@

    Contents

  • Classes
  • Concepts
  • Enums
  • +
  • Typedefs
  • +
  • Functions
  • Variables
  • @@ -123,9 +125,9 @@

    Concepts

    Enums

    - enum class scoped_enum { val_0, - val_1 = 1, - val_2 = 2 } + enum class scoped_enum: unsigned { val_0, + val_1 = 1, + val_2 = 2 }
    A C++11 scoped enum.
    @@ -136,6 +138,42 @@

    Enums

    A pre-C++11 unscoped enum.
    +
    +

    Typedefs

    +
    +
    + using a_shit_typedef = int +
    +
    An old-school typedef.
    +
    + using a_typedef = int +
    +
    A C++11 'using' typedef.
    +
    +
    template<typename T>
    + using a_typedef_template = T +
    +
    A C++11 'using' typedef template.
    +
    +
    +
    +

    Functions

    +
    +
    +
    template<typename T, typename U>
    + auto do_the_other_thing(U u) -> T constexpr noexcept +
    +
    A function template.
    +
    + auto do_the_thing() -> std::uint8_t +
    +
    A function.
    +
    + auto do_the_thing_automatically() -> int -> auto +
    +
    A function with a trailing return type.
    +
    +

    Variables

    @@ -147,9 +185,9 @@

    Variables

    Enum documentation

    -
    +

    - enum class test::scoped_enum + enum class test::scoped_enum: unsigned
    #include <src/code.h>

    A C++11 scoped enum.

    @@ -158,19 +196,19 @@

    Enumerators - val_0 + val_0

    Value zero.

    - val_1 + val_1

    Value one.

    - val_2 + val_2

    Value two.

    @@ -210,6 +248,96 @@

    +
    +

    Typedef documentation

    +
    +

    + typedef int test::a_shit_typedef +
    #include <src/code.h>
    +

    +

    An old-school typedef.

    +

    More info.

    +
    +
    +

    + using test::a_typedef = int +
    #include <src/code.h>
    +

    +

    A C++11 'using' typedef.

    +

    More info.

    +
    +
    +

    +
    #include <src/code.h>
    +
    + template<typename T> +
    + using test::a_typedef_template = T +

    +

    A C++11 'using' typedef template.

    +

    More info.

    +
    +
    +
    +

    Function documentation

    +
    +

    +
    #include <src/code.h>
    +
    + template<typename T, typename U> +
    + T test::do_the_other_thing(U u) constexpr noexcept +

    +

    A function template.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Template parameters
    TA type.
    UAnother type.
    Parameters
    uAn argument.
    ReturnsA T.
    +

    More info.

    +
    +
    +

    + std::uint8_t test::do_the_thing() +
    #include <src/code.h>
    +

    +

    A function.

    +

    More info.

    +
    +
    +

    + auto test::do_the_thing_automatically() -> int +
    #include <src/code.h>
    +

    +

    A function with a trailing return type.

    +

    More info.

    +
    +

    Variable documentation

    diff --git a/tests/test_project/expected_html/searchdata-v2.js b/tests/test_project/expected_html/searchdata-v2.js index 8d68fc3..86249b6 100644 --- a/tests/test_project/expected_html/searchdata-v2.js +++ b/tests/test_project/expected_html/searchdata-v2.js @@ -1,2 +1,2 @@ /* Generated by https://mcss.mosra.cz/documentation/doxygen/. Do not edit. */ -Search.load('O+!-x000009smFU9tQvb91Q>f+y(#u0RR9100CtL00001YzP1V00CkS00001VH5xW00C(l00001av%Tz00Chp00001b}Rq@00Cbz00001V>kc+00C(}00001bVL9E00Ci200001bWi{Q0RRC200CuO00001Y+wKY00ClV00001VQc^Z00C)o00001a(Dm$00Cis00002b9Pn$0E7Sl00Cc$00001W0U{@00C*500001Y@h%D00CmA000310ssI3WwHPO00C^a00001V!QwV00Cjb00001X~+No00DB&00001VblNs00DN|00001U*G@$00Cp>00001Y3u+100DIJ00001VfX+500DIV000310{{R4WeNcR00C?c0RR92ViW-Y00Chd0RR92X&?ar00D9)0RR92VJrav00MJ%1OWgt0RR92UpN5(00Cq|0RR92Wk>-400DGP0RR92V^jeE00CuM0RR92bYKAh00D1i0RR93b#kl#0BivO00D4v0RR92I(z{D0RaR6I)VX!00A+G0RR92UyuO+00DEB0RR92bDRMH00Cj50RRC22mk;9F|Gjs00CdJ0RR92bhrTk00D5m0RR92WyAph00Cpl0RR92ZqNY$00L}pr~v@h0RRC21poj6G3EgP00Cd>0RR92bMOHG00DFM0RR92Vf+CA00C?Q0ssI3V+aBO00CbP0ssI3WfTGc00DFw0ssI3VITqk00C?!0ssI3a4Z4<00C_>0ssI3WjF!=0RRaA00A;Y0ssI3Uq}J~00DGP0ssI3a8v>S00CuM0ssI3V_*UR00C}h0ssI3Z)^ep00Coi0ssI3I(PyA0Ra{OI(`Cx00Cr#0ssI3WsCv<00DH80ssI3bC?1E00Cv50ssL33jhHC3;+QD4FCWGFflT!0syW80I~uA00CdN0ssI3Y{UWp00Cjj0ssI3cF+O<00BDI0ssL44gfmb0)PMkZQ=p|00DLC0ssI3ZtwyC00Cw80ssI3U;F|900CqI0{{R4We5WR00D3g0{{R4ZxjOn0RR^O0RR{P0RR~Q00S^FG9Uv0Bm)2_0{{R4Un~Ov00C?|0{{R4VL$@_00DMJ0{{R4I!prq0Rb8SI#L6G00C`S0{{R4bzlPk00C}h0{{U49RL6UbaDd#00Com0{{R4b$kN=00DA>0{{R4bch2000L!mYy$v}0{{R4Uz7s?00Cs40{{R4WuyZD00DHW0{{R4bFc#d00CvT0{{U48vp&K00CtN1poj6a0~?i00D0j1poj6V;BVh00DCz1poj6ZX^W&0RR&K00Ct%1poj6Y%~P`00Ck;1poj6VL$}{00C)61poj6a!dsP00CiA1poj6c31@f00CcK1poj6Wn={a00C}l1poj6X>bJq00C@v1poj6ZhQp*0RR;M00DW31poj6bc_W600D541poj6ZI}fB00(1qZgX{MW!wP(J^}!w0svM704fClf&~Dc1poj6I-~^v0Ra>MI>H5j00DH!1poj7b8XNC05Spq00Cvz1pom62LJ#8Xyyd~00A!Q1poj6W$*<600J>Gt^oi<0ssI3U;G6C00DFc1^@s7a0~_j00CtZ1^@s7V;BYi00Lxg_5}bQ1^@s8Y;ULm03-$g0RRR700C<_1^@s7WjqD|00Ct71poj6Z%76J00Co81^@v83IH!u27mwoV_F6P00MGhN(2B11ONa7a$|I21^^lZ0B8mP0Rk2PI$wSQfRqCO00Crx1^@s7WsC*@00DH81^@s7bC?DI00Cv51^@y83jh}Y0sssE7ytqQ4FDMc00S^FGO7juum%9O1^@s7U%UnY00C^u1^@s7VbBHu01I?uaBFjJc6DrNW#9z>E(QQ{0RTb<0CENZqy_-i1^_Ar0L}yef&~Dc1ps>h0J#7F@BjcY0RU?O0FnU!%mD!Y0RR;O073!)c>(~Y0szti0R931Dgyvr0|1o+0L=pc1q1*(1ORaa0Eh$tp#%WD1OVs+02BoPKm`DE1pulA0MrEl{sjQ&=;-K3NU)gr_<-2h*vQz}*vRN0C@9$2*vRNuSOfqXIy!K5Vr*$+UvqR}bZKK>c42a9VPb4$03%^CH#s&oF*Gc42a9VPb4$03%^DGcY+hGi5nqGGS#mG-EboVliPdI5jmhH#IqCWjQtk02(?vaB^>SWn*+@WM6h+a%o{=Y-Io=VP!OAIAvxuF=H}jGGb*hH)Jz7WI1JFV>B>jH8VCjITQc@Iyz%)VRLg|F#uz1VRLhIWpi|2F<&ubY+-YAUtcjUXmo9C6aWA^I&@`iaBN|8WnW`#VRLg|F#uz1VRLhIWpi|2F<&usWo>Y5VRU6*Ut?@xb8}x`F)nCyZEQwmSI5IXjWHB=^IbktjW;kUyGC4P8W;A0nWoBYyWMW}1Xmo9C4gfSdI(A`fUoZe+GB7!1G&C?YVqs%7WM(!uFfd^?F*0OkIb}IGIAUQl4gfSdI(A`fUoik-WHvcuW@a=oV`DKfH90mlVPs}yVPP<4H8^HvWHvN04gfSdI(A`fUorq;GdDCfVr4NhWjACpWH&isH8nOkGGR7iF=b{oI5sn56aW%BI&))haAjm)Wo~tC03%^DVKQPlG&ndhVlrc7G&wXfHa0goH#ImnIbu0EGGaLr05m!}OhrdQLs?%%PE}1`RzXZ(FaTjQVm4+sH)LXDFgay0H)AzrIAJp|WMgDAG&eLbF*0Tn05m!}OhrdQLs?%%PE}1`RzXZ(F#us^F*splHDNPiW-w$kF*YzTGc`0cI5Re3I5#siW@I!H05m!}OhrdQLs?%%PE}1`RzXZ(G5}#WGcjW{Ib&sHIc8%wH#9h5V`gPyV=!SfIWaIaIW%Mx01`Snb#8NGZ*XN~UuAA}Z2%)-V=-YeF*0UmHfCfvVmM)8HZx*mFl03~Gd5vmH!wCe6aW%BI%#ffX>Mg-c42a9VPb4$03%^$GG;YmFg7tVWjQ!yVKZfBIb>xsIb~uoFgZ9eI5#M)EHa9RZVKp%_WM(;KIXF0CVKW*4J~}#fVQgP90AXY{Ib>#LG%;giF)=kcHZ@^nW@TYvFlIG4W@ThHG%*?gJ~}#fVQgPA0AVvXG&N#nF*0R0WHMwoIbk(5HaIe2HexYlW;HlAGh-Y896CB~Wpi|8WM5@&b!`A6VKQVlV`eZkH#229F=H@cH#jjhVlgpfWHM$lF*9Z`V;le+Iy!T7VRUI@Uv^<~X<=e)WdI{#IWuHtVrDfsF=aMoGht$4GC4IdHZeFeGBGqWWMelq6aWA^I&*Y#bz^j2F#vOPa&=>LbY*jNUol@Xb98cbV{~6%F)nCyZEPF>7dkp_Wpi|8WM6Z1a&=>L0ADd*F>YmZbY)~;UvqSFbz^icXmo9C0w4h-0xbbD0XG3X0YU>w155=_1yliA0A2uQ0cikmaA9X<{ADe?dS0RadADE0t=00D0N00001Zv+7V00C(V0RR92bPxdm00Cnb0RR92ZX5vs00DI(0RR95baQrQCIA4w008U&04Msm600MSqt^ok*0RR92U+@6{00CqA0RR92X#fHM00DFc0ssI3VGIHQ00DFo0ssL31poj6WgY?m00C?w0ssI3VkiOt00Chx0ssI3X*2==00DA30ssI3VL$=^0RRmE00AjX0ssL43;-xl0)PMkZdL*S00D1a0ssI3X=DNb00DGr0ssI3V{ifh00C}x0ssI3b$kK<00eY%c4mM902l%QMgjnW0ssI3Ux)$#00Cs00ssI3WuO8800DHS0ssI3W2^!I00CvP0ssI3bhrWl00D2l0ssI4b#hn%0K@_S00D5y0ssI3I@AIH0RavGI@$t&00A-J0ssI3U+4k=00DFE0ssI3bMyiL00Ck80ssL35&!@JF$Mzw00CbL0{{R4bPxjo00D3o0{{R4WgG(l00Cnn0{{R4ZYTo)00L}p00RIn0{{U44*&oGF+KwT00Cb@0{{R4b4UXK00DDO0{{R4VN?SE00C@T0{{R4V_*XS00CcS0{{R4Wo!cg00DGz0{{R4VR!=o00C@%0{{R4aD)Q@00C`^0{{R4Wsm~^0RR&K00Au0Rb)mI;sSK00C{V1ONa5b+`lo00C~k1ONd5F#rGobjkz(00Cpp1ONa5b<_j^00DB^1ONa5bl?O400L!m!~_851ONa5U+e?`00Ct71ONa5W&8vH00DFY1poj6a|i_h00CtV1pom6F8}}mWf}zl00C?s1poj6Vk89s00Cht1poj6X)px<00D9~1poj6VLSx@00DMF1poj6Uq}T200Co81poj6X;cLO00DGb1poj6VPFLS00DGn1poj7ZgUg`0Bi*S00BC31pom7FaSDy1%LnnF@gmE00Cc$1poj6bdUuA00Co`1poj6b({qN00DBM1poj7V{~!^0H_530RS2R0RS5S0RS8T00S^FGPnf*yafQj1poj6U&I9f00C^$1poj6Vb}!#00DO11poj6U*rV<00C|41poj6b?^lM00D0H1poj6W&8yI00CbD1^@s7c?bpo00CnT1^@s7VH5@c00Czj1^@s7WgrFs00C?!1^@s7IxGeN0RbHVIx+@;00C_{1^@s7bwCCH00C}B1^@s7WlRPD00Cc81^@s7WLO3O00CuQ1^@s7aAXDm00D1m1^@s7V{irl00DD$1^@s7ZhQs+0RSEV00Cx*1^@s7WsC*@00Cr@1^@s7Wtav400D5G1^@s7d87sa00DHW1^@s7U$6!M00DHi1^@s7X}ksi00C&i1^@v7AOHXXWzGfw00DH;1^@s7Vb}%$00C^?1^@s7aO4I600C|41^@s7W$*?700DIN1^@v89{^wc1^@s7W&#HQ00CtR2LJ#8WDo}c00Ctd2LJ#8a2y8!00DU-2LJ#9b9BfC04N6l00Cbv2LJ&8ApigYWj+T000C@52LJ#8Vn_!700Ci62LJ#8X;cRQ00DAZ2LJ#8VPFRU00DMp2LJ#8Uu*{e00Cuk2LJ#8Zg>X(00C)!2LJ#8Y=j2@00C}_2LJ&8BLDyaDV7HS0RbWaD4GX=00C#92LJ#8Zm0(U00C*P2LJ#8XtW0a00DHm2LJ#8U%&?d00DBw2LJ#8Wy}Wv00C&y2LJ#8bl3*~0RSZc00AlF2LJ&8CjbBeDeeaV0RbieDDnq@00DXU2LJ#8Yyb!V00C?U2mk;9VGIZW00CnX2mk;9X&49q00DF!2mk;9VI&9u00C_(2mk;9Z!ib|00DG12mk;9bvy_F00Ch_2mk>BBmgL1=m&sE2mk;9XHEzJ00C}V2mk;9X_0t_1*s1^_q*0FVa&ng{^42mk;9I=lz~0RbogI^qa`00DI92mk;Ab8YYl0Ez<{AE({3(00CtZ2><{BF){`N0GtB=00Cbf2><{AbSMb`00D3=2><{AWi$x@00Cn<2><{BWNsJ<06+-<00L}p00RI<2><~A5C8xHYg!2a00CuQ2><~A5dZ)IXle-n00Ay;2><{AWpoJu00Crr2><{AZ-5B^0RS=p00C%>2><{AE|3WT00Cu|2><{AWSj{A00D2J2><{AW2gxL0Ra^NFRlrI00DBe2><{AWxNRh00Cse2><{AY{&@!00D2#2><{AX4DA)00Cm!2><{Ab>ImA00Lukh6w=X2><~B6aX*m34j0rWA+IE00MGhq6Gk81poj9a&=>L{s{o!2>@~g00asE0Rk!jI$zcUfb0YS00Cqm3IG5BWh@E+00DF|3IG5Bb2thB00Ct_3IGBC762;%0st2PEC2!k7yvB*00S^FGD->nPznH63IG5BUt9_R00C@j3IG5BVQ>ln01b3waBFjJc6DrFX=G&p2>?(D0LlUYWC;Kc3IIe30Coxhf(8I41^_q*0FVa&ng{^42msFj00jX6J^=uH0RXcB0Q3O>E&>2=0swLX0IUK4uL1z`0s!^`05bysHUj`%0|1u;0LcRY-~#{+1OP+?0C5BWqyzxa1OWU504N0jTLl1<1pvzh00Ra9I0gW21^}N10L%se0|x*>2LMF}0DK1ke+K}|2LR6p0Okh({|5jX2mnS10CETbo(KTZ2mlBP05S;xTnPYv2>^I_=;-L^si>)_si>)_NJy}l`1pW`*x1;}*x1;}czAf|si~=`si`0+DA?H8$mm#Dm<|9MIy!K5Vr*$+UvznJWn^V$03%^FGBGtUW@0%rVKq54He)bkGG=38GdMA3IXPxDGB#!o02(?vaB^>SWn*+@WM6c7aAjm=W&k5$VKp~nWn*SGVK+5mH)J_sH#0OdVlXy0Fg9j0I5sn54geZDI&gJjY-wX(b97;JX=7h@VRC6(|VPrEmI4}+X8ag^~bz*F3V_$Y*a%o{=Y-Io=VKF&0GG#PkG%#jkFk@peVmUK8Vlp&jF=I7mG-Wk3V-5fsIy!K2Z**m2bY)~;b97;JX=7h@VRC6V=BF<~+|H8nFgH92NwIW`Ue8ag^~a&L5HV{~O?Uv^<~X<=e)WdI{#Wi(_sWo9%nV=`tkVr4NmWHUHqIb~sEG%#f~Gd4Ln4geYgIy!K5Vr*$+UvqR}bZKK>W_503bZKvHC@BCVVP-O8VPP;bHZ(S7W->4{Gc!46VP-TjVK8N8V>DtiFa`iT4geYgIy!K5Vr*$+UuJb~V{~b6ZYU`LBVjgTGdD71GGS&iWjJLxH#9glIc6|qWM(xmVKXo`W?~2cJPrUF0y;Wya&L5HV{~O?UvqR}bZKK>W_503bZKvHC@BCVVPiC6GBRQ{V=-klFk)dbVK6pkFf%bZH8wUfH8NsiI0^tf4geYgIy!K2Z**m2bY)~;W_503bZKvHC@BCVVPZBhWHU81W;HQkIWS^kG&y85IAk(1WjA3rWH)9sI1B(hC;$LDI%8~Mb8}xY0Ap-nb8~cMb97%ZUom5BVRLg|UokFdbZu-X0025VbY*RDY+-a|Ut?@xb8}xY0Ap-nb8~cMb97%ZUomuLZE$R1bY)*(V{Bn_b6;OEE@*UZY)eH;0Ap`tWnVaGE@*UZY$IVfHaBH4WjSRwV>vQ3GdVLfHZo&oH)AnjH#lQ9V>4wG001vzZ)9aIXaHkxWMyACXf9}UZEPq206IEjZ*F5{aCBcW0Ap`%V`Xr3bY*jNUol@XV{dL_WpH#~UokFdbZu-Z0025VV{dL_WpH#~G5}+5ZewL|baZ8NbYC%FF>YmZbY)~;F<&ubZ*F5{aCBc^GA?LzZESOLV*q4na$hz$GB!74F*7hZVKHH5IAu68IX7iyG-ESmW@2MxVqq?5bZu-D00b{{bz)|3Y-D9}0A*x0G&VIgGBh$`FfcJ;F=91jH)LdCWMgJzWHe%CGA?LzZEP6;G&(wVVQgP80AVsPIb}37Ff?LeV>M)EHa9RZVKp%_WM(;KIXF0CVKW&3G&(wVVQgP90AXY{Ib>#LG%;giF)=kcHZ@^nW@TYvFlIG4W@ThHG%*CHqWn?*KV>dT6IALREWnyD6VKg~0Ff=(dWGDa>!5;{6zUvp?_bYFCNaAjm=W&k5$H8EvmFl8_>HZwLiG&W{5FgIZ`Fg7_iWoBbIH8D74C;$>VI$>XQd2nT9Wo7^)VK6ZdHqWiUBpI5=ZwVjln$Uvy<{aBN|8WdLG0FlIC}IA%9wF=RGmHf1wnF*r10H)CZpGGt~pI5#K&5;{6*Zft38WnXq-a%o{=Y-Io=VP-OBHDWL}F*0R2IAvioWo9{KWivTtVlgl|I59XkIVb=U13EfnZ(nq1WnXV}Xk~I=bZBXAXDC%E03%^#Ha226Win+sG-fz7IWsh4W@a^HWiw?pF*0LiFlA*T05kz801^T^I%IEObZBK?bZBXAXDBHEBVjNxF=IAnGB;&8FlI6`IAdfsIAUcvWMyVCHDY5hH#Q^yG$;TP2|7AtZ(nq1WnXk?X>MmEnHe+OAI51>kHaIb4G&D0bI3@rz2Xtj~bO3H)ZDn(CVPj=YmZbY)~NXmo9CE&x6{I(A`fUoZe+GB7!1G&C?YVqs%7WM(!uFfd^?F*0OkIb}IGIAUQlE&x6{I(A`fUoik-WHvcuW@a=oV`DKfH90mlVPs}yVPP<4H8^HvWHvN0E&x6{I(A`fUorq;GdDCfVr4NhWjACpWH&isH8nOkGGR7iF=b{oI5sn5FaR7nI&NiibY)~;Wo~tC03%^CWH)1GFf=zaWjQfpFkv@1F*RZ_F=S*iW->7|W-wzg0313xb97;JX=7h@VRC6LUtcjUXmo9CFaQ@iI&NiibY)~;b98cbV{`ytF<&umWpi|8WM5x%baHiLbS`LgZEO_)054;2WMwXB0CRO>W^Zg{WpZCKV{c?-UpQzkXmo9C0w4h-0xbbD0XG3X0YU>w155=_1yliA0A2uQ0cikmaA9X + + + src/subfolder/code.h file | Test! A C++ Project + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +

    + src/subfolder/code.h file +

    +

    More code, yo.

    +

    This file has the same name as another one at a different level of the file hierarchy. Let's see how Doxygen handles this. I suspect poorly.

    +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/tests/test_project/expected_xml/Test!.tagfile.xml b/tests/test_project/expected_xml/Test!.tagfile.xml index c59f1ee..f3e4522 100644 --- a/tests/test_project/expected_xml/Test!.tagfile.xml +++ b/tests/test_project/expected_xml/Test!.tagfile.xml @@ -19,6 +19,27 @@ a867e1e9e6c924393462cf7c1a78c6c3e + + int + a_shit_typedef + namespacetest.html + a51ed0e00636746f407a20697efc8518e + + + + int + a_typedef + namespacetest.html + a011ffb640026d2f49f9c73fe09d88cfb + + + + T + a_typedef_template + namespacetest.html + ab80f438f7d1d6d6e3b184b7ce32df687 + + unscoped_enum @@ -48,11 +69,32 @@ scoped_enum namespacetest.html - a3a2b94881b2cd4942667975879b992b9 + a393facd551443a4aaea74c533c2bce6f - val_0 - val_1 - val_2 + val_0 + val_1 + val_2 + + + std::uint8_t + do_the_thing + namespacetest.html + a011c6f27e90f228cd68be9def15bc076 + () + + + constexpr T + do_the_other_thing + namespacetest.html + ae66b7e2e94f84934cff5de3e512ce0ee + (U u) noexcept + + + auto + do_the_thing_automatically + namespacetest.html + af2e6008ff8ee50f6cda80da681d44348 + () -> int constexpr bool @@ -62,9 +104,34 @@ + + code.h + subfolder_2code_8h.html + test::class_1 classtest_1_1class__1.html + + int + public_typedef + classtest_1_1class__1.html + a52150fb93a5946c0d2fca381f99f426f + + + + bool + public_function + classtest_1_1class__1.html + a6b372d2af2e8e874869f0ddf50a306fb + () + + + static constexpr struct_1 + public_static_function + classtest_1_1class__1.html + af2caa02646ff203339faf41a0efc4b20 + () + bool public_variable @@ -73,12 +140,33 @@ - static constexpr bool + static constexpr std::byte public_static_variable classtest_1_1class__1.html - a27966148032ee64e169e8e24ad6c7449 + ae05cfedbf34e9f128e87410c09ad3780 + + + + int + protected_typedef + classtest_1_1class__1.html + aa57cecf6a75b7d9a7343b06706f3863c + + bool + protected_function + classtest_1_1class__1.html + ab61d354f51a90bb49d38d22e7a7d7f48 + () + + + static constexpr bool + protected_static_function + classtest_1_1class__1.html + ac4b22b5c1e50ba1a06f0319566252bb8 + () + bool protected_variable @@ -142,6 +230,27 @@ test::struct_1 test::template_class_1 test::concept_1 + + int + a_shit_typedef + namespacetest.html + a51ed0e00636746f407a20697efc8518e + + + + int + a_typedef + namespacetest.html + a011ffb640026d2f49f9c73fe09d88cfb + + + + T + a_typedef_template + namespacetest.html + ab80f438f7d1d6d6e3b184b7ce32df687 + + unscoped_enum @@ -171,11 +280,32 @@ scoped_enum namespacetest.html - a3a2b94881b2cd4942667975879b992b9 + a393facd551443a4aaea74c533c2bce6f - val_0 - val_1 - val_2 + val_0 + val_1 + val_2 + + + std::uint8_t + do_the_thing + namespacetest.html + a011c6f27e90f228cd68be9def15bc076 + () + + + constexpr T + do_the_other_thing + namespacetest.html + ae66b7e2e94f84934cff5de3e512ce0ee + (U u) noexcept + + + auto + do_the_thing_automatically + namespacetest.html + af2e6008ff8ee50f6cda80da681d44348 + () -> int constexpr bool diff --git a/tests/test_project/expected_xml/classtest_1_1class__1.xml b/tests/test_project/expected_xml/classtest_1_1class__1.xml index 263d3e1..6a6844d 100644 --- a/tests/test_project/expected_xml/classtest_1_1class__1.xml +++ b/tests/test_project/expected_xml/classtest_1_1class__1.xml @@ -3,13 +3,68 @@ test::class_1 code.h + + + int + using test::class_1::public_typedef = int + + public_typedef + test::class_1::public_typedef + +A public typedef. + + +More info. + + + + + + + + + int + using test::class_1::protected_typedef = int + + protected_typedef + test::class_1::protected_typedef + +A protected typedef. + + +More info. + + + + + + + + + int + using test::class_1::private_typedef = int + + private_typedef + test::class_1::private_typedef + +A private typedef. + + +More info. + + + + + + - - constexpr bool - constexpr bool test::class_1::public_static_variable + + constexpr std::byte + constexpr std::byte test::class_1::public_static_variable public_static_variable - = false + test::class_1::public_static_variable + = {} A public static variable. @@ -18,7 +73,7 @@ - + @@ -27,6 +82,7 @@ bool test::class_1::public_variable public_variable + test::class_1::public_variable A public variable. @@ -44,6 +100,7 @@ constexpr bool test::class_1::protected_static_variable protected_static_variable + test::class_1::protected_static_variable = false A protected static variable. @@ -53,7 +110,7 @@ - + @@ -62,6 +119,7 @@ bool test::class_1::protected_variable protected_variable + test::class_1::protected_variable A protected variable. @@ -70,7 +128,7 @@ - + @@ -79,6 +137,7 @@ constexpr bool test::class_1::private_static_variable private_static_variable + test::class_1::private_static_variable = false A private static variable. @@ -88,7 +147,7 @@ - + @@ -97,6 +156,7 @@ bool test::class_1::private_variable private_variable + test::class_1::private_variable A private variable. @@ -105,7 +165,115 @@ - + + + + + + struct_1 + static constexpr struct_1 test::class_1::public_static_function + () + public_static_function + test::class_1::public_static_function + +A public static function. + + +More info. + + + + + + + + + bool + bool test::class_1::public_function + () + public_function + test::class_1::public_function + +A public function. + + +More info. + + + + + + + + + bool + static constexpr bool test::class_1::protected_static_function + () + protected_static_function + test::class_1::protected_static_function + +A protected static function. + + +More info. + + + + + + + + + bool + bool test::class_1::protected_function + () + protected_function + test::class_1::protected_function + +A protected function. + + +More info. + + + + + + + + + bool + static constexpr bool test::class_1::private_static_function + () + private_static_function + test::class_1::private_static_function + +A private static function. + + +More info. + + + + + + + + + bool + bool test::class_1::private_function + () + private_function + test::class_1::private_function + +A private function. + + +More info. + + + + @@ -114,13 +282,35 @@ More info. - + + + + + + + + + + public_static_variable + + + + + test::class_1private_function + test::class_1private_static_function test::class_1private_static_variable + test::class_1private_typedef test::class_1private_variable + test::class_1protected_function + test::class_1protected_static_function test::class_1protected_static_variable + test::class_1protected_typedef test::class_1protected_variable - test::class_1public_static_variable + test::class_1public_function + test::class_1public_static_function + test::class_1public_static_variable + test::class_1public_typedef test::class_1public_variable diff --git a/tests/test_project/expected_xml/classtest_1_1template__class__1.xml b/tests/test_project/expected_xml/classtest_1_1template__class__1.xml index 486a37b..a200c92 100644 --- a/tests/test_project/expected_xml/classtest_1_1template__class__1.xml +++ b/tests/test_project/expected_xml/classtest_1_1template__class__1.xml @@ -23,7 +23,7 @@ - + diff --git a/tests/test_project/expected_xml/code_8h.xml b/tests/test_project/expected_xml/code_8h.xml index 8981b76..34c485a 100644 --- a/tests/test_project/expected_xml/code_8h.xml +++ b/tests/test_project/expected_xml/code_8h.xml @@ -25,10 +25,11 @@ - - + + unsigned scoped_enum - + test::scoped_enum + val_0 Value zero. @@ -36,7 +37,7 @@ - + val_1 = 1 @@ -45,7 +46,7 @@ - + val_2 = 2 @@ -62,11 +63,12 @@ - + unscoped_enum + test::unscoped_enum LEGACY_ENUM_VAL_0 @@ -101,7 +103,62 @@ - + + + + + + int + typedef int test::a_shit_typedef + + a_shit_typedef + test::a_shit_typedef + +An old-school typedef. + + +More info. + + + + + + + int + using test::a_typedef = typedef int + + a_typedef + test::a_typedef + +A C++11 'using' typedef. + + +More info. + + + + + + + + + typename T + + + T + using test::a_typedef_template = typedef T + + a_typedef_template + test::a_typedef_template + +A C++11 'using' typedef template. + + +More info. + + + + @@ -110,6 +167,7 @@ constexpr bool test::inline_variable inline_variable + test::inline_variable = false An inline variable. @@ -122,6 +180,96 @@ + + + + + typename T + + + typename U + + + T + constexpr T test::do_the_other_thing + (U u) noexcept + do_the_other_thing + test::do_the_other_thing + + U + u + + +A function template. + + +More info. + +T + + +A type. + + + + +U + + +Another type. + + + + + +u + + +An argument. + + + +A T. + + + + + + + + + std::uint8_t + std::uint8_t test::do_the_thing + () + do_the_thing + test::do_the_thing + +A function. + + +More info. + + + + + + + auto + auto test::do_the_thing_automatically + () -> int + do_the_thing_automatically + test::do_the_thing_automatically + +A function with a trailing return type. + + +More info. + + + + + + Code, yo. diff --git a/tests/test_project/expected_xml/concepttest_1_1concept__1.xml b/tests/test_project/expected_xml/concepttest_1_1concept__1.xml index 9d7e73c..4b72f32 100644 --- a/tests/test_project/expected_xml/concepttest_1_1concept__1.xml +++ b/tests/test_project/expected_xml/concepttest_1_1concept__1.xml @@ -29,6 +29,6 @@ concept test::concept_ - + diff --git a/tests/test_project/expected_xml/concepttest_1_1nested_1_1concept__2.xml b/tests/test_project/expected_xml/concepttest_1_1nested_1_1concept__2.xml index 4d56ed7..e60f60d 100644 --- a/tests/test_project/expected_xml/concepttest_1_1nested_1_1concept__2.xml +++ b/tests/test_project/expected_xml/concepttest_1_1nested_1_1concept__2.xml @@ -29,6 +29,6 @@ concept test - + diff --git a/tests/test_project/expected_xml/dir_68267d1309a1af8e8297ef4c3efbcdba.xml b/tests/test_project/expected_xml/dir_68267d1309a1af8e8297ef4c3efbcdba.xml index c267b86..508d994 100644 --- a/tests/test_project/expected_xml/dir_68267d1309a1af8e8297ef4c3efbcdba.xml +++ b/tests/test_project/expected_xml/dir_68267d1309a1af8e8297ef4c3efbcdba.xml @@ -2,6 +2,7 @@ src + src/empty_subfolder src/subfolder code.h diff --git a/tests/test_project/expected_xml/dir_ed64655242b001a1b5d7ddadcfdd4bf2.xml b/tests/test_project/expected_xml/dir_ed64655242b001a1b5d7ddadcfdd4bf2.xml new file mode 100644 index 0000000..9b63010 --- /dev/null +++ b/tests/test_project/expected_xml/dir_ed64655242b001a1b5d7ddadcfdd4bf2.xml @@ -0,0 +1,13 @@ + + + + src/subfolder + code.h + +A subfolder. + + + + + + diff --git a/tests/test_project/expected_xml/index.xml b/tests/test_project/expected_xml/index.xml index 4227c21..b67e5a5 100644 --- a/tests/test_project/expected_xml/index.xml +++ b/tests/test_project/expected_xml/index.xml @@ -1,12 +1,21 @@ test::class_1 - public_static_variable + public_typedef + protected_typedef + private_typedef + public_static_variable public_variable protected_static_variable protected_variable private_static_variable private_variable + public_static_function + public_function + protected_static_function + protected_function + private_static_function + private_function test::struct_1::nested_struct @@ -28,11 +37,17 @@ LEGACY_ENUM_VAL_0 LEGACY_ENUM_VAL_1 LEGACY_ENUM_VAL_2 - scoped_enum - val_0 - val_1 - val_2 + scoped_enum + val_0 + val_1 + val_2 + a_shit_typedef + a_typedef + a_typedef_template inline_variable + do_the_thing + do_the_other_thing + do_the_thing_automatically test::empty @@ -44,12 +59,22 @@ LEGACY_ENUM_VAL_0 LEGACY_ENUM_VAL_1 LEGACY_ENUM_VAL_2 - scoped_enum - val_0 - val_1 - val_2 + scoped_enum + val_0 + val_1 + val_2 + a_shit_typedef + a_typedef + a_typedef_template inline_variable + do_the_thing + do_the_other_thing + do_the_thing_automatically + + code.h src - \ No newline at end of file + src/subfolder + + \ No newline at end of file diff --git a/tests/test_project/expected_xml/namespacetest.xml b/tests/test_project/expected_xml/namespacetest.xml index 7fcdf37..a08038b 100644 --- a/tests/test_project/expected_xml/namespacetest.xml +++ b/tests/test_project/expected_xml/namespacetest.xml @@ -3,10 +3,11 @@ test - - + + unsigned scoped_enum - + test::scoped_enum + val_0 Value zero. @@ -14,7 +15,7 @@ - + val_1 = 1 @@ -23,7 +24,7 @@ - + val_2 = 2 @@ -40,11 +41,12 @@ - + unscoped_enum + test::unscoped_enum LEGACY_ENUM_VAL_0 @@ -79,7 +81,62 @@ - + + + + + + int + typedef int test::a_shit_typedef + + a_shit_typedef + test::a_shit_typedef + +An old-school typedef. + + +More info. + + + + + + + int + using test::a_typedef = typedef int + + a_typedef + test::a_typedef + +A C++11 'using' typedef. + + +More info. + + + + + + + + + typename T + + + T + using test::a_typedef_template = typedef T + + a_typedef_template + test::a_typedef_template + +A C++11 'using' typedef template. + + +More info. + + + + @@ -88,6 +145,7 @@ constexpr bool test::inline_variable inline_variable + test::inline_variable = false An inline variable. @@ -100,6 +158,96 @@ + + + + + typename T + + + typename U + + + T + constexpr T test::do_the_other_thing + (U u) noexcept + do_the_other_thing + test::do_the_other_thing + + U + u + + +A function template. + + +More info. + +T + + +A type. + + + + +U + + +Another type. + + + + + +u + + +An argument. + + + +A T. + + + + + + + + + std::uint8_t + std::uint8_t test::do_the_thing + () + do_the_thing + test::do_the_thing + +A function. + + +More info. + + + + + + + auto + auto test::do_the_thing_automatically + () -> int + do_the_thing_automatically + test::do_the_thing_automatically + +A function with a trailing return type. + + +More info. + + + + + + A namespace. diff --git a/tests/test_project/expected_xml/namespacetest_1_1empty.xml b/tests/test_project/expected_xml/namespacetest_1_1empty.xml index 6c722fe..4d9b35b 100644 --- a/tests/test_project/expected_xml/namespacetest_1_1empty.xml +++ b/tests/test_project/expected_xml/namespacetest_1_1empty.xml @@ -8,6 +8,6 @@ More info. - + diff --git a/tests/test_project/expected_xml/namespacetest_1_1nested.xml b/tests/test_project/expected_xml/namespacetest_1_1nested.xml index 790db5f..5f8ec7c 100644 --- a/tests/test_project/expected_xml/namespacetest_1_1nested.xml +++ b/tests/test_project/expected_xml/namespacetest_1_1nested.xml @@ -7,6 +7,6 @@ - + test::nested::concept_2 \ No newline at end of file diff --git a/tests/test_project/expected_xml/structtest_1_1struct__1.xml b/tests/test_project/expected_xml/structtest_1_1struct__1.xml index bbeeb88..97aca39 100644 --- a/tests/test_project/expected_xml/structtest_1_1struct__1.xml +++ b/tests/test_project/expected_xml/structtest_1_1struct__1.xml @@ -8,6 +8,7 @@ nested_enum + test::struct_1::nested_enum val_0 @@ -51,6 +52,7 @@ constexpr bool test::struct_1::static_variable static_variable + test::struct_1::static_variable = false A static variable. diff --git a/tests/test_project/expected_xml/subfolder_2code_8h.xml b/tests/test_project/expected_xml/subfolder_2code_8h.xml new file mode 100644 index 0000000..cc71e27 --- /dev/null +++ b/tests/test_project/expected_xml/subfolder_2code_8h.xml @@ -0,0 +1,13 @@ + + + + code.h + +More code, yo. + + +This file has the same name as another one at a different level of the file hierarchy. Let's see how Doxygen handles this. I suspect poorly. + + + + diff --git a/tests/test_project/src/code.h b/tests/test_project/src/code.h index e939557..6fe351d 100644 --- a/tests/test_project/src/code.h +++ b/tests/test_project/src/code.h @@ -58,12 +58,24 @@ namespace test public: /// \brief A public static variable. /// \details More info. - static constexpr bool public_static_variable = false; + static constexpr std::byte public_static_variable = {}; /// \brief A public variable. /// \details More info. bool public_variable; + /// \brief A public static function. + /// \details More info. + static constexpr struct_1 public_static_function(); + + /// \brief A public function. + /// \details More info. + bool public_function(); + + /// \brief A public typedef. + /// \details More info. + using public_typedef = int; + protected: /// \brief A protected static variable. /// \details More info. @@ -73,6 +85,18 @@ namespace test /// \details More info. bool protected_variable; + /// \brief A protected static function. + /// \details More info. + static constexpr bool protected_static_function(); + + /// \brief A protected function. + /// \details More info. + bool protected_function(); + + /// \brief A protected typedef. + /// \details More info. + using protected_typedef = int; + private: /// \brief A private static variable. /// \details More info. @@ -81,6 +105,18 @@ namespace test /// \brief A private variable. /// \details More info. bool private_variable; + + /// \brief A private static function. + /// \details More info. + static constexpr bool private_static_function(); + + /// \brief A private function. + /// \details More info. + bool private_function(); + + /// \brief A private typedef. + /// \details More info. + using private_typedef = int; }; /// \brief A template class. @@ -133,7 +169,7 @@ namespace test /// \brief A C++11 scoped enum. /// \details More info. - enum class scoped_enum + enum class scoped_enum : unsigned { val_0, ///< Value zero. val_1 = 1, ///< Value one. @@ -141,4 +177,37 @@ namespace test /// \brief Value two. val_2 = 2 }; + + /// \brief A function. + /// \details More info. + std::uint8_t do_the_thing(); + + /// \brief A function template. + /// \details More info. + /// \tparam T A type. + /// \tparam U Another type. + /// \param u An argument. + /// \returns A T. + template + constexpr T do_the_other_thing(U u) noexcept + { + return T{}; + } + + /// \brief A function with a trailing return type + /// \details More info. + auto do_the_thing_automatically() -> int; + + /// \brief An old-school typedef. + /// \details More info. + typedef int a_shit_typedef; + + /// \brief A C++11 'using' typedef. + /// \details More info. + using a_typedef = int; + + /// \brief A C++11 'using' typedef template. + /// \details More info. + template + using a_typedef_template = T; } diff --git a/tests/test_project/src/empty_subfolder/folder.dox b/tests/test_project/src/empty_subfolder/folder.dox new file mode 100644 index 0000000..cdf6c22 --- /dev/null +++ b/tests/test_project/src/empty_subfolder/folder.dox @@ -0,0 +1,3 @@ +/** \dir +\brief An empty subfolder. Poxy should remove this. +*/ diff --git a/tests/test_project/src/subfolder/code.h b/tests/test_project/src/subfolder/code.h new file mode 100644 index 0000000..0632bba --- /dev/null +++ b/tests/test_project/src/subfolder/code.h @@ -0,0 +1,4 @@ +/// \file +/// \brief More code, yo. +/// \details This file has the same name as another one at a different level of the file hierarchy. +/// Let's see how Doxygen handles this. I suspect poorly.