Skip to content

Commit

Permalink
[sourcegen] Add doxygen tag lookup
Browse files Browse the repository at this point in the history
  • Loading branch information
ischoegl committed Dec 29, 2024
1 parent 427c9f1 commit 963a3bd
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 33 deletions.
124 changes: 101 additions & 23 deletions interfaces/sourcegen/sourcegen/_TagFileParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ def qualified_name(self):
return self.name


@dataclass(frozen=True)
@with_unpack_iter
class TagDetails(TagInfo):
"""Create tag information based on XML data."""

location: str = "" #: file containing doxygen description
briefdescription: str = "" #: brief doxygen description
parameterlist: list[Param] = None #: annotated doxygen parameter list


class TagFileParser:
"""Class handling contents of doxygen tag file."""

Expand Down Expand Up @@ -101,9 +111,8 @@ def xml_compounds(kind: str, names: list[str]) -> dict[str,str]:
unknown = set(bases) - set(class_names)
if unknown:
unknown = "', '".join(unknown)
_logger.warning(
"Class(es) in configuration file are missing from tag file: '%s'",
unknown)
_logger.warning("Class(es) in configuration file are missing "
f"from tag file: {unknown!r}")

# Parse content of classes that are specified by the configuration file
class_names = set(bases) & set(class_names)
Expand Down Expand Up @@ -144,7 +153,7 @@ def tag_info(self, func_string: str) -> TagInfo:
"""Look up tag information based on (partial) function signature."""
cxx_func = func_string.split("(")[0].split(" ")[-1]
if cxx_func not in self._known:
_logger.critical(f"Did not find {cxx_func!r} in doxygen tag file.")
_logger.critical(f"Could not find {cxx_func!r} in doxygen tag file.")
sys.exit(1)
ix = 0
if len(self._known[cxx_func]) > 1:
Expand Down Expand Up @@ -174,28 +183,97 @@ def tag_info(self, func_string: str) -> TagInfo:
return TagInfo.from_xml(cxx_func, self._known[cxx_func][ix])


def xml_tag(tag: str, text: str, suffix: str="", index=0) -> str:
"""Extract content enclosed between XML tags, optionally skipping matches."""
if suffix:
suffix = f" {suffix.strip()}"
regex = re.compile(rf'(?<=<{tag}{suffix}>)(.*?)(?=</{tag}>)', flags=re.DOTALL)
match = re.findall(regex, text)
if index >= len(match):
return "" # not enough matches found
return match[index].strip()


def xml_tags(tag: str, text: str, suffix: str="") -> list[str]:
def tag_lookup(tag_info: TagInfo) -> TagDetails:
"""Retrieve tag details from doxygen tree."""
xml_file = _xml_path / tag_info.anchorfile
if not xml_file.exists():
msg = (f"XML file does not exist at expected location: {xml_file}")
_logger.error(msg)
return TagDetails()

with xml_file.open() as fid:
xml_details = fid.read()

id_ = tag_info.id
regex = re.compile(rf'<memberdef kind="function" id="{id_}"[\s\S]*?</memberdef>')
matches = re.findall(regex, xml_details)

if not matches:
_logger.error(f"No XML matches found for {tag_info.qualified_name!r}")
return TagDetails()
if len(matches) != 1:
_logger.error(f"Inconclusive XML matches found for {tag_info.qualified_name!r}")
return TagDetails()

def cleanup(entry: str) -> str:
# Remove stray XML markup
if entry.startswith("<para>"):
entry = xml_tag("para", entry)
if "<ref" in entry:
regex = re.compile(r'<ref [\s\S]*?>')
for ref in re.findall(regex, entry):
entry = entry.replace(ref, "<ref>")
entry = entry.replace("<ref>", "").replace("</ref>", "")
return entry

def resolve_parameteritem(par_map: str) -> list[Param]:
# Resolve/flatten parameter list
name_lines = xml_tag("parameternamelist", par_map).split("\n")
regex = re.compile(r'(?<=<parametername)(.*?)(?=>)')
names = []
directions = []
for name_line in name_lines:
direction = re.findall(regex, name_line)[0]
if "=" in direction:
name_line = name_line.replace(direction, "")
direction = direction.split("=")[1].strip('"')
directions.append(direction)
names.append(xml_tag("parametername", name_line))
description = cleanup(xml_tag("parameterdescription", par_map))
return [Param("", n, description, d) for n, d in zip(names, directions)]

xml = matches[0]
par_list = []
par_block = xml_tag("parameterlist", xml, suffix='kind="param"')
if par_block:
for par_map in xml_tags("parameteritem", par_block):
par_list.extend(resolve_parameteritem(par_map))

def xml_attribute(attr: str, text: str, *, entry: str) -> str:
"""Extract XML attribute."""
regex = re.compile(rf'(?<=<{attr} )(.*?)(?=/>)', flags=re.DOTALL)
match = re.findall(regex, text)
if not match:
return "" # not enough matches found
entries = dict([tuple(_.split("=")) for _ in match[0].strip().split(" ")])
return entries.get(entry, "").strip('"')

return TagDetails(*tag_info,
xml_attribute("location", xml, entry="file"),
cleanup(xml_tag("briefdescription", xml)),
par_list)


def xml_tag(tag: str, text: str, suffix: str="") -> str:
"""Extract first match of content enclosed between XML tags."""
match = xml_tags(tag, text, suffix, True)
if match:
return match[0].strip()
return "" # not enough matches found


def xml_tags(tag: str, text: str, suffix: str="", permissive: bool=False) -> list[str]:
"""Extract list of content enclosed by XML tags."""
if suffix:
suffix = f" {suffix.strip()}"
regex = re.compile(rf'(?<=<{tag}{suffix}>)(.*?)(?=</{tag}>)',
flags=re.DOTALL|re.MULTILINE)
matched = re.findall(regex, text)
if not matched:
blanks = text.split("\n")[-1].split("<")[0]
msg = f"Could not extract {tag!r} from:\n{blanks}{text}\n"
msg += f"using regex: {regex}"
_logger.error(msg)
return []
return matched
if matched or permissive:
return matched

blanks = text.split("\n")[-1].split("<")[0]
msg = f"Could not extract {tag!r} from:\n{blanks}{text}\n"
msg += f"using regex: {regex}"
_logger.error(msg)
return []
20 changes: 10 additions & 10 deletions interfaces/sourcegen/sourcegen/clib/_CLibSourceGenerator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@

from ._Config import Config

from .._dataclasses import HeaderFile, Recipe #ArgList, Param, Func
from .._dataclasses import HeaderFile, Recipe, Param #ArgList, Param, Func
from .._SourceGenerator import SourceGenerator
from .._TagFileParser import TagFileParser #, TagDetails, tag_lookup
from .._TagFileParser import TagFileParser, TagDetails, tag_lookup


_logger = logging.getLogger()
Expand Down Expand Up @@ -57,22 +57,22 @@ def build_declaration(self, recipe: Recipe) -> tuple[str, str, list[str]]:

if recipe.implements:
tag_info = self._doxygen_tags.tag_info(recipe.implements)
_logger.info(f" recipe for {recipe.name!r}: {tag_info.anchorfile}")
# details = tag_lookup(tag_info)
details = tag_lookup(tag_info)

# # convert XML return type to format suitable for crosswalk
# ret_type = Param.from_xml(details.type).p_type
# convert XML return type to format suitable for crosswalk
ret_type = Param.from_xml(details.type).p_type
_logger.info(f" recipe for {recipe.name!r}: {details.qualified_name}")
# ret_param, buffer_params, cabinets = self._ret_crosswalk(ret_type)
# par_list = merge_params(recipe.implements, details)
# prop_params, prop_cabinets = self._prop_crosswalk(par_list)
# cabinets += prop_cabinets
# args = prop_params + buffer_params

elif recipe.what == "destructor":
_logger.info(f" recipe for {recipe.name!r}: destructor")
# args = [Param("int", "handle", f"Handle to {recipe.base} object.")]
# details = TagDetails(
# "", "", "", "", "", "", f"Delete {recipe.base} object.", "", args)
args = [Param("int", "handle", f"Handle to {recipe.base} object.")]
details = TagDetails(
"", "", "", "", "", "", f"Delete {recipe.base} object.", "", args)
_logger.info(f" recipe for {recipe.name!r}: void")
# ret_param = Param(
# "int", "", "Zero for success and -1 for exception handling.")
# annotations = f"//! {details.briefdescription}"
Expand Down

0 comments on commit 963a3bd

Please sign in to comment.