From 3c51899a5abe7319272ebad0043c7b827f597333 Mon Sep 17 00:00:00 2001 From: Rob van der Most Date: Tue, 18 Jan 2022 19:34:52 +0100 Subject: [PATCH] refactor: common parser infrastructure for different input types Enables: #38 and #41 --- Makefile | 2 + asciidoxy/api_reference.py | 96 +++++- asciidoxy/cli.py | 15 +- asciidoxy/packaging/manager.py | 19 +- asciidoxy/parser/__init__.py | 5 + asciidoxy/parser/base.py | 45 +++ asciidoxy/parser/doxygen/__init__.py | 4 +- asciidoxy/parser/doxygen/driver.py | 209 ----------- asciidoxy/parser/doxygen/driver_base.py | 44 --- asciidoxy/parser/doxygen/language/__init__.py | 14 + .../parser/doxygen/{ => language}/cpp.py | 13 +- .../parser/doxygen/{ => language}/java.py | 8 +- .../parser/doxygen/{ => language}/objc.py | 10 +- .../parser/doxygen/{ => language}/python.py | 8 +- .../{parser_base.py => language_parser.py} | 24 +- asciidoxy/parser/doxygen/parser.py | 127 +++++++ asciidoxy/parser/doxygen/type_parser.py | 36 +- asciidoxy/parser/factory.py | 43 +++ documentation/Makefile | 2 + tests/unit/conftest.py | 35 +- tests/unit/packaging/test_manager.py | 20 +- tests/unit/parser/doxygen/test_objc.py | 2 +- tests/unit/parser/doxygen/test_parser.py | 283 --------------- .../parser/doxygen/typeparser/conftest.py | 14 +- .../parser/doxygen/typeparser/test_cpp.py | 136 ++++---- .../parser/doxygen/typeparser/test_java.py | 61 ++-- .../parser/doxygen/typeparser/test_objc.py | 62 ++-- .../parser/doxygen/typeparser/test_python.py | 2 +- .../doxygen/typeparser/test_type_parser.py | 46 ++- tests/unit/shared.py | 1 + tests/unit/test_api_reference.py | 324 +++++++++++++++++- 31 files changed, 910 insertions(+), 800 deletions(-) create mode 100644 asciidoxy/parser/base.py delete mode 100644 asciidoxy/parser/doxygen/driver.py delete mode 100644 asciidoxy/parser/doxygen/driver_base.py create mode 100644 asciidoxy/parser/doxygen/language/__init__.py rename asciidoxy/parser/doxygen/{ => language}/cpp.py (95%) rename asciidoxy/parser/doxygen/{ => language}/java.py (96%) rename asciidoxy/parser/doxygen/{ => language}/objc.py (97%) rename asciidoxy/parser/doxygen/{ => language}/python.py (93%) rename asciidoxy/parser/doxygen/{parser_base.py => language_parser.py} (95%) create mode 100644 asciidoxy/parser/doxygen/parser.py create mode 100644 asciidoxy/parser/factory.py diff --git a/Makefile b/Makefile index 7680105b..ddbd2552 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,7 @@ export LATEST_DOXYGEN_VERSION := 1.9.2 DOCKER_IMAGE_NAME ?= silvester747/asciidoxy DOCKER_IMAGE_VERSION ?= testing DOCKER_IMAGE_PLATFORM ?= linux/amd64 +LOG_LEVEL ?= WARNING help: @python3 -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) @@ -161,6 +162,7 @@ visual-test-$(notdir $(basename $(1))): $(patsubst %.toml,%.adoc,$(1)) --debug \ --require asciidoctor-diagram \ --failure-level ERROR \ + --log $(LOG_LEVEL) \ --multipage mv $(VISUAL_TEST_CASE_BUILD_DIR)/debug.json $(VISUAL_TEST_CASE_BUILD_DIR)/$(notdir $(basename $(1))).debug.json diff --git a/asciidoxy/api_reference.py b/asciidoxy/api_reference.py index e5f1ff2d..4ae0c6ee 100644 --- a/asciidoxy/api_reference.py +++ b/asciidoxy/api_reference.py @@ -13,12 +13,17 @@ # limitations under the License. """API reference storage and search.""" +import logging import re from abc import ABC, abstractmethod from collections import defaultdict -from typing import Dict, List, Optional, Tuple, TypeVar +from typing import Dict, List, Optional, Set, Tuple, TypeVar -from .model import Compound, ReferableElement +from tqdm import tqdm + +from .model import Compound, ReferableElement, TypeRef + +logger = logging.getLogger(__name__) class AmbiguousLookupError(Exception): @@ -359,11 +364,25 @@ class ApiReference: elements: List[ReferableElement] _id_index: Dict[str, ReferableElement] _name_index: Dict[str, List[ReferableElement]] + _unresolved_refs: List[TypeRef] + _unchecked_refs: List[TypeRef] + _inner_type_refs: List[Tuple[Compound, TypeRef]] def __init__(self): self.elements = [] self._id_index = {} self._name_index = defaultdict(list) + self._unresolved_refs = [] + self._unchecked_refs = [] + self._inner_type_refs = [] + + @property + def unresolved_ref_count(self): + return len(self._unresolved_refs) + len(self._inner_type_refs) + + @property + def unchecked_ref_count(self): + return len(self._unchecked_refs) def append(self, element: ReferableElement) -> None: self.elements.append(element) @@ -448,6 +467,79 @@ def find(self, raise AmbiguousLookupError(matches) + def add_unresolved_reference(self, ref: TypeRef) -> None: + self._unresolved_refs.append(ref) + + def add_inner_type_reference(self, parent: Compound, ref: TypeRef) -> None: + self._inner_type_refs.append((parent, ref)) + + def add_unchecked_reference(self, ref: TypeRef) -> None: + self._unchecked_refs.append(ref) + + def resolve_references(self, progress: Optional[tqdm] = None) -> None: + """Resolve all references between objects from different XML files.""" + + unresolved_names: Set[str] = set() + if progress is not None: + progress.total = len(self._unresolved_refs) + len(self._inner_type_refs) + + still_unresolved_refs = [] + for ref in self._unresolved_refs: + if progress is not None: + progress.update() + assert ref.name + element = self.resolve_reference(ref) + if element is not None: + ref.resolve(element) + else: + still_unresolved_refs.append(ref) + unresolved_names.add(ref.name) + + still_unresolved_inner_type_refs: List[Tuple[Compound, TypeRef]] = [] + for parent, ref in self._inner_type_refs: + if progress is not None: + progress.update() + assert ref.name + element = self.resolve_reference(ref) + if element is not None: + assert isinstance(element, Compound) + if ref.prot: + element.prot = ref.prot + parent.members.append(element) + else: + still_unresolved_inner_type_refs.append((parent, ref)) + unresolved_names.add(ref.name) + + resolved_ref_count = len(self._unresolved_refs) - len(still_unresolved_refs) + resolved_inner_type_ref_count = (len(self._inner_type_refs) - + len(still_unresolved_inner_type_refs)) + unresolved_ref_count = len(still_unresolved_refs) + len(still_unresolved_inner_type_refs) + logger.debug(f"Resolved refs: {resolved_ref_count + resolved_inner_type_ref_count}") + logger.debug(f"Still unresolved: {unresolved_ref_count}: {', '.join(unresolved_names)}") + + self._unresolved_refs = still_unresolved_refs + self._inner_type_refs = still_unresolved_inner_type_refs + + def check_references(self, progress: Optional[tqdm] = None) -> None: + """Verify all references point to an existing element.""" + if progress is not None: + progress.total = len(self._unchecked_refs) + + for ref in self._unchecked_refs: + if progress is not None: + progress.update() + if self.resolve_reference(ref) is None: + logger.warning(f"Unknown reference id `{ref.id}`. Some XML files may be missing or" + " cannot be parsed.") + ref.id = None + self._unchecked_refs = [] + + def resolve_reference(self, ref: TypeRef) -> Optional[ReferableElement]: + try: + return self.find(ref.name, target_id=ref.id, lang=ref.language, namespace=ref.namespace) + except AmbiguousLookupError: + return None + MaybeOptionalStr = TypeVar("MaybeOptionalStr", str, Optional[str]) diff --git a/asciidoxy/cli.py b/asciidoxy/cli.py index cf14c7ab..bd105941 100644 --- a/asciidoxy/cli.py +++ b/asciidoxy/cli.py @@ -28,7 +28,6 @@ from .generator import process_adoc from .model import json_repr from .packaging import CollectError, PackageManager, SpecificationError -from .parser.doxygen import Driver as DoxygenDriver def error(*args, **kwargs) -> None: @@ -54,6 +53,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: logger = logging.getLogger(__name__) pkg_mgr = PackageManager(config.build_dir, config.warnings_are_errors) + api_reference = ApiReference() if config.spec_file is not None: try: with tqdm(desc="Collecting packages ", unit="pkg") as progress: @@ -65,24 +65,19 @@ def main(argv: Optional[Sequence[str]] = None) -> None: logger.exception("Failed to collect packages.") sys.exit(1) - xml_parser = DoxygenDriver(force_language=config.force_language) with tqdm(desc="Loading API reference ", unit="pkg") as progress: - pkg_mgr.load_reference(xml_parser, progress) + pkg_mgr.load_reference(api_reference, config, progress) with tqdm(desc="Resolving references ", unit="ref") as progress: - xml_parser.resolve_references(progress) + api_reference.resolve_references(progress) with tqdm(desc="Checking references ", unit="ref") as progress: - xml_parser.check_references(progress) + api_reference.check_references(progress) if config.debug: logger.info("Writing debug data, sorry for the delay!") with (config.build_dir / "debug.json").open("w", encoding="utf-8") as f: - json.dump(xml_parser.api_reference.elements, f, default=json_repr, indent=2) - - api_reference = xml_parser.api_reference - else: - api_reference = ApiReference() + json.dump(api_reference.elements, f, default=json_repr, indent=2) if config.backend == "adoc": pkg_mgr.work_dir = config.destination_dir diff --git a/asciidoxy/packaging/manager.py b/asciidoxy/packaging/manager.py index 7af90d14..428c8d25 100644 --- a/asciidoxy/packaging/manager.py +++ b/asciidoxy/packaging/manager.py @@ -21,8 +21,10 @@ from tqdm import tqdm +from ..api_reference import ApiReference +from ..config import Configuration from ..document import Document, Package -from ..parser.doxygen import Driver +from ..parser import parser_factory from .collect import CollectError, collect, specs_from_file logger = logging.getLogger(__name__) @@ -141,21 +143,24 @@ def collect(self, packages = loop.run_until_complete(collect(specs, download_dir, progress)) self.packages.update({pkg.name: pkg for pkg in packages}) - def load_reference(self, parser: Driver, progress: Optional[tqdm] = None) -> None: + def load_reference(self, + api_reference: ApiReference, + config: Configuration, + progress: Optional[tqdm] = None) -> None: """Load API reference from available packages. Args: - parser: Parser to feed the API reference. - progress: Optional progress reporting. + api_reference: API reference collection to store loaded reference in. + config: Application configuration. + progress: Optional progress reporting. """ if progress is not None: progress.total = len(self.packages) progress.update(0) for pkg in self.packages.values(): - if pkg.reference_dir is not None: - for xml_file in pkg.reference_dir.glob("**/*.xml"): - parser.parse(xml_file) + if pkg.reference_type and pkg.reference_dir is not None: + parser_factory(pkg.reference_type, api_reference, config).parse(pkg.reference_dir) if progress is not None: progress.update() diff --git a/asciidoxy/parser/__init__.py b/asciidoxy/parser/__init__.py index 2b4353d0..6fd4d303 100644 --- a/asciidoxy/parser/__init__.py +++ b/asciidoxy/parser/__init__.py @@ -12,3 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. """Parsers for API reference information.""" + +from .base import ReferenceParserBase +from .factory import UnsupportedReferenceTypeError, parser_factory + +__all__ = "UnsupportedReferenceTypeError", "ReferenceParserBase", "parser_factory" diff --git a/asciidoxy/parser/base.py b/asciidoxy/parser/base.py new file mode 100644 index 00000000..d7396f2f --- /dev/null +++ b/asciidoxy/parser/base.py @@ -0,0 +1,45 @@ +# Copyright (C) 2019, TomTom (http://tomtom.com). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Factory methods for API reference information parsers.""" + +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Union + +from ..api_reference import ApiReference + + +class ReferenceParserBase(ABC): + """Abstract base class for reference parser. + + Attributes: + api_reference: API reference holder where parsed reference documentation is added. + """ + + api_reference: ApiReference + + def __init__(self, api_reference: ApiReference): + self.api_reference = api_reference + + @abstractmethod + def parse(self, reference_path: Union[Path, str]) -> bool: + """Parse reference documentation from the given path. + + Args: + reference_path File or directory containing the reference documentation. + + Returns: + True if the reference has been parsed. False if the reference path does not contain + valid content for this parser. + """ diff --git a/asciidoxy/parser/doxygen/__init__.py b/asciidoxy/parser/doxygen/__init__.py index 787a6cf4..4e2b6e06 100644 --- a/asciidoxy/parser/doxygen/__init__.py +++ b/asciidoxy/parser/doxygen/__init__.py @@ -13,6 +13,6 @@ # limitations under the License. """Parser for Doxygen XML output.""" -from .driver import Driver, safe_language_tag +from .parser import Parser, safe_language_tag -__all__ = ["Driver", "safe_language_tag"] +__all__ = ["Parser", "safe_language_tag"] diff --git a/asciidoxy/parser/doxygen/driver.py b/asciidoxy/parser/doxygen/driver.py deleted file mode 100644 index eb75f8d3..00000000 --- a/asciidoxy/parser/doxygen/driver.py +++ /dev/null @@ -1,209 +0,0 @@ -# Copyright (C) 2019, TomTom (http://tomtom.com). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Read API reference information from Doxygen XML output.""" - -import logging -import xml.etree.ElementTree as ET -from typing import List, Mapping, Optional, Set, Tuple - -from tqdm import tqdm - -from ...api_reference import AmbiguousLookupError, ApiReference -from ...model import Compound, ReferableElement, TypeRef -from .cpp import CppParser -from .driver_base import DriverBase -from .java import JavaParser -from .objc import ObjectiveCParser -from .parser_base import ParserBase -from .python import PythonParser - -logger = logging.getLogger(__name__) - - -class Driver(DriverBase): - """Driver for parsing Doxygen XML output.""" - api_reference: ApiReference - _unresolved_refs: List[TypeRef] - _unchecked_refs: List[TypeRef] - _inner_type_refs: List[Tuple[Compound, TypeRef]] - _force_language: Optional[str] - - _parsers: Mapping[str, ParserBase] - - def __init__(self, force_language: Optional[str] = None): - self.api_reference = ApiReference() - self._unresolved_refs = [] - self._unchecked_refs = [] - self._inner_type_refs = [] - self._force_language = safe_language_tag(force_language) - - self._parsers = { - CppParser.TRAITS.TAG: CppParser(self), - JavaParser.TRAITS.TAG: JavaParser(self), - ObjectiveCParser.TRAITS.TAG: ObjectiveCParser(self), - PythonParser.TRAITS.TAG: PythonParser(self), - } - - if not self._force_language: - self._force_language = None - elif self._force_language not in self._parsers: - logger.error(f"Unknown forced language: {self._force_language}. Falling back to auto" - " detection.") - self._force_language = None - - @property - def unresolved_ref_count(self): - return len(self._unresolved_refs) + len(self._inner_type_refs) - - @property - def unchecked_ref_count(self): - return len(self._unchecked_refs) - - def _parse_element(self, xml_element: ET.Element) -> None: - if self._force_language is not None: - language_tag = self._force_language - else: - language_tag = safe_language_tag(xml_element.get("language")) - if not language_tag: - return - if language_tag not in self._parsers: - logger.debug(f"Unknown language: {language_tag}") - return - - if xml_element.tag == "compounddef": - self._parsers[language_tag].parse_compounddef(xml_element) - else: - logger.debug(f"Unhandled element: {xml_element.tag}") - - def parse(self, file_or_path) -> bool: - """Parse all objects in an XML file and make them available for API reference generation. - - Params: - file_or_path: File object or path for the XML file to parse. - - Returns: - True if file is parsed. False if the file is invalid. - """ - - try: - tree = ET.parse(file_or_path) - except ET.ParseError: - logger.exception(f"Failure while parsing XML from `{file_or_path}`. The XML may be" - " malformed or the file has encoding errors.") - return False - - root = tree.getroot() - if root.tag != "doxygen": - if root.tag != "doxygenindex": - logger.error(f"File `{file_or_path}` does not contain valid Doxygen XML.") - return False - - for e in root: - self._parse_element(e) - return True - - def register(self, element: ReferableElement) -> None: - self.api_reference.append(element) - - def unchecked_ref(self, ref: TypeRef) -> None: - self._unchecked_refs.append(ref) - - def unresolved_ref(self, ref: TypeRef) -> None: - self._unresolved_refs.append(ref) - - def inner_type_ref(self, parent: Compound, ref: TypeRef) -> None: - self._inner_type_refs.append((parent, ref)) - - def resolve_references(self, progress: Optional[tqdm] = None) -> None: - """Resolve all references between objects from different XML files.""" - - unresolved_names: Set[str] = set() - if progress is not None: - progress.total = len(self._unresolved_refs) + len(self._inner_type_refs) - - still_unresolved_refs = [] - for ref in self._unresolved_refs: - if progress is not None: - progress.update() - assert ref.name - element = self.resolve_reference(ref) - if element is not None: - ref.resolve(element) - else: - still_unresolved_refs.append(ref) - unresolved_names.add(ref.name) - - still_unresolved_inner_type_refs: List[Tuple[Compound, TypeRef]] = [] - for parent, ref in self._inner_type_refs: - if progress is not None: - progress.update() - assert ref.name - element = self.resolve_reference(ref) - if element is not None: - assert isinstance(element, Compound) - if ref.prot: - element.prot = ref.prot - parent.members.append(element) - else: - still_unresolved_inner_type_refs.append((parent, ref)) - unresolved_names.add(ref.name) - - resolved_ref_count = len(self._unresolved_refs) - len(still_unresolved_refs) - resolved_inner_type_ref_count = (len(self._inner_type_refs) - - len(still_unresolved_inner_type_refs)) - unresolved_ref_count = len(still_unresolved_refs) + len(still_unresolved_inner_type_refs) - logger.debug(f"Resolved refs: {resolved_ref_count + resolved_inner_type_ref_count}") - logger.debug(f"Still unresolved: {unresolved_ref_count}: {', '.join(unresolved_names)}") - - self._unresolved_refs = still_unresolved_refs - self._inner_type_refs = still_unresolved_inner_type_refs - - def check_references(self, progress: Optional[tqdm] = None) -> None: - """Verify all references point to an existing element.""" - if progress is not None: - progress.total = len(self._unchecked_refs) - - for ref in self._unchecked_refs: - if progress is not None: - progress.update() - if self.resolve_reference(ref) is None: - logger.warning(f"Unknown reference id `{ref.id}`. Some XML files may be missing or" - " cannot be parsed.") - ref.id = None - self._unchecked_refs = [] - - def resolve_reference(self, ref: TypeRef) -> Optional[ReferableElement]: - try: - return self.api_reference.find(ref.name, - target_id=ref.id, - lang=ref.language, - namespace=ref.namespace) - except AmbiguousLookupError: - return None - - -def safe_language_tag(name: Optional[str]) -> str: - """Convert language names to tags that are safe to use for identifiers and file names. - - Args: - name: Name to convert to a safe name. Can be `None`. - - Returns: - A safe string to use for identifiers and file names. - """ - if name is None: - return "" - - name = name.lower() - return {"c++": "cpp", "objective-c": "objc"}.get(name, name) diff --git a/asciidoxy/parser/doxygen/driver_base.py b/asciidoxy/parser/doxygen/driver_base.py deleted file mode 100644 index a04dee5c..00000000 --- a/asciidoxy/parser/doxygen/driver_base.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (C) 2019, TomTom (http://tomtom.com). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Base classes for parser drivers.""" - -from abc import ABC, abstractmethod - -from ...model import Compound, ReferableElement, TypeRef - - -class DriverBase(ABC): - """Base class for drivers.""" - @abstractmethod - def register(self, element: ReferableElement) -> None: - """Register a new element.""" - pass - - @abstractmethod - def unchecked_ref(self, ref: TypeRef) -> None: - """Register an unchecked reference. - - Unchecked references have an associated id, but existence of the id must still be verified. - """ - pass - - @abstractmethod - def unresolved_ref(self, ref: TypeRef) -> None: - """Register an unresolved reference.""" - pass - - @abstractmethod - def inner_type_ref(self, parent: Compound, ref: TypeRef) -> None: - """Register an inner type reference.""" - pass diff --git a/asciidoxy/parser/doxygen/language/__init__.py b/asciidoxy/parser/doxygen/language/__init__.py new file mode 100644 index 00000000..00d6a661 --- /dev/null +++ b/asciidoxy/parser/doxygen/language/__init__.py @@ -0,0 +1,14 @@ +# Copyright (C) 2019, TomTom (http://tomtom.com). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Supported programming languages for the Doxygen parser.""" diff --git a/asciidoxy/parser/doxygen/cpp.py b/asciidoxy/parser/doxygen/language/cpp.py similarity index 95% rename from asciidoxy/parser/doxygen/cpp.py rename to asciidoxy/parser/doxygen/language/cpp.py index c012b28c..155588f7 100644 --- a/asciidoxy/parser/doxygen/cpp.py +++ b/asciidoxy/parser/doxygen/language/cpp.py @@ -18,10 +18,10 @@ import xml.etree.ElementTree as ET from typing import List, Optional -from ...model import Compound, Parameter -from .language_traits import LanguageTraits, TokenCategory -from .parser_base import ParserBase -from .type_parser import Token, TypeParser +from ....model import Compound, Parameter +from ..language_parser import LanguageParser +from ..language_traits import LanguageTraits, TokenCategory +from ..type_parser import Token, TypeParser class CppTraits(LanguageTraits): @@ -139,7 +139,7 @@ def fix_function_typedefs(tokens: List[Token]) -> List[Token]: return tokens -class CppParser(ParserBase): +class CppParser(LanguageParser): """Parser for C++ documentation.""" TRAITS = CppTraits TYPE_PARSER = CppTypeParser @@ -184,7 +184,8 @@ def _fix_function_typedef(self, member: Optional[Compound], tokens.pop(0) if type_tokens: - ref = self.TYPE_PARSER.type_from_tokens(type_tokens, self._driver, member.full_name) + ref = self.TYPE_PARSER.type_from_tokens(type_tokens, self._api_reference, + member.full_name) if ref is not None: member.params.append(Parameter(type=ref)) diff --git a/asciidoxy/parser/doxygen/java.py b/asciidoxy/parser/doxygen/language/java.py similarity index 96% rename from asciidoxy/parser/doxygen/java.py rename to asciidoxy/parser/doxygen/language/java.py index 3a8e8cf9..75cbfb71 100644 --- a/asciidoxy/parser/doxygen/java.py +++ b/asciidoxy/parser/doxygen/language/java.py @@ -16,9 +16,9 @@ import string from typing import List, Optional -from .language_traits import LanguageTraits, TokenCategory -from .parser_base import ParserBase -from .type_parser import Token, TypeParser, find_tokens +from ..language_parser import LanguageParser +from ..language_traits import LanguageTraits, TokenCategory +from ..type_parser import Token, TypeParser, find_tokens class JavaTraits(LanguageTraits): @@ -138,7 +138,7 @@ def detect_annotations(tokens: List[Token]) -> List[Token]: return tokens -class JavaParser(ParserBase): +class JavaParser(LanguageParser): """Parser for Java documentation.""" TRAITS = JavaTraits TYPE_PARSER = JavaTypeParser diff --git a/asciidoxy/parser/doxygen/objc.py b/asciidoxy/parser/doxygen/language/objc.py similarity index 97% rename from asciidoxy/parser/doxygen/objc.py rename to asciidoxy/parser/doxygen/language/objc.py index c4ebfadf..98f86d4f 100644 --- a/asciidoxy/parser/doxygen/objc.py +++ b/asciidoxy/parser/doxygen/language/objc.py @@ -17,10 +17,10 @@ import xml.etree.ElementTree as ET from typing import List, Optional, Tuple -from ...model import Compound -from .language_traits import LanguageTraits, TokenCategory -from .parser_base import ParserBase -from .type_parser import Token, TypeParser, find_tokens +from ....model import Compound +from ..language_parser import LanguageParser +from ..language_traits import LanguageTraits, TokenCategory +from ..type_parser import Token, TypeParser, find_tokens class ObjectiveCTraits(LanguageTraits): @@ -140,7 +140,7 @@ def remove_block_prefix(tokens: List[Token]) -> List[Token]: return tokens -class ObjectiveCParser(ParserBase): +class ObjectiveCParser(LanguageParser): """Parser for Objective C documentation.""" TRAITS = ObjectiveCTraits TYPE_PARSER = ObjectiveCTypeParser diff --git a/asciidoxy/parser/doxygen/python.py b/asciidoxy/parser/doxygen/language/python.py similarity index 93% rename from asciidoxy/parser/doxygen/python.py rename to asciidoxy/parser/doxygen/language/python.py index 1f90d2d7..14bc939e 100644 --- a/asciidoxy/parser/doxygen/python.py +++ b/asciidoxy/parser/doxygen/language/python.py @@ -16,9 +16,9 @@ import string from typing import List, Optional -from .language_traits import LanguageTraits, TokenCategory -from .parser_base import ParserBase -from .type_parser import Token, TypeParser +from ..language_parser import LanguageParser +from ..language_traits import LanguageTraits, TokenCategory +from ..type_parser import Token, TypeParser class PythonTraits(LanguageTraits): @@ -77,7 +77,7 @@ def adapt_tokens(cls, return tokens -class PythonParser(ParserBase): +class PythonParser(LanguageParser): """Parser for python documentation.""" TRAITS = PythonTraits TYPE_PARSER = PythonTypeParser diff --git a/asciidoxy/parser/doxygen/parser_base.py b/asciidoxy/parser/doxygen/language_parser.py similarity index 95% rename from asciidoxy/parser/doxygen/parser_base.py rename to asciidoxy/parser/doxygen/language_parser.py index d5194951..acec883b 100644 --- a/asciidoxy/parser/doxygen/parser_base.py +++ b/asciidoxy/parser/doxygen/language_parser.py @@ -18,6 +18,7 @@ from abc import ABC from typing import Dict, List, Optional, Type +from ...api_reference import ApiReference from ...model import Compound, Parameter, ReturnValue, ThrowsClause, TypeRef from .description_parser import ( Admonition, @@ -29,7 +30,6 @@ parse_description, select_descriptions, ) -from .driver_base import DriverBase from .language_traits import LanguageTraits from .type_parser import TypeParseError, TypeParser @@ -83,7 +83,7 @@ def _find_parameter_documentation(descriptions: ParameterList, return None -class ParserBase(ABC): +class LanguageParser(ABC): """Base functionality for language parsers. The parser is mostly anemic by design: there is no internal state that changes during parsing. @@ -95,10 +95,10 @@ class ParserBase(ABC): TRAITS: Type[LanguageTraits] TYPE_PARSER: Type[TypeParser] - _driver: DriverBase + _api_reference: ApiReference - def __init__(self, driver: DriverBase): - self._driver = driver + def __init__(self, api_reference: ApiReference): + self._api_reference = api_reference def parse_parameters(self, memberdef_element: ET.Element, parent: Compound, descriptions: Optional[ParameterList], @@ -148,7 +148,7 @@ def parse_type(self, try: return self.TYPE_PARSER.parse_xml(type_element, array_element, - driver=self._driver, + api_reference=self._api_reference, namespace=namespace) except TypeParseError: logger.exception( @@ -183,9 +183,9 @@ def parse_exceptions(self, memberdef_element: ET.Element, parent: Compound, exceptions.append(exception) if exception.type.id: - self._driver.unchecked_ref(exception.type) + self._api_reference.add_unchecked_reference(exception.type) else: - self._driver.unresolved_ref(exception.type) + self._api_reference.add_unresolved_reference(exception.type) return exceptions def parse_returns(self, memberdef_element: ET.Element, parent: Compound, @@ -218,7 +218,7 @@ def parse_enumvalue(self, enumvalue_element: ET.Element, parent_name: str) -> Co parse_description(enumvalue_element.find("briefdescription"), self.TRAITS.TAG), parse_description(enumvalue_element.find("detaileddescription"), self.TRAITS.TAG)) - self._driver.register(enumvalue) + self._api_reference.append(enumvalue) return enumvalue def parse_member(self, memberdef_element: ET.Element, parent: Compound) -> Optional[Compound]: @@ -257,7 +257,7 @@ def parse_member(self, memberdef_element: ET.Element, parent: Compound) -> Optio member.const = _yes_no_to_bool(memberdef_element.get("const", "no")) member.constexpr = _yes_no_to_bool(memberdef_element.get("constexpr", "no")) - self._driver.register(member) + self._api_reference.append(member) return member def parse_innerclass(self, parent: Compound, innerclass_element: ET.Element) -> None: @@ -268,7 +268,7 @@ def parse_innerclass(self, parent: Compound, innerclass_element: ET.Element) -> inner_type.namespace = parent.full_name inner_type.prot = innerclass_element.get("prot", "") - self._driver.inner_type_ref(parent, inner_type) + self._api_reference.add_inner_type_reference(parent, inner_type) def parse_compounddef(self, compounddef_element: ET.Element) -> None: compound = Compound(self.TRAITS.TAG) @@ -303,7 +303,7 @@ def parse_compounddef(self, compounddef_element: ET.Element) -> None: for innerclass_element in compounddef_element.iterfind("innerclass"): self.parse_innerclass(compound, innerclass_element) - self._driver.register(compound) + self._api_reference.append(compound) def find_include(self, element: ET.Element) -> Optional[str]: include = element.findtext("includes") diff --git a/asciidoxy/parser/doxygen/parser.py b/asciidoxy/parser/doxygen/parser.py new file mode 100644 index 00000000..df0b3208 --- /dev/null +++ b/asciidoxy/parser/doxygen/parser.py @@ -0,0 +1,127 @@ +# Copyright (C) 2019, TomTom (http://tomtom.com). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Read API reference information from Doxygen XML output.""" + +import logging +import xml.etree.ElementTree as ET +from pathlib import Path +from typing import Mapping, Optional, Union + +from ...api_reference import ApiReference +from ..factory import ReferenceParserBase +from .language.cpp import CppParser +from .language.java import JavaParser +from .language.objc import ObjectiveCParser +from .language.python import PythonParser +from .language_parser import LanguageParser + +logger = logging.getLogger(__name__) + + +class Parser(ReferenceParserBase): + """Parser Doxygen XML output.""" + api_reference: ApiReference + _force_language: Optional[str] + + _parsers: Mapping[str, LanguageParser] + + def __init__(self, api_reference: ApiReference, force_language: Optional[str] = None): + ReferenceParserBase.__init__(self, api_reference) + + self._force_language = safe_language_tag(force_language) + + self._parsers = { + CppParser.TRAITS.TAG: CppParser(api_reference), + JavaParser.TRAITS.TAG: JavaParser(api_reference), + ObjectiveCParser.TRAITS.TAG: ObjectiveCParser(api_reference), + PythonParser.TRAITS.TAG: PythonParser(api_reference), + } + + if not self._force_language: + self._force_language = None + elif self._force_language not in self._parsers: + logger.error(f"Unknown forced language: {self._force_language}. Falling back to auto" + " detection.") + self._force_language = None + + def _parse_element(self, xml_element: ET.Element) -> None: + if self._force_language is not None: + language_tag = self._force_language + else: + language_tag = safe_language_tag(xml_element.get("language")) + if not language_tag: + return + if language_tag not in self._parsers: + logger.debug(f"Unknown language: {language_tag}") + return + + if xml_element.tag == "compounddef": + self._parsers[language_tag].parse_compounddef(xml_element) + else: + logger.debug(f"Unhandled element: {xml_element.tag}") + + def parse(self, reference_path: Union[Path, str]) -> bool: + """Parse reference documentation from the given path. + + Args: + reference_path File or directory containing the reference documentation. + + Returns: + True if the reference has been parsed. False if the reference path does not contain + valid content for this parser. + """ + reference_path = Path(reference_path) + if reference_path.is_file(): + return self._parse_file(reference_path) + else: + for xml_file in reference_path.glob("**/*.xml"): + if xml_file.stem in ("index", "Doxyfile"): + continue + if not self._parse_file(xml_file): + return False + return True + + def _parse_file(self, file: Path) -> bool: + try: + tree = ET.parse(file) + except ET.ParseError: + logger.exception(f"Failure while parsing XML from `{file}`. The XML may be" + " malformed or the file has encoding errors.") + return False + + root = tree.getroot() + if root.tag != "doxygen": + if root.tag != "doxygenindex": + logger.error(f"File `{file}` does not contain valid Doxygen XML.") + return False + + for e in root: + self._parse_element(e) + return True + + +def safe_language_tag(name: Optional[str]) -> str: + """Convert language names to tags that are safe to use for identifiers and file names. + + Args: + name: Name to convert to a safe name. Can be `None`. + + Returns: + A safe string to use for identifiers and file names. + """ + if name is None: + return "" + + name = name.lower() + return {"c++": "cpp", "objective-c": "objc"}.get(name, name) diff --git a/asciidoxy/parser/doxygen/type_parser.py b/asciidoxy/parser/doxygen/type_parser.py index d31fe5e6..7144ef75 100644 --- a/asciidoxy/parser/doxygen/type_parser.py +++ b/asciidoxy/parser/doxygen/type_parser.py @@ -17,8 +17,8 @@ import xml.etree.ElementTree as ET from typing import Iterator, List, Optional, Sequence, Tuple, Type +from ...api_reference import ApiReference from ...model import Parameter, TypeRef -from .driver_base import DriverBase from .language_traits import LanguageTraits, TokenCategory logger = logging.getLogger(__name__) @@ -88,7 +88,7 @@ class TypeParser: def parse_xml(cls, type_element: ET.Element, array_element: Optional[ET.Element] = None, - driver: Optional[DriverBase] = None, + api_reference: Optional[ApiReference] = None, namespace: Optional[str] = None) -> Optional[TypeRef]: """Parse a type from an XML element. @@ -97,7 +97,7 @@ def parse_xml(cls, Arguments: type_element: The `` element from Doxygen. array_element: The `` element from Doxygen, if available. - driver: Driver to register types without refids with. + api_reference: API reference to register types without refids with. namespace: Namespace containing the type. Returns: A `TypeRef` for the type, or None if there is no type information. @@ -107,7 +107,7 @@ def parse_xml(cls, tokens = cls.adapt_tokens(tokens, array_tokens) if len(tokens) == 0 or all(token.category == TokenCategory.WHITESPACE for token in tokens): return None - return cls.type_from_tokens(tokens, driver, namespace) + return cls.type_from_tokens(tokens, api_reference, namespace) @classmethod def adapt_tokens(cls, @@ -231,7 +231,7 @@ def tokenize_xml(cls, element: ET.Element) -> List[Token]: @classmethod def type_from_tokens(cls, tokens: List[Token], - driver: Optional[DriverBase] = None, + api_reference: Optional[ApiReference] = None, namespace: Optional[str] = None) -> Optional[TypeRef]: """Create a `TypeRef` from a sequence of tokens. @@ -260,7 +260,7 @@ def log_parse_warning(): nested_types: Optional[List[TypeRef]] = [] try: - nested_types, tokens = cls.nested_types(tokens, driver, namespace) + nested_types, tokens = cls.nested_types(tokens, api_reference, namespace) except TypeParseError as e: log_parse_warning() logger.debug(f"Failed to parse nested types: {e}") @@ -271,7 +271,7 @@ def log_parse_warning(): arg_types: Optional[List[Parameter]] = [] try: - arg_types, tokens = cls.arg_types(tokens, driver, namespace) + arg_types, tokens = cls.arg_types(tokens, api_reference, namespace) except TypeParseError as e: log_parse_warning() logger.debug(f"Failed to parse args: {e}") @@ -311,19 +311,19 @@ def log_parse_warning(): type_ref.args = arg_types type_ref.namespace = namespace - if driver is not None: + if api_reference is not None: if type_ref.id: - driver.unchecked_ref(type_ref) + api_reference.add_unchecked_reference(type_ref) elif (type_ref.name and not type_ref.id and not cls.TRAITS.is_language_standard_type(type_ref.name)): - driver.unresolved_ref(type_ref) + api_reference.add_unresolved_reference(type_ref) if type_ref.returns is not None: if type_ref.returns.id: - driver.unchecked_ref(type_ref.returns) + api_reference.add_unchecked_reference(type_ref.returns) elif (type_ref.returns.name and not type_ref.returns.id and not cls.TRAITS.is_language_standard_type(type_ref.returns.name)): - driver.unresolved_ref(type_ref.returns) + api_reference.add_unresolved_reference(type_ref.returns) return type_ref @@ -372,7 +372,7 @@ def _remove_whitespace(tokens, location) -> List[Token]: def nested_types( cls, tokens: List[Token], - driver: Optional[DriverBase] = None, + api_reference: Optional[ApiReference] = None, namespace: Optional[str] = None) -> Tuple[Optional[List[TypeRef]], List[Token]]: """Parse nested types from tokens if present. @@ -386,13 +386,13 @@ def nested_types( TokenCategory.NESTED_SEPARATOR) if nested_type_tokens is None: return None, tokens - types = (cls.type_from_tokens(ntt, driver, namespace) for ntt in nested_type_tokens) + types = (cls.type_from_tokens(ntt, api_reference, namespace) for ntt in nested_type_tokens) return [t for t in types if t is not None], tokens @classmethod def arg_types(cls, tokens: List[Token], - driver: Optional[DriverBase] = None, + api_reference: Optional[ApiReference] = None, namespace: Optional[str] = None) -> Tuple[Optional[List[Parameter]], List[Token]]: """Parse function argument types from tokens if present. @@ -407,13 +407,13 @@ def arg_types(cls, if nested_type_tokens is None: return None, tokens - args = (cls.arg_from_tokens(ntt, driver, namespace) for ntt in nested_type_tokens) + args = (cls.arg_from_tokens(ntt, api_reference, namespace) for ntt in nested_type_tokens) return [a for a in args if a is not None], tokens @classmethod def arg_from_tokens(cls, tokens: List[Token], - driver: Optional[DriverBase] = None, + api_reference: Optional[ApiReference] = None, namespace: Optional[str] = None) -> Optional[Parameter]: """Parse an argument definition from a sequence of tokens. @@ -428,7 +428,7 @@ def arg_from_tokens(cls, name_tokens.insert(0, tokens.pop(-1)) arg = Parameter() - arg.type = cls.type_from_tokens(tokens, driver, namespace) + arg.type = cls.type_from_tokens(tokens, api_reference, namespace) arg.name = "".join(t.text for t in name_tokens) return arg diff --git a/asciidoxy/parser/factory.py b/asciidoxy/parser/factory.py new file mode 100644 index 00000000..e6d0754c --- /dev/null +++ b/asciidoxy/parser/factory.py @@ -0,0 +1,43 @@ +# Copyright (C) 2019, TomTom (http://tomtom.com). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Factory methods for API reference information parsers.""" + +from ..api_reference import ApiReference +from ..config import Configuration +from .base import ReferenceParserBase +from .doxygen import Parser as DoxygenParser + + +class UnsupportedReferenceTypeError(Exception): + """The requested type of reference is not supported (yet) and cannot be parsed. + + Attributes: + reference_type: The requested type of reference. + """ + reference_type: str + + def __init__(self, reference_type: str): + self.reference_type = reference_type + + def __str__(self) -> str: + return f"Reference of type {self.reference_type} is not supported." + + +def parser_factory(reference_type: str, api_reference: ApiReference, + config: Configuration) -> ReferenceParserBase: + """Create a parser for the given type of API reference documentation.""" + if reference_type == "doxygen": + return DoxygenParser(api_reference, force_language=config.force_language) + else: + raise UnsupportedReferenceTypeError(reference_type) diff --git a/documentation/Makefile b/documentation/Makefile index d0644558..12a9b661 100644 --- a/documentation/Makefile +++ b/documentation/Makefile @@ -18,6 +18,7 @@ export DOC_BUILD_DIR := $(BUILD_DIR)/doc export DOXYGEN_BUILD_DIR := $(DOC_BUILD_DIR)/doxygen MULTIPAGE ?= --multipage +LOG_LEVEL ?= WARNING docs: $(DOC_BUILD_DIR)/output/index.html @@ -69,4 +70,5 @@ $(ADOC_OUT_FILES): $(ADOC_IN_FILES) $(DOC_BUILD_DIR)/asciidoxy $(CURDIR)/asciido --destination-dir $(DOC_BUILD_DIR)/output \ --spec-file asciidoxy.toml \ --debug $(MULTIPAGE) \ + --log $(LOG_LEVEL) \ -a linkcss diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 1e15b658..735d2276 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -24,7 +24,7 @@ from asciidoxy.generator.context import Context from asciidoxy.model import Compound, Parameter, ReturnValue, ThrowsClause, TypeRef from asciidoxy.packaging import PackageManager -from asciidoxy.parser.doxygen import Driver as ParserDriver +from asciidoxy.parser import parser_factory from .builders import SimpleClassBuilder from .file_builder import FileBuilder @@ -54,20 +54,6 @@ def xml_data(doxygen_version): return Path(__file__).parent.parent / "data" / "generated" / "xml" / doxygen_version -@pytest.fixture -def parser_driver_factory(xml_data): - def factory(*test_dirs, force_language=None): - parser_driver = ParserDriver(force_language=force_language) - - for test_dir in test_dirs: - for xml_file in (xml_data / test_dir).glob("**/*.xml"): - parser_driver.parse(xml_file) - - return parser_driver - - return factory - - @pytest.fixture def adoc_data(): return test_data_dir / "adoc" @@ -144,10 +130,27 @@ def forced_language(): return None +# TODO: rename and rework +@pytest.fixture +def parser_driver_factory(xml_data): + def factory(*test_dirs, force_language=None): + config = Configuration() + config.force_language = force_language + parser = parser_factory("doxygen", ApiReference(), config) + + for test_dir in test_dirs: + parser.parse(xml_data / test_dir) + + return parser + + return factory + + +# TODO: rework @pytest.fixture def api_reference(parser_driver_factory, api_reference_set, forced_language): driver = parser_driver_factory(*api_reference_set, force_language=forced_language) - driver.resolve_references() + driver.api_reference.resolve_references() return driver.api_reference diff --git a/tests/unit/packaging/test_manager.py b/tests/unit/packaging/test_manager.py index 8969d2cf..3346bb2e 100644 --- a/tests/unit/packaging/test_manager.py +++ b/tests/unit/packaging/test_manager.py @@ -14,11 +14,13 @@ """Tests for managing packages.""" from pathlib import Path -from unittest.mock import MagicMock, call +from unittest.mock import MagicMock, call, patch import pytest import toml +from asciidoxy.api_reference import ApiReference +from asciidoxy.config import Configuration from asciidoxy.document import Package from asciidoxy.packaging.manager import ( FileCollisionError, @@ -143,17 +145,23 @@ def test_collect(package_manager, tmp_path, build_dir): assert pkg_b.python_dir.is_dir() -def test_load_reference(package_manager, tmp_path, build_dir): +@pytest.fixture +def parser_mock(): + with patch("asciidoxy.packaging.manager.parser_factory") as mock: + parser_mock = MagicMock() + mock.return_value = parser_mock + yield parser_mock + + +def test_load_reference(package_manager, tmp_path, parser_mock): pkg_a_dir = create_package_dir(tmp_path, "a") pkg_b_dir = create_package_dir(tmp_path, "b") spec_file = create_package_spec(tmp_path, "a", "b") package_manager.collect(spec_file) - parser_mock = MagicMock() - package_manager.load_reference(parser_mock) + package_manager.load_reference(ApiReference(), Configuration()) parser_mock.parse.assert_has_calls( - [call(pkg_a_dir / "xml" / "a.xml"), - call(pkg_b_dir / "xml" / "b.xml")], any_order=True) + [call(pkg_a_dir / "xml"), call(pkg_b_dir / "xml")], any_order=True) def test_prepare_work_directory(package_manager, tmp_path, build_dir): diff --git a/tests/unit/parser/doxygen/test_objc.py b/tests/unit/parser/doxygen/test_objc.py index 2524f268..e02ce7af 100644 --- a/tests/unit/parser/doxygen/test_objc.py +++ b/tests/unit/parser/doxygen/test_objc.py @@ -18,7 +18,7 @@ import pytest from pytest import param -from asciidoxy.parser.doxygen.objc import ObjectiveCTraits +from asciidoxy.parser.doxygen.language.objc import ObjectiveCTraits from tests.unit.matchers import ( IsEmpty, IsFalse, diff --git a/tests/unit/parser/doxygen/test_parser.py b/tests/unit/parser/doxygen/test_parser.py index 5c52f26a..a68b01c8 100644 --- a/tests/unit/parser/doxygen/test_parser.py +++ b/tests/unit/parser/doxygen/test_parser.py @@ -13,289 +13,6 @@ # limitations under the License. """Generic tests for parsing Doxygen XML files.""" -from asciidoxy.parser.doxygen import Driver as ParserDriver -from tests.unit.shared import ProgressMock - - -def test_resolve_references_for_return_types(parser_driver_factory): - parser = parser_driver_factory("cpp/default", "cpp/consumer") - - member = parser.api_reference.find("asciidoxy::positioning::Positioning::CurrentPosition", - kind="function", - lang="cpp") - assert member is not None - assert member.returns - assert member.returns.type - assert not member.returns.type.id - - parser.resolve_references() - - assert member.returns - assert member.returns.type - assert member.returns.type.id == "cpp-classasciidoxy_1_1geometry_1_1_coordinate" - assert member.returns.type.kind == "class" - - -def test_resolve_partial_references_for_return_types(parser_driver_factory): - parser = parser_driver_factory("cpp/default", "cpp/consumer") - - member = parser.api_reference.find("asciidoxy::positioning::Positioning::TrafficNearby", - kind="function", - lang="cpp") - assert member is not None - assert member.returns - assert member.returns.type - assert len(member.returns.type.nested) == 1 - assert not member.returns.type.nested[0].id - - parser.resolve_references() - - assert member.returns - assert member.returns.type - assert len(member.returns.type.nested) == 1 - assert member.returns.type.nested[0].id == "cpp-classasciidoxy_1_1traffic_1_1_traffic_event" - assert member.returns.type.nested[0].kind == "class" - - -def test_resolve_references_for_parameters(parser_driver_factory): - parser = parser_driver_factory("cpp/default", "cpp/consumer") - - member = parser.api_reference.find("asciidoxy::positioning::Positioning::IsNearby", - kind="function", - lang="cpp") - assert member is not None - assert len(member.params) == 1 - assert member.params[0].type - assert not member.params[0].type.id - - parser.resolve_references() - - assert len(member.params) == 1 - assert member.params[0].type - assert member.params[0].type.id == "cpp-classasciidoxy_1_1geometry_1_1_coordinate" - assert member.params[0].type.kind == "class" - - -def test_resolve_partial_references_for_parameters(parser_driver_factory): - parser = parser_driver_factory("cpp/default", "cpp/consumer") - - member = parser.api_reference.find("asciidoxy::positioning::Positioning::InTraffic", - kind="function", - lang="cpp") - assert member is not None - assert len(member.params) == 1 - assert member.params[0].type - assert not member.params[0].type.id - - parser.resolve_references() - - assert len(member.params) == 1 - assert member.params[0].type - assert member.params[0].type.id == "cpp-classasciidoxy_1_1traffic_1_1_traffic_event" - assert member.params[0].type.kind == "class" - - -def test_resolve_references_for_typedefs(parser_driver_factory): - parser = parser_driver_factory("cpp/default", "cpp/consumer") - - member = parser.api_reference.find("asciidoxy::positioning::Traffic", kind="alias") - assert member is not None - assert member.returns - assert member.returns.type - assert not member.returns.type.id - - parser.resolve_references() - - assert member.returns - assert member.returns.type - assert member.returns.type.id == "cpp-classasciidoxy_1_1traffic_1_1_traffic_event" - assert member.returns.type.kind == "class" - - -def test_resolve_references_for_inner_type_reference(parser_driver_factory): - parser = parser_driver_factory("cpp/default") - - parent_class = parser.api_reference.find("asciidoxy::traffic::TrafficEvent", - kind="class", - lang="cpp") - assert parent_class is not None - inner_classes = [m for m in parent_class.members if m.kind == "struct"] - assert len(inner_classes) == 0 - - parser.resolve_references() - - inner_classes = [m for m in parent_class.members if m.kind == "struct"] - assert len(inner_classes) > 0 - - nested_class = inner_classes[0] - assert nested_class.full_name == "asciidoxy::traffic::TrafficEvent::TrafficEventData" - - -def test_resolve_references_for_exceptions(parser_driver_factory): - parser = parser_driver_factory("cpp/default", "cpp/consumer") - - member = parser.api_reference.find("asciidoxy::positioning::Positioning::Override", - kind="function") - assert member is not None - assert len(member.exceptions) == 1 - assert member.exceptions[0].type - assert not member.exceptions[0].type.id - - parser.resolve_references() - - assert member.returns - assert len(member.exceptions) == 1 - assert member.exceptions[0].type - assert member.exceptions[0].type.id == "cpp-classasciidoxy_1_1geometry_1_1_invalid_coordinate" - assert member.exceptions[0].type.kind == "class" - - -def test_resolve_partial_references_for_exceptions(parser_driver_factory): - parser = parser_driver_factory("cpp/default", "cpp/consumer") - - member = parser.api_reference.find("asciidoxy::positioning::Positioning::TrafficNearby", - kind="function") - assert member is not None - assert len(member.exceptions) == 1 - assert member.exceptions[0].type - assert not member.exceptions[0].type.id - - parser.resolve_references() - - assert member.returns - assert len(member.exceptions) == 1 - assert member.exceptions[0].type - assert member.exceptions[0].type.id == "cpp-classasciidoxy_1_1geometry_1_1_invalid_coordinate" - assert member.exceptions[0].type.kind == "class" - - -def test_resolve_references_prefer_same_namespace(parser_driver_factory): - parser = parser_driver_factory("cpp/default", "cpp/consumer") - - member_a_t = parser.api_reference.find("asciidoxy::traffic::CreateConvertor", - kind="function", - lang="cpp") - assert member_a_t is not None - assert member_a_t.returns - assert member_a_t.returns.type - assert not member_a_t.returns.type.id - assert not member_a_t.returns.type.kind - - member_a = parser.api_reference.find("asciidoxy::CreateConvertor", kind="function", lang="cpp") - assert member_a is not None - assert member_a.returns - assert member_a.returns.type - assert not member_a.returns.type.id - assert not member_a.returns.type.kind - - member_a_g = parser.api_reference.find("asciidoxy::geometry::CreateConvertor", - kind="function", - lang="cpp") - assert member_a_g is not None - assert member_a_g.returns - assert member_a_g.returns.type - assert not member_a_g.returns.type.id - assert not member_a_g.returns.type.kind - - parser.resolve_references() - - assert member_a_t.returns - assert member_a_t.returns.type - assert member_a_t.returns.type.id == "cpp-classasciidoxy_1_1traffic_1_1_convertor" - assert member_a_t.returns.type.kind == "class" - assert member_a_t.returns.type.namespace == "asciidoxy::traffic" - - assert member_a.returns - assert member_a.returns.type - assert member_a.returns.type.id == "cpp-classasciidoxy_1_1_convertor" - assert member_a.returns.type.kind == "class" - assert member_a.returns.type.namespace == "asciidoxy" - - assert member_a_g.returns - assert member_a_g.returns.type - assert member_a_g.returns.type.id == "cpp-classasciidoxy_1_1geometry_1_1_convertor" - assert member_a_g.returns.type.kind == "class" - assert member_a_g.returns.type.namespace == "asciidoxy::geometry" - - -def test_resolve_references_fails_when_ambiguous(parser_driver_factory): - # When internal resolving fails, the type will not be resolved to the exact type, and will be - # shown in the documentation as plain text. It should not raise an error. - parser = parser_driver_factory("cpp/default", "cpp/consumer") - - member = parser.api_reference.find("asciidoxy::traffic::geometry::CreateConvertor", - kind="function", - lang="cpp") - assert member is not None - assert member.returns - assert member.returns.type - assert not member.returns.type.id - assert not member.returns.type.kind - - parser.resolve_references() - - assert member.returns - assert member.returns.type - assert not member.returns.type.id - assert not member.returns.type.kind - - -def test_resolve_references__report_progress(parser_driver_factory): - parser = parser_driver_factory("cpp/default", "cpp/consumer") - unresolved_ref_count = parser.unresolved_ref_count - assert unresolved_ref_count > 70 - - progress_mock = ProgressMock() - parser.resolve_references(progress=progress_mock) - - assert progress_mock.ready == progress_mock.total - assert progress_mock.total == unresolved_ref_count - - -def test_check_references__reference_found(parser_driver_factory): - parser = parser_driver_factory("cpp/default") - - element = parser.api_reference.find("asciidoxy::geometry::Print") - assert element is not None - assert len(element.params) == 1 - assert element.params[0].type - assert element.params[0].type.id - - assert parser.unchecked_ref_count > 0 - parser.check_references() - assert parser.unchecked_ref_count == 0 - - assert element.params[0].type.id - - -def test_check_references__remove_missing_refids(xml_data): - parser = ParserDriver() - parser.parse(xml_data / "cpp/default/xml/namespaceasciidoxy_1_1geometry.xml") - - element = parser.api_reference.find("asciidoxy::geometry::Print") - assert element is not None - assert len(element.params) == 1 - assert element.params[0].type - assert element.params[0].type.id - - assert parser.unchecked_ref_count > 0 - parser.check_references() - assert parser.unchecked_ref_count == 0 - - assert not element.params[0].type.id - - -def test_check_references__report_progress(parser_driver_factory): - parser = parser_driver_factory("cpp/default", "cpp/consumer") - unchecked_ref_count = parser.unchecked_ref_count - assert unchecked_ref_count > 0 - - progress_mock = ProgressMock() - parser.check_references(progress=progress_mock) - - assert progress_mock.ready == progress_mock.total - assert progress_mock.total == unchecked_ref_count - def test_force_language_java(parser_driver_factory): parser = parser_driver_factory("cpp/default", force_language="java") diff --git a/tests/unit/parser/doxygen/typeparser/conftest.py b/tests/unit/parser/doxygen/typeparser/conftest.py index 32982cb2..ab67492b 100644 --- a/tests/unit/parser/doxygen/typeparser/conftest.py +++ b/tests/unit/parser/doxygen/typeparser/conftest.py @@ -18,12 +18,16 @@ import pytest -class DriverMock(MagicMock): +class ReferenceMock(MagicMock): def assert_unresolved(self, *names): - assert (sorted([args[0].name - for args, _ in self.unresolved_ref.call_args_list]) == sorted(names)) + assert (sorted([args[0].name for args, _ in self.add_unresolved_reference.call_args_list + ]) == sorted(names)) + + def assert_unchecked(self, *ids): + assert (sorted([args[0].id + for args, _ in self.add_unchecked_reference.call_args_list]) == sorted(ids)) @pytest.fixture -def driver_mock(): - return DriverMock() +def reference_mock(): + return ReferenceMock() diff --git a/tests/unit/parser/doxygen/typeparser/test_cpp.py b/tests/unit/parser/doxygen/typeparser/test_cpp.py index 49e0bc3a..298d2500 100644 --- a/tests/unit/parser/doxygen/typeparser/test_cpp.py +++ b/tests/unit/parser/doxygen/typeparser/test_cpp.py @@ -17,7 +17,7 @@ import pytest -from asciidoxy.parser.doxygen.cpp import CppTypeParser +from asciidoxy.parser.doxygen.language.cpp import CppTypeParser from asciidoxy.parser.doxygen.language_traits import TokenCategory from asciidoxy.parser.doxygen.type_parser import Token, TypeParseError from tests.unit.matchers import IsEmpty, IsNone, m_parameter, m_typeref @@ -39,7 +39,7 @@ def cpp_type_suffix(request): return request.param -def test_parse_cpp_type_from_text_simple(driver_mock, cpp_type_prefix, cpp_type_suffix): +def test_parse_cpp_type_from_text_simple(reference_mock, cpp_type_prefix, cpp_type_suffix): type_element = ET.Element("type") type_element.text = f"{cpp_type_prefix}double{cpp_type_suffix}" @@ -51,11 +51,11 @@ def test_parse_cpp_type_from_text_simple(driver_mock, cpp_type_prefix, cpp_type_ prefix=cpp_type_prefix, suffix=cpp_type_suffix, nested=IsEmpty(), - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() # built-in type + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() # built-in type -def test_parse_cpp_type_from_text_nested_with_prefix_and_suffix(driver_mock, cpp_type_prefix, +def test_parse_cpp_type_from_text_nested_with_prefix_and_suffix(reference_mock, cpp_type_prefix, cpp_type_suffix): type_element = ET.Element("type") type_element.text = (f"{cpp_type_prefix}Coordinate< {cpp_type_prefix}Unit{cpp_type_suffix} " @@ -79,11 +79,11 @@ def test_parse_cpp_type_from_text_nested_with_prefix_and_suffix(driver_mock, cpp nested=IsEmpty(), ), ], - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("Coordinate", "Unit") + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("Coordinate", "Unit") -def test_parse_cpp_type_from_ref_with_prefix_and_suffix(driver_mock, cpp_type_prefix, +def test_parse_cpp_type_from_ref_with_prefix_and_suffix(reference_mock, cpp_type_prefix, cpp_type_suffix): type_element = ET.Element("type") type_element.text = cpp_type_prefix @@ -102,11 +102,11 @@ def test_parse_cpp_type_from_ref_with_prefix_and_suffix(driver_mock, cpp_type_pr prefix=cpp_type_prefix, suffix=cpp_type_suffix, nested=IsEmpty(), - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() # has_id, so not unresolved + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() # has_id, so not unresolved -def test_parse_cpp_type_from_ref_with_nested_text_type(driver_mock): +def test_parse_cpp_type_from_ref_with_nested_text_type(reference_mock): type_element = ET.Element("type") type_element.text = "const " sub_element(type_element, @@ -133,11 +133,11 @@ def test_parse_cpp_type_from_ref_with_nested_text_type(driver_mock): suffix=IsEmpty(), ) ], - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("Unit") + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("Unit") -def test_parse_cpp_type_from_text_with_nested_ref_type(driver_mock): +def test_parse_cpp_type_from_text_with_nested_ref_type(reference_mock): type_element = ET.Element("type") type_element.text = "const std::unique_ptr< const " sub_element(type_element, @@ -164,11 +164,11 @@ def test_parse_cpp_type_from_text_with_nested_ref_type(driver_mock): suffix=" &", ), ], - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() # has id, so not unresolved + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() # has id, so not unresolved -def test_parse_cpp_type_from_multiple_nested_text_and_ref(driver_mock): +def test_parse_cpp_type_from_multiple_nested_text_and_ref(reference_mock): type_element = ET.Element("type") type_element.text = "const " sub_element(type_element, @@ -237,11 +237,11 @@ def test_parse_cpp_type_from_multiple_nested_text_and_ref(driver_mock): ], ), ], - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() # has id, so not unresolved + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() # has id, so not unresolved -def test_parse_cpp_type_multiple_prefix_and_suffix(driver_mock): +def test_parse_cpp_type_multiple_prefix_and_suffix(reference_mock): type_element = ET.Element("type") type_element.text = "mutable volatile std::string * const *" @@ -252,8 +252,8 @@ def test_parse_cpp_type_multiple_prefix_and_suffix(driver_mock): name="std::string", prefix="mutable volatile ", suffix=" * const *", - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() # built-in type + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() # built-in type @pytest.fixture(params=[ @@ -267,7 +267,7 @@ def cpp_type_with_space(request): return request.param -def test_parse_cpp_type_with_space(driver_mock, cpp_type_prefix, cpp_type_with_space, +def test_parse_cpp_type_with_space(reference_mock, cpp_type_prefix, cpp_type_with_space, cpp_type_suffix): type_element = ET.Element("type") type_element.text = f"{cpp_type_prefix}{cpp_type_with_space}{cpp_type_suffix}" @@ -279,11 +279,11 @@ def test_parse_cpp_type_with_space(driver_mock, cpp_type_prefix, cpp_type_with_s name=cpp_type_with_space, prefix=cpp_type_prefix, suffix=cpp_type_suffix, - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() # built-in type + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() # built-in type -def test_parse_cpp_type_with_member(driver_mock): +def test_parse_cpp_type_with_member(reference_mock): type_element = ET.Element("type") type_element.text = "MyType::member" @@ -302,11 +302,11 @@ def test_parse_cpp_type_with_member(driver_mock): nested=IsEmpty(), ), ], - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("MyType", "NestedType") + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("MyType", "NestedType") -def test_parse_cpp_type_with_function_arguments(driver_mock): +def test_parse_cpp_type_with_function_arguments(reference_mock): type_element = ET.Element("type") type_element.text = "MyType(const Message&, ErrorCode code)" @@ -347,11 +347,11 @@ def test_parse_cpp_type_with_function_arguments(driver_mock): nested=IsEmpty(), args=IsEmpty(), ), - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("MyType", "Message", "ErrorCode") + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("MyType", "Message", "ErrorCode") -def test_parse_cpp_type_with_function_arguments__with_prefix_and_suffix(driver_mock): +def test_parse_cpp_type_with_function_arguments__with_prefix_and_suffix(reference_mock): type_element = ET.Element("type") type_element.text = "const MyType&(const Message&, ErrorCode code)" @@ -393,13 +393,13 @@ def test_parse_cpp_type_with_function_arguments__with_prefix_and_suffix(driver_m nested=IsEmpty(), args=IsEmpty(), ), - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("MyType", "Message", "ErrorCode") + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("MyType", "Message", "ErrorCode") @pytest.mark.parametrize("arg_name", ["", " value"]) -def test_parse_cpp_type_with_function_arguments_with_space_in_type(driver_mock, cpp_type_with_space, - arg_name): +def test_parse_cpp_type_with_function_arguments_with_space_in_type(reference_mock, + cpp_type_with_space, arg_name): type_element = ET.Element("type") type_element.text = f"MyType({cpp_type_with_space}{arg_name})" @@ -430,11 +430,11 @@ def test_parse_cpp_type_with_function_arguments_with_space_in_type(driver_mock, nested=IsEmpty(), args=IsEmpty(), ), - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("MyType") + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("MyType") -def test_parse_cpp_type__remove_constexpr(driver_mock): +def test_parse_cpp_type__remove_constexpr(reference_mock): type_element = ET.Element("type") type_element.text = "constexpr double" @@ -447,19 +447,19 @@ def test_parse_cpp_type__remove_constexpr(driver_mock): suffix=IsEmpty(), nested=IsEmpty(), args=IsEmpty(), - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() # built-in type + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() # built-in type -def test_parse_cpp_type__remove_constexpr_only(driver_mock): +def test_parse_cpp_type__remove_constexpr_only(reference_mock): type_element = ET.Element("type") type_element.text = "constexpr" - assert CppTypeParser.parse_xml(type_element, driver=driver_mock) is None - driver_mock.assert_unresolved() # nothing left + assert CppTypeParser.parse_xml(type_element, api_reference=reference_mock) is None + reference_mock.assert_unresolved() # nothing left -def test_parse_cpp_type__array(driver_mock): +def test_parse_cpp_type__array(reference_mock): type_element = ET.Element("type") type_element.text = "MyType[]" @@ -467,11 +467,11 @@ def test_parse_cpp_type__array(driver_mock): name="MyType", prefix=IsEmpty(), suffix="[]", - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("MyType") + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("MyType") -def test_parse_cpp_type__array__with_size(driver_mock): +def test_parse_cpp_type__array__with_size(reference_mock): type_element = ET.Element("type") type_element.text = "MyType[16]" @@ -479,11 +479,11 @@ def test_parse_cpp_type__array__with_size(driver_mock): name="MyType", prefix=IsEmpty(), suffix="[16]", - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("MyType") + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("MyType") -def test_parse_cpp_type__array__with_prefix_and_suffix(driver_mock): +def test_parse_cpp_type__array__with_prefix_and_suffix(reference_mock): type_element = ET.Element("type") type_element.text = "const MyType[]*" @@ -491,11 +491,11 @@ def test_parse_cpp_type__array__with_prefix_and_suffix(driver_mock): name="MyType", prefix="const ", suffix="[]*", - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("MyType") + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("MyType") -def test_parse_cpp_type__array__as_nested_type(driver_mock): +def test_parse_cpp_type__array__as_nested_type(reference_mock): type_element = ET.Element("type") type_element.text = "std::shared_ptr" @@ -510,11 +510,11 @@ def test_parse_cpp_type__array__as_nested_type(driver_mock): suffix="[]", ), ], - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("MyType") + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("MyType") -def test_parse_cpp_type__array__brackets_inside_name_element(driver_mock): +def test_parse_cpp_type__array__brackets_inside_name_element(reference_mock): type_element = ET.Element("type") sub_element(type_element, "ref", refid="tomtom_mytype", kindref="compound", text="MyType[]") ET.dump(type_element) @@ -524,11 +524,11 @@ def test_parse_cpp_type__array__brackets_inside_name_element(driver_mock): name="MyType", prefix=IsEmpty(), suffix="[]", - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() -def test_parse_cpp_type__array__multiple_brackets_inside_name_element(driver_mock): +def test_parse_cpp_type__array__multiple_brackets_inside_name_element(reference_mock): type_element = ET.Element("type") sub_element(type_element, "ref", refid="tomtom_mytype", kindref="compound", text="MyType[][]") ET.dump(type_element) @@ -538,11 +538,11 @@ def test_parse_cpp_type__array__multiple_brackets_inside_name_element(driver_moc name="MyType", prefix=IsEmpty(), suffix="[][]", - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() -def test_parse_cpp_type__array__with_size_inside_name_element(driver_mock): +def test_parse_cpp_type__array__with_size_inside_name_element(reference_mock): type_element = ET.Element("type") sub_element(type_element, "ref", refid="tomtom_mytype", kindref="compound", text="MyType[12]") ET.dump(type_element) @@ -552,18 +552,18 @@ def test_parse_cpp_type__array__with_size_inside_name_element(driver_mock): name="MyType", prefix=IsEmpty(), suffix="[12]", - ).assert_matches(CppTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() + ).assert_matches(CppTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() -def test_parse_cpp_type__array__end_bracket_without_start_inside_name_element(driver_mock): +def test_parse_cpp_type__array__end_bracket_without_start_inside_name_element(reference_mock): type_element = ET.Element("type") sub_element(type_element, "ref", refid="tomtom_mytype", kindref="compound", text="MyType]") ET.dump(type_element) with pytest.raises(TypeParseError): - CppTypeParser.parse_xml(type_element, driver=driver_mock) - driver_mock.assert_unresolved() + CppTypeParser.parse_xml(type_element, api_reference=reference_mock) + reference_mock.assert_unresolved() def namespace_sep(text: str = ":") -> Token: diff --git a/tests/unit/parser/doxygen/typeparser/test_java.py b/tests/unit/parser/doxygen/typeparser/test_java.py index 8a2e95c5..65424548 100644 --- a/tests/unit/parser/doxygen/typeparser/test_java.py +++ b/tests/unit/parser/doxygen/typeparser/test_java.py @@ -17,7 +17,7 @@ import pytest -from asciidoxy.parser.doxygen.java import JavaTypeParser +from asciidoxy.parser.doxygen.language.java import JavaTypeParser from asciidoxy.parser.doxygen.language_traits import TokenCategory from asciidoxy.parser.doxygen.type_parser import Token from tests.unit.matchers import IsEmpty, IsNone, m_typeref @@ -29,7 +29,7 @@ def java_type_prefix(request): return request.param -def test_parse_java_type_from_text_simple(driver_mock, java_type_prefix): +def test_parse_java_type_from_text_simple(reference_mock, java_type_prefix): type_element = ET.Element("type") type_element.text = f"{java_type_prefix}double" @@ -41,11 +41,11 @@ def test_parse_java_type_from_text_simple(driver_mock, java_type_prefix): prefix=java_type_prefix, suffix=IsEmpty(), nested=IsEmpty(), - ).assert_matches(JavaTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() # built-in type + ).assert_matches(JavaTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() # built-in type -def test_parse_java_type_with_mangled_annotation(driver_mock, java_type_prefix): +def test_parse_java_type_with_mangled_annotation(reference_mock, java_type_prefix): type_element = ET.Element("type") type_element.text = f"{java_type_prefix}__AT__Nullable__ Data" @@ -57,11 +57,11 @@ def test_parse_java_type_with_mangled_annotation(driver_mock, java_type_prefix): prefix=f"{java_type_prefix}@Nullable ", suffix=IsEmpty(), nested=IsEmpty(), - ).assert_matches(JavaTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("Data") + ).assert_matches(JavaTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("Data") -def test_parse_java_type_with_original_annotation(driver_mock, java_type_prefix): +def test_parse_java_type_with_original_annotation(reference_mock, java_type_prefix): type_element = ET.Element("type") type_element.text = f"{java_type_prefix}@Nullable Data" @@ -73,14 +73,15 @@ def test_parse_java_type_with_original_annotation(driver_mock, java_type_prefix) prefix=f"{java_type_prefix}@Nullable ", suffix=IsEmpty(), nested=IsEmpty(), - ).assert_matches(JavaTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("Data") + ).assert_matches(JavaTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("Data") @pytest.mark.parametrize("generic_prefix, generic_name", [("? extends ", "Unit"), ("T extends ", "Unit"), ("T extends ", "Unit "), ("? super ", "Unit"), ("T super ", "Unit"), ("", "T "), ("", "T")]) -def test_parse_java_type_with_generic(driver_mock, java_type_prefix, generic_prefix, generic_name): +def test_parse_java_type_with_generic(reference_mock, java_type_prefix, generic_prefix, + generic_name): type_element = ET.Element("type") type_element.text = f"{java_type_prefix}Position<{generic_prefix or ''}{generic_name}>" @@ -98,16 +99,16 @@ def test_parse_java_type_with_generic(driver_mock, java_type_prefix, generic_pre suffix=IsEmpty(), ), ], - ).assert_matches(JavaTypeParser.parse_xml(type_element, driver=driver_mock)) + ).assert_matches(JavaTypeParser.parse_xml(type_element, api_reference=reference_mock)) if generic_name.strip() == "T": - driver_mock.assert_unresolved("Position") - assert driver_mock.unresolved_ref.call_count == 1 + reference_mock.assert_unresolved("Position") + assert reference_mock.add_unresolved_reference.call_count == 1 else: - driver_mock.assert_unresolved("Position", generic_name.strip()) + reference_mock.assert_unresolved("Position", generic_name.strip()) -def test_parse_java_type_with_nested_wildcard_generic(driver_mock): +def test_parse_java_type_with_nested_wildcard_generic(reference_mock): type_element = ET.Element("type") type_element.text = "Position>" @@ -132,11 +133,11 @@ def test_parse_java_type_with_nested_wildcard_generic(driver_mock): ], ), ], - ).assert_matches(JavaTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("Position", "Getter") + ).assert_matches(JavaTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("Position", "Getter") -def test_parse_java_type_with_separate_wildcard_bounds(driver_mock): +def test_parse_java_type_with_separate_wildcard_bounds(reference_mock): type_element = ET.Element("type") type_element.text = "> T" @@ -148,11 +149,11 @@ def test_parse_java_type_with_separate_wildcard_bounds(driver_mock): prefix="> ", suffix=IsEmpty(), nested=IsEmpty(), - ).assert_matches(JavaTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() + ).assert_matches(JavaTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() -def test_parse_java_type__array(driver_mock): +def test_parse_java_type__array(reference_mock): type_element = ET.Element("type") type_element.text = "MyType[]" @@ -160,11 +161,11 @@ def test_parse_java_type__array(driver_mock): name="MyType", prefix=IsEmpty(), suffix="[]", - ).assert_matches(JavaTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("MyType") + ).assert_matches(JavaTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("MyType") -def test_parse_java_type__array__brackets_inside_name_element(driver_mock): +def test_parse_java_type__array__brackets_inside_name_element(reference_mock): type_element = ET.Element("type") sub_element(type_element, "ref", refid="tomtom_mytype", kindref="compound", text="MyType[]") ET.dump(type_element) @@ -174,11 +175,11 @@ def test_parse_java_type__array__brackets_inside_name_element(driver_mock): name="MyType", prefix=IsEmpty(), suffix="[]", - ).assert_matches(JavaTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() + ).assert_matches(JavaTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() -def test_parse_java_type__array__multiple_brackets_inside_name_element(driver_mock): +def test_parse_java_type__array__multiple_brackets_inside_name_element(reference_mock): type_element = ET.Element("type") sub_element(type_element, "ref", refid="tomtom_mytype", kindref="compound", text="MyType[][]") ET.dump(type_element) @@ -188,8 +189,8 @@ def test_parse_java_type__array__multiple_brackets_inside_name_element(driver_mo name="MyType", prefix=IsEmpty(), suffix="[][]", - ).assert_matches(JavaTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() + ).assert_matches(JavaTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() @pytest.mark.parametrize("tokens,expected", [ diff --git a/tests/unit/parser/doxygen/typeparser/test_objc.py b/tests/unit/parser/doxygen/typeparser/test_objc.py index bf1c090c..c7543e2b 100644 --- a/tests/unit/parser/doxygen/typeparser/test_objc.py +++ b/tests/unit/parser/doxygen/typeparser/test_objc.py @@ -17,8 +17,8 @@ import pytest +from asciidoxy.parser.doxygen.language.objc import ObjectiveCTypeParser from asciidoxy.parser.doxygen.language_traits import TokenCategory -from asciidoxy.parser.doxygen.objc import ObjectiveCTypeParser from asciidoxy.parser.doxygen.type_parser import Token, TypeParseError from tests.unit.matchers import IsEmpty, IsNone, m_typeref from tests.unit.shared import sub_element @@ -58,7 +58,7 @@ def objc_type_suffix(request): return request.param -def test_parse_objc_type_from_text_simple(driver_mock, objc_type_prefix, objc_type_suffix): +def test_parse_objc_type_from_text_simple(reference_mock, objc_type_prefix, objc_type_suffix): type_element = ET.Element("type") type_element.text = f"{objc_type_prefix}NSInteger{objc_type_suffix}" @@ -70,8 +70,8 @@ def test_parse_objc_type_from_text_simple(driver_mock, objc_type_prefix, objc_ty prefix=objc_type_prefix, suffix=objc_type_suffix, nested=IsEmpty(), - ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() # built-in type + ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() # built-in type @pytest.mark.parametrize("type_with_space", [ @@ -98,7 +98,7 @@ def test_parse_objc_type_from_text_simple(driver_mock, objc_type_prefix, objc_ty "unsigned char", "signed char", ]) -def test_parse_objc_type_with_space(driver_mock, type_with_space): +def test_parse_objc_type_with_space(reference_mock, type_with_space): type_element = ET.Element("type") type_element.text = type_with_space @@ -110,11 +110,11 @@ def test_parse_objc_type_with_space(driver_mock, type_with_space): prefix=IsEmpty(), suffix=IsEmpty(), nested=IsEmpty(), - ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() # built-in type + ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() # built-in type -def test_parse_objc_type__array(driver_mock): +def test_parse_objc_type__array(reference_mock): type_element = ET.Element("type") type_element.text = "MyType[]" @@ -122,11 +122,11 @@ def test_parse_objc_type__array(driver_mock): name="MyType", prefix=IsEmpty(), suffix="[]", - ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("MyType") + ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("MyType") -def test_parse_objc_type__array__with_size(driver_mock): +def test_parse_objc_type__array__with_size(reference_mock): type_element = ET.Element("type") type_element.text = "MyType[16]" @@ -134,11 +134,11 @@ def test_parse_objc_type__array__with_size(driver_mock): name="MyType", prefix=IsEmpty(), suffix="[16]", - ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("MyType") + ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("MyType") -def test_parse_objc_type__array__with_prefix_and_suffix(driver_mock): +def test_parse_objc_type__array__with_prefix_and_suffix(reference_mock): type_element = ET.Element("type") type_element.text = "const MyType[]*" @@ -146,11 +146,11 @@ def test_parse_objc_type__array__with_prefix_and_suffix(driver_mock): name="MyType", prefix="const ", suffix="[]*", - ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("MyType") + ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("MyType") -def test_parse_objc_type__array__as_nested_type(driver_mock): +def test_parse_objc_type__array__as_nested_type(reference_mock): type_element = ET.Element("type") type_element.text = "id" @@ -165,11 +165,11 @@ def test_parse_objc_type__array__as_nested_type(driver_mock): suffix="[]", ), ], - ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved("MyType") + ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved("MyType") -def test_parse_objc_type__array__brackets_inside_name_element(driver_mock): +def test_parse_objc_type__array__brackets_inside_name_element(reference_mock): type_element = ET.Element("type") sub_element(type_element, "ref", refid="tomtom_mytype", kindref="compound", text="MyType[]") ET.dump(type_element) @@ -179,11 +179,11 @@ def test_parse_objc_type__array__brackets_inside_name_element(driver_mock): name="MyType", prefix=IsEmpty(), suffix="[]", - ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() + ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() -def test_parse_objc_type__array__multiple_brackets_inside_name_element(driver_mock): +def test_parse_objc_type__array__multiple_brackets_inside_name_element(reference_mock): type_element = ET.Element("type") sub_element(type_element, "ref", refid="tomtom_mytype", kindref="compound", text="MyType[][]") ET.dump(type_element) @@ -193,11 +193,11 @@ def test_parse_objc_type__array__multiple_brackets_inside_name_element(driver_mo name="MyType", prefix=IsEmpty(), suffix="[][]", - ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() + ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() -def test_parse_objc_type__array__with_size_inside_name_element(driver_mock): +def test_parse_objc_type__array__with_size_inside_name_element(reference_mock): type_element = ET.Element("type") sub_element(type_element, "ref", refid="tomtom_mytype", kindref="compound", text="MyType[12]") ET.dump(type_element) @@ -207,18 +207,18 @@ def test_parse_objc_type__array__with_size_inside_name_element(driver_mock): name="MyType", prefix=IsEmpty(), suffix="[12]", - ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, driver=driver_mock)) - driver_mock.assert_unresolved() + ).assert_matches(ObjectiveCTypeParser.parse_xml(type_element, api_reference=reference_mock)) + reference_mock.assert_unresolved() -def test_parse_objc_type__array__end_bracket_without_start_inside_name_element(driver_mock): +def test_parse_objc_type__array__end_bracket_without_start_inside_name_element(reference_mock): type_element = ET.Element("type") sub_element(type_element, "ref", refid="tomtom_mytype", kindref="compound", text="MyType]") ET.dump(type_element) with pytest.raises(TypeParseError): - ObjectiveCTypeParser.parse_xml(type_element, driver=driver_mock) - driver_mock.assert_unresolved() + ObjectiveCTypeParser.parse_xml(type_element, api_reference=reference_mock) + reference_mock.assert_unresolved() def block(text: str = "^") -> Token: diff --git a/tests/unit/parser/doxygen/typeparser/test_python.py b/tests/unit/parser/doxygen/typeparser/test_python.py index c9e3cca4..ae10710f 100644 --- a/tests/unit/parser/doxygen/typeparser/test_python.py +++ b/tests/unit/parser/doxygen/typeparser/test_python.py @@ -13,8 +13,8 @@ # limitations under the License. """Tests for parsing python types.""" +from asciidoxy.parser.doxygen.language.python import PythonTypeParser from asciidoxy.parser.doxygen.language_traits import TokenCategory -from asciidoxy.parser.doxygen.python import PythonTypeParser from asciidoxy.parser.doxygen.type_parser import Token diff --git a/tests/unit/parser/doxygen/typeparser/test_type_parser.py b/tests/unit/parser/doxygen/typeparser/test_type_parser.py index 2f6a44b0..cc50e068 100644 --- a/tests/unit/parser/doxygen/typeparser/test_type_parser.py +++ b/tests/unit/parser/doxygen/typeparser/test_type_parser.py @@ -612,11 +612,11 @@ def _match_type(actual, assert not actual.args -def test_type_parser__type_from_tokens(prefixes, names, nested_types, arg_types, suffixes): - driver_mock = MagicMock() +def test_type_parser__type_from_tokens(prefixes, names, nested_types, arg_types, suffixes, + reference_mock): type_ref = TestParser.type_from_tokens(prefixes + names.tokens + nested_types.tokens + suffixes + arg_types.tokens, - driver=driver_mock) + api_reference=reference_mock) assert type_ref is not None if arg_types.expected_types: @@ -655,10 +655,8 @@ def test_type_parser__type_from_tokens(prefixes, names, nested_types, arg_types, unchecked_type_ids.append(expected_type.refid) else: unresolved_type_names.append(expected_type.name) - assert (sorted([args[0].name for args, _ in driver_mock.unresolved_ref.call_args_list - ]) == sorted(unresolved_type_names)) - assert (sorted([args[0].id for args, _ in driver_mock.unchecked_ref.call_args_list - ]) == sorted(unchecked_type_ids)) + reference_mock.assert_unresolved(*unresolved_type_names) + reference_mock.assert_unchecked(*unchecked_type_ids) @pytest.mark.parametrize("tokens,expected_type", [ @@ -767,7 +765,7 @@ def test_type_parser__type_from_tokens__no_namespace(): assert type_ref.nested[1].namespace is None -def test_type_parser__type_from_tokens__do_not_register_builtin_types(): +def test_type_parser__type_from_tokens__do_not_register_builtin_types(reference_mock): tokens = [ name("BuiltinType"), nested_start(), @@ -782,11 +780,9 @@ def test_type_parser__type_from_tokens__do_not_register_builtin_types(): nested_end() ] - driver_mock = MagicMock() - type_ref = TestParser.type_from_tokens(tokens, driver=driver_mock) + type_ref = TestParser.type_from_tokens(tokens, api_reference=reference_mock) assert type_ref.name == "BuiltinType" - assert (sorted([args[0].name for args, _ in driver_mock.unresolved_ref.call_args_list - ]) == sorted(["OtherType", "MyType", "OtherType"])) + reference_mock.assert_unresolved("OtherType", "MyType", "OtherType") @pytest.mark.parametrize("tokens,expected", [ @@ -1040,10 +1036,10 @@ def test_type_parser__parse_xml__unresolved_ref_with_driver(): element = ET.Element("type") element.text = "MyType" - driver_mock = MagicMock() - type_ref = TestParser.parse_xml(element, driver=driver_mock) - driver_mock.unresolved_ref.assert_called_once_with(type_ref) - driver_mock.unchecked_ref.assert_not_called() + reference_mock = MagicMock() + type_ref = TestParser.parse_xml(element, api_reference=reference_mock) + reference_mock.add_unresolved_reference.assert_called_once_with(type_ref) + reference_mock.add_unchecked_reference.assert_not_called() assert type_ref is not None assert type_ref.name == "MyType" @@ -1056,10 +1052,10 @@ def test_type_parser__parse_xml__register_ref_with_id_as_unchecked(): element = ET.Element("type") sub_element(element, "ref", text="MyType", refid="my_type", kindref="compound") - driver_mock = MagicMock() - type_ref = TestParser.parse_xml(element, driver=driver_mock) - driver_mock.unresolved_ref.assert_not_called() - driver_mock.unchecked_ref.assert_called_once_with(type_ref) + reference_mock = MagicMock() + type_ref = TestParser.parse_xml(element, api_reference=reference_mock) + reference_mock.add_unresolved_reference.assert_not_called() + reference_mock.add_unchecked_reference.assert_called_once_with(type_ref) assert type_ref is not None assert type_ref.name == "MyType" @@ -1072,10 +1068,12 @@ def test_type_parser__parse_xml__namespace(): element = ET.Element("type") sub_element(element, "ref", text="MyType", refid="my_type", kindref="compound") - driver_mock = MagicMock() - type_ref = TestParser.parse_xml(element, driver=driver_mock, namespace="asciidoxy::test") - driver_mock.unresolved_ref.assert_not_called() - driver_mock.unchecked_ref.assert_called_once_with(type_ref) + reference_mock = MagicMock() + type_ref = TestParser.parse_xml(element, + api_reference=reference_mock, + namespace="asciidoxy::test") + reference_mock.add_unresolved_reference.assert_not_called() + reference_mock.add_unchecked_reference.assert_called_once_with(type_ref) assert type_ref is not None assert type_ref.name == "MyType" diff --git a/tests/unit/shared.py b/tests/unit/shared.py index 8aa4739a..8890751e 100644 --- a/tests/unit/shared.py +++ b/tests/unit/shared.py @@ -35,6 +35,7 @@ def assert_equal_or_none_if_empty(value: Optional[str], text: str) -> None: assert value == text +# TODO: Make fixture class ProgressMock: def __init__(self): self.total = 0 diff --git a/tests/unit/test_api_reference.py b/tests/unit/test_api_reference.py index 266ef138..a6c8603d 100644 --- a/tests/unit/test_api_reference.py +++ b/tests/unit/test_api_reference.py @@ -15,8 +15,14 @@ import pytest -from asciidoxy.api_reference import AmbiguousLookupError, NameFilter, ParameterTypeMatcher -from asciidoxy.parser.doxygen import Driver as ParserDriver +from asciidoxy.api_reference import ( + AmbiguousLookupError, + ApiReference, + NameFilter, + ParameterTypeMatcher, +) +from asciidoxy.parser import parser_factory +from tests.unit.shared import ProgressMock def test_function_matcher__parse__no_arguments(): @@ -276,8 +282,8 @@ def test_find_by_name_with_spaces(api_reference): namespace="asciidoxy::tparam") is not None -def test_find_by_name_and_kind(test_data): - parser = ParserDriver() +def test_find_by_name_and_kind(test_data, default_config): + parser = parser_factory("doxygen", ApiReference(), default_config) parser.parse(test_data / "ambiguous_names.xml") element = parser.api_reference.find("Coordinate", kind="class") @@ -291,8 +297,8 @@ def test_find_by_name_and_kind(test_data): assert parser.api_reference.find("Coordinate", kind="function") is None -def test_find_by_name_and_lang(test_data): - parser = ParserDriver() +def test_find_by_name_and_lang(test_data, default_config): + parser = parser_factory("doxygen", ApiReference(), default_config) parser.parse(test_data / "ambiguous_names.xml") element = parser.api_reference.find("BoundingBox", lang="java") @@ -336,8 +342,8 @@ def test_find_by_name__prefer_exact_match(api_reference): namespace="asciidoxy::geometry").namespace == "asciidoxy::geometry" -def test_find_by_name_ambiguous(test_data): - parser = ParserDriver() +def test_find_by_name_ambiguous(test_data, default_config): + parser = parser_factory("doxygen", ApiReference(), default_config) parser.parse(test_data / "ambiguous_names.xml") with pytest.raises(AmbiguousLookupError) as exception1: @@ -386,8 +392,8 @@ def test_find_by_name__overload_set__multiple_namespaces_are_ambiguous(api_refer assert len(exception.value.candidates) == 3 -def test_find_by_name__overload_set__multiple_kinds_are_ambiguous(test_data): - parser = ParserDriver() +def test_find_by_name__overload_set__multiple_kinds_are_ambiguous(test_data, default_config): + parser = parser_factory("doxygen", ApiReference(), default_config) parser.parse(test_data / "ambiguous_names.xml") with pytest.raises(AmbiguousLookupError) as exception: @@ -407,8 +413,8 @@ def test_find_by_name__overload_set__multiple_kinds_are_ambiguous(test_data): assert element.kind == "interface" -def test_find_by_name__overload_set__multiple_languages_are_ambiguous(test_data): - parser = ParserDriver() +def test_find_by_name__overload_set__multiple_languages_are_ambiguous(test_data, default_config): + parser = parser_factory("doxygen", ApiReference(), default_config) parser.parse(test_data / "ambiguous_names.xml") with pytest.raises(AmbiguousLookupError) as exception: @@ -498,3 +504,297 @@ def test_find_method__select_based_on_args_2__spaces_dont_matter(api_reference): element = api_reference.find("asciidoxy::geometry::Coordinate::Update(double,double,double)") assert element is not None + + +def test_resolve_references_for_return_types(parser_driver_factory): + # TODO: Rewrite independent from parser + parser = parser_driver_factory("cpp/default", "cpp/consumer") + + member = parser.api_reference.find("asciidoxy::positioning::Positioning::CurrentPosition", + kind="function", + lang="cpp") + assert member is not None + assert member.returns + assert member.returns.type + assert not member.returns.type.id + + parser.api_reference.resolve_references() + + assert member.returns + assert member.returns.type + assert member.returns.type.id == "cpp-classasciidoxy_1_1geometry_1_1_coordinate" + assert member.returns.type.kind == "class" + + +def test_resolve_partial_references_for_return_types(parser_driver_factory): + # TODO: Rewrite independent from parser + parser = parser_driver_factory("cpp/default", "cpp/consumer") + + member = parser.api_reference.find("asciidoxy::positioning::Positioning::TrafficNearby", + kind="function", + lang="cpp") + assert member is not None + assert member.returns + assert member.returns.type + assert len(member.returns.type.nested) == 1 + assert not member.returns.type.nested[0].id + + parser.api_reference.resolve_references() + + assert member.returns + assert member.returns.type + assert len(member.returns.type.nested) == 1 + assert member.returns.type.nested[0].id == "cpp-classasciidoxy_1_1traffic_1_1_traffic_event" + assert member.returns.type.nested[0].kind == "class" + + +def test_resolve_references_for_parameters(parser_driver_factory): + # TODO: Rewrite independent from parser + parser = parser_driver_factory("cpp/default", "cpp/consumer") + + member = parser.api_reference.find("asciidoxy::positioning::Positioning::IsNearby", + kind="function", + lang="cpp") + assert member is not None + assert len(member.params) == 1 + assert member.params[0].type + assert not member.params[0].type.id + + parser.api_reference.resolve_references() + + assert len(member.params) == 1 + assert member.params[0].type + assert member.params[0].type.id == "cpp-classasciidoxy_1_1geometry_1_1_coordinate" + assert member.params[0].type.kind == "class" + + +def test_resolve_partial_references_for_parameters(parser_driver_factory): + # TODO: Rewrite independent from parser + parser = parser_driver_factory("cpp/default", "cpp/consumer") + + member = parser.api_reference.find("asciidoxy::positioning::Positioning::InTraffic", + kind="function", + lang="cpp") + assert member is not None + assert len(member.params) == 1 + assert member.params[0].type + assert not member.params[0].type.id + + parser.api_reference.resolve_references() + + assert len(member.params) == 1 + assert member.params[0].type + assert member.params[0].type.id == "cpp-classasciidoxy_1_1traffic_1_1_traffic_event" + assert member.params[0].type.kind == "class" + + +def test_resolve_references_for_typedefs(parser_driver_factory): + # TODO: Rewrite independent from parser + parser = parser_driver_factory("cpp/default", "cpp/consumer") + + member = parser.api_reference.find("asciidoxy::positioning::Traffic", kind="alias") + assert member is not None + assert member.returns + assert member.returns.type + assert not member.returns.type.id + + parser.api_reference.resolve_references() + + assert member.returns + assert member.returns.type + assert member.returns.type.id == "cpp-classasciidoxy_1_1traffic_1_1_traffic_event" + assert member.returns.type.kind == "class" + + +def test_resolve_references_for_inner_type_reference(parser_driver_factory): + # TODO: Rewrite independent from parser + parser = parser_driver_factory("cpp/default") + + parent_class = parser.api_reference.find("asciidoxy::traffic::TrafficEvent", + kind="class", + lang="cpp") + assert parent_class is not None + inner_classes = [m for m in parent_class.members if m.kind == "struct"] + assert len(inner_classes) == 0 + + parser.api_reference.resolve_references() + + inner_classes = [m for m in parent_class.members if m.kind == "struct"] + assert len(inner_classes) > 0 + + nested_class = inner_classes[0] + assert nested_class.full_name == "asciidoxy::traffic::TrafficEvent::TrafficEventData" + + +def test_resolve_references_for_exceptions(parser_driver_factory): + # TODO: Rewrite independent from parser + parser = parser_driver_factory("cpp/default", "cpp/consumer") + + member = parser.api_reference.find("asciidoxy::positioning::Positioning::Override", + kind="function") + assert member is not None + assert len(member.exceptions) == 1 + assert member.exceptions[0].type + assert not member.exceptions[0].type.id + + parser.api_reference.resolve_references() + + assert member.returns + assert len(member.exceptions) == 1 + assert member.exceptions[0].type + assert member.exceptions[0].type.id == "cpp-classasciidoxy_1_1geometry_1_1_invalid_coordinate" + assert member.exceptions[0].type.kind == "class" + + +def test_resolve_partial_references_for_exceptions(parser_driver_factory): + # TODO: Rewrite independent from parser + parser = parser_driver_factory("cpp/default", "cpp/consumer") + + member = parser.api_reference.find("asciidoxy::positioning::Positioning::TrafficNearby", + kind="function") + assert member is not None + assert len(member.exceptions) == 1 + assert member.exceptions[0].type + assert not member.exceptions[0].type.id + + parser.api_reference.resolve_references() + + assert member.returns + assert len(member.exceptions) == 1 + assert member.exceptions[0].type + assert member.exceptions[0].type.id == "cpp-classasciidoxy_1_1geometry_1_1_invalid_coordinate" + assert member.exceptions[0].type.kind == "class" + + +def test_resolve_references_prefer_same_namespace(parser_driver_factory): + # TODO: Rewrite independent from parser + parser = parser_driver_factory("cpp/default", "cpp/consumer") + + member_a_t = parser.api_reference.find("asciidoxy::traffic::CreateConvertor", + kind="function", + lang="cpp") + assert member_a_t is not None + assert member_a_t.returns + assert member_a_t.returns.type + assert not member_a_t.returns.type.id + assert not member_a_t.returns.type.kind + + member_a = parser.api_reference.find("asciidoxy::CreateConvertor", kind="function", lang="cpp") + assert member_a is not None + assert member_a.returns + assert member_a.returns.type + assert not member_a.returns.type.id + assert not member_a.returns.type.kind + + member_a_g = parser.api_reference.find("asciidoxy::geometry::CreateConvertor", + kind="function", + lang="cpp") + assert member_a_g is not None + assert member_a_g.returns + assert member_a_g.returns.type + assert not member_a_g.returns.type.id + assert not member_a_g.returns.type.kind + + parser.api_reference.resolve_references() + + assert member_a_t.returns + assert member_a_t.returns.type + assert member_a_t.returns.type.id == "cpp-classasciidoxy_1_1traffic_1_1_convertor" + assert member_a_t.returns.type.kind == "class" + assert member_a_t.returns.type.namespace == "asciidoxy::traffic" + + assert member_a.returns + assert member_a.returns.type + assert member_a.returns.type.id == "cpp-classasciidoxy_1_1_convertor" + assert member_a.returns.type.kind == "class" + assert member_a.returns.type.namespace == "asciidoxy" + + assert member_a_g.returns + assert member_a_g.returns.type + assert member_a_g.returns.type.id == "cpp-classasciidoxy_1_1geometry_1_1_convertor" + assert member_a_g.returns.type.kind == "class" + assert member_a_g.returns.type.namespace == "asciidoxy::geometry" + + +def test_resolve_references_fails_when_ambiguous(parser_driver_factory): + # TODO: Rewrite independent from parser + # When internal resolving fails, the type will not be resolved to the exact type, and will be + # shown in the documentation as plain text. It should not raise an error. + parser = parser_driver_factory("cpp/default", "cpp/consumer") + + member = parser.api_reference.find("asciidoxy::traffic::geometry::CreateConvertor", + kind="function", + lang="cpp") + assert member is not None + assert member.returns + assert member.returns.type + assert not member.returns.type.id + assert not member.returns.type.kind + + parser.api_reference.resolve_references() + + assert member.returns + assert member.returns.type + assert not member.returns.type.id + assert not member.returns.type.kind + + +def test_resolve_references__report_progress(parser_driver_factory): + # TODO: Rewrite independent from parser + parser = parser_driver_factory("cpp/default", "cpp/consumer") + unresolved_ref_count = parser.api_reference.unresolved_ref_count + assert unresolved_ref_count > 70 + + progress_mock = ProgressMock() + parser.api_reference.resolve_references(progress=progress_mock) + + assert progress_mock.ready == progress_mock.total + assert progress_mock.total == unresolved_ref_count + + +def test_check_references__reference_found(parser_driver_factory): + # TODO: Rewrite independent from parser + parser = parser_driver_factory("cpp/default") + + element = parser.api_reference.find("asciidoxy::geometry::Print") + assert element is not None + assert len(element.params) == 1 + assert element.params[0].type + assert element.params[0].type.id + + assert parser.api_reference.unchecked_ref_count > 0 + parser.api_reference.check_references() + assert parser.api_reference.unchecked_ref_count == 0 + + assert element.params[0].type.id + + +def test_check_references__remove_missing_refids(xml_data, default_config): + # TODO: Rewrite independent from parser + parser = parser_factory("doxygen", ApiReference(), default_config) + parser.parse(xml_data / "cpp/default/xml/namespaceasciidoxy_1_1geometry.xml") + + element = parser.api_reference.find("asciidoxy::geometry::Print") + assert element is not None + assert len(element.params) == 1 + assert element.params[0].type + assert element.params[0].type.id + + assert parser.api_reference.unchecked_ref_count > 0 + parser.api_reference.check_references() + assert parser.api_reference.unchecked_ref_count == 0 + + assert not element.params[0].type.id + + +def test_check_references__report_progress(parser_driver_factory): + # TODO: Rewrite independent from parser + parser = parser_driver_factory("cpp/default", "cpp/consumer") + unchecked_ref_count = parser.api_reference.unchecked_ref_count + assert unchecked_ref_count > 0 + + progress_mock = ProgressMock() + parser.api_reference.check_references(progress=progress_mock) + + assert progress_mock.ready == progress_mock.total + assert progress_mock.total == unchecked_ref_count