├── docs ├── .gitignore ├── requirements.txt ├── simple.rst ├── types.rst ├── Makefile ├── make.bat ├── tools.rst ├── index.rst ├── custom.rst └── conf.py ├── cxxheaderparser ├── py.typed ├── __main__.py ├── __init__.py ├── _ply │ └── __init__.py ├── errors.py ├── options.py ├── tokfmt.py ├── dump.py ├── parserstate.py ├── gentest.py ├── visitor.py ├── simple.py └── preprocessor.py ├── tests ├── requirements.txt ├── README.md ├── test_tokfmt.py ├── test_numeric_literals.py ├── test_union.py ├── test_skip.py ├── test_namespaces.py ├── test_preprocessor.py ├── test_attributes.py ├── test_class_base.py ├── test_misc.py ├── test_typefmt.py ├── test_class_bitfields.py ├── test_friends.py ├── test_doxygen.py └── test_abv_template.py ├── .gitignore ├── mypy.ini ├── .readthedocs.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ └── parser-error.yml └── workflows │ ├── deploy.yml │ └── dist.yml ├── pyproject.toml ├── README.md └── LICENSE.txt /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /_build -------------------------------------------------------------------------------- /cxxheaderparser/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pcpp~=1.30 -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx >= 3.0 2 | sphinx-rtd-theme 3 | sphinx-autodoc-typehints 4 | pcpp -------------------------------------------------------------------------------- /cxxheaderparser/__main__.py: -------------------------------------------------------------------------------- 1 | from cxxheaderparser.dump import dumpmain 2 | 3 | if __name__ == "__main__": 4 | dumpmain() 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.egg-info 3 | /build 4 | /dist 5 | /cxxheaderparser/version.py 6 | /.vscode 7 | 8 | .coverage 9 | .pytest_cache -------------------------------------------------------------------------------- /cxxheaderparser/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from .version import __version__ # type: ignore 3 | except ImportError: 4 | __version__ = "master" 5 | -------------------------------------------------------------------------------- /cxxheaderparser/_ply/__init__.py: -------------------------------------------------------------------------------- 1 | # PLY package 2 | # Author: David Beazley (dave@dabeaz.com) 3 | # https://github.com/dabeaz/ply 4 | 5 | __version__ = "2022.10.27" 6 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | exclude = setup\.py|docs 3 | 4 | [mypy-pcpp.*] 5 | ignore_missing_imports = True 6 | 7 | [mypy-cxxheaderparser._ply.*] 8 | ignore_errors = True -------------------------------------------------------------------------------- /docs/simple.rst: -------------------------------------------------------------------------------- 1 | .. _simple: 2 | 3 | Simple API 4 | ========== 5 | 6 | .. automodule:: cxxheaderparser.simple 7 | :members: 8 | :undoc-members: 9 | 10 | .. automodule:: cxxheaderparser.options 11 | :members: 12 | :undoc-members: -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: docs/conf.py 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.11" 10 | 11 | python: 12 | install: 13 | - requirements: docs/requirements.txt 14 | - method: pip 15 | path: . 16 | -------------------------------------------------------------------------------- /docs/types.rst: -------------------------------------------------------------------------------- 1 | Types 2 | ===== 3 | 4 | parser types 5 | ------------ 6 | 7 | .. automodule:: cxxheaderparser.types 8 | :members: 9 | :undoc-members: 10 | 11 | exceptions 12 | ---------- 13 | 14 | .. automodule:: cxxheaderparser.errors 15 | :members: 16 | :undoc-members: -------------------------------------------------------------------------------- /cxxheaderparser/errors.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | if typing.TYPE_CHECKING: 4 | from .lexer import LexToken 5 | 6 | 7 | class CxxParseError(Exception): 8 | """ 9 | Exception raised when a parsing error occurs 10 | """ 11 | 12 | def __init__(self, msg: str, tok: typing.Optional["LexToken"] = None) -> None: 13 | Exception.__init__(self, msg) 14 | self.tok = tok 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Other bug Report 2 | description: File an issue about the Python API or other non-parsing issues 3 | title: "[BUG]: " 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Problem description 9 | placeholder: >- 10 | Provide a short description, state the expected behavior and what 11 | actually happens. 12 | validations: 13 | required: true 14 | 15 | - type: textarea 16 | id: code 17 | attributes: 18 | label: Reproducible example code 19 | placeholder: >- 20 | Minimal code to reproduce this issue 21 | render: text -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/parser-error.yml: -------------------------------------------------------------------------------- 1 | name: C++ parsing error 2 | description: cxxheaderparser fails to parse valid C/C++ code 3 | title: "[PARSE BUG]: " 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Problem description 9 | placeholder: >- 10 | Provide a short description 11 | validations: 12 | required: true 13 | 14 | - type: textarea 15 | id: code 16 | attributes: 17 | label: C++ code that can't be parsed correctly (please double-check that https://robotpy.github.io/cxxheaderparser/ has the same error) 18 | placeholder: >- 19 | Paste header here 20 | render: text 21 | validations: 22 | required: true -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /cxxheaderparser/options.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Callable, Optional 3 | 4 | #: arguments are (filename, content) 5 | PreprocessorFunction = Callable[[str, Optional[str]], str] 6 | 7 | 8 | @dataclass 9 | class ParserOptions: 10 | """ 11 | Options that control parsing behaviors 12 | """ 13 | 14 | #: If true, prints out 15 | verbose: bool = False 16 | 17 | #: If true, converts a single void parameter to zero parameters 18 | convert_void_to_zero_params: bool = True 19 | 20 | #: A function that will preprocess the header before parsing. See 21 | #: :py:mod:`cxxheaderparser.preprocessor` for available preprocessors 22 | preprocessor: Optional[PreprocessorFunction] = None 23 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | Tests 2 | ===== 3 | 4 | To run the tests, install `cxxheaderparser` and `pytest`, then just run: 5 | 6 | pytest 7 | 8 | Adding new tests 9 | ---------------- 10 | 11 | There's a helper script in cxxheaderparser explicitly for generating many of the 12 | unit tests in this directory. To run it: 13 | 14 | * Create a file with your C++ content in it 15 | * Run `python -m cxxheaderparser.gentest FILENAME.h some_name` 16 | * Copy the stdout to one of these `test_*.py` files 17 | 18 | Content origin 19 | -------------- 20 | 21 | * Some are scraps of real code derived from various sources 22 | * Some were derived from the original `CppHeaderParser` tests 23 | * Some have been derived from examples found on https://en.cppreference.com, 24 | which are available under Creative Commons Attribution-Sharealike 3.0 25 | Unported License (CC-BY-SA) 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # deploy to github pages 2 | name: Build and Deploy 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | deploy: 9 | concurrency: ci-${{ github.ref }} 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 🛎️ 13 | uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Build 18 | run: | 19 | echo "__version__ = '$(git describe --tags)'" > cxxheaderparser/version.py 20 | 21 | mkdir build 22 | cp -r cxxheaderparser build 23 | 24 | 25 | - name: Deploy 🚀 26 | uses: JamesIves/github-pages-deploy-action@v4.3.3 27 | with: 28 | branch: gh-pages 29 | folder: build 30 | clean: true 31 | clean-exclude: | 32 | .nojekyll 33 | index.html 34 | _index.py -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/tools.rst: -------------------------------------------------------------------------------- 1 | Tools 2 | ===== 3 | 4 | There are a variety of command line tools provided by the cxxheaderparser 5 | project. 6 | 7 | dump tool 8 | --------- 9 | 10 | Dump data from a header to stdout 11 | 12 | .. code-block:: sh 13 | 14 | # pprint format 15 | python -m cxxheaderparser myheader.h 16 | 17 | # JSON format 18 | python -m cxxheaderparser --mode=json myheader.h 19 | 20 | # dataclasses repr format 21 | python -m cxxheaderparser --mode=repr myheader.h 22 | 23 | # dataclasses repr format (formatted with black) 24 | python -m cxxheaderparser --mode=brepr myheader.h 25 | 26 | Anything more than that and you should use the python API, start with the 27 | :ref:`simple API ` first. 28 | 29 | test generator 30 | -------------- 31 | 32 | To generate a unit test for cxxheaderparser: 33 | 34 | * Put the C++ header content in a file 35 | * Run the following: 36 | 37 | .. code-block:: sh 38 | 39 | python -m cxxheaderparser.gentest FILENAME.h TESTNAME 40 | 41 | You can copy/paste the stdout to one of the test files in the tests directory. -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. cxxheaderparser documentation master file, created by 2 | sphinx-quickstart on Thu Dec 31 00:46:02 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | cxxheaderparser 7 | =============== 8 | 9 | A pure python C++ header parser that parses C++ headers in a mildly naive 10 | manner that allows it to handle many C++ constructs, including many modern 11 | (C++11 and beyond) features. 12 | 13 | .. warning:: cxxheaderparser intentionally does not use a C preprocessor by 14 | default. If you are parsing code with macros in it, you need to 15 | provide a preprocessor function in :py:class:`.ParserOptions`. 16 | 17 | .. seealso:: :py:attr:`cxxheaderparser.options.ParserOptions.preprocessor` 18 | 19 | .. _pcpp: https://github.com/ned14/pcpp 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | :caption: Contents: 24 | 25 | tools 26 | simple 27 | custom 28 | types 29 | 30 | 31 | 32 | Indices and tables 33 | ================== 34 | 35 | * :ref:`genindex` 36 | * :ref:`modindex` 37 | * :ref:`search` 38 | 39 | -------------------------------------------------------------------------------- /docs/custom.rst: -------------------------------------------------------------------------------- 1 | Custom parsing 2 | ============== 3 | 4 | For many users, the data provided by the simple API is enough. In some advanced 5 | cases you may find it necessary to use this more customizable parsing mechanism. 6 | 7 | First, define a visitor that implements the :class:`CxxVisitor` protocol. Then 8 | you can create an instance of it and pass it to the :class:`CxxParser`. 9 | 10 | .. code-block:: python 11 | 12 | visitor = MyVisitor() 13 | parser = CxxParser(filename, content, visitor) 14 | parser.parse() 15 | 16 | # do something with the data collected by the visitor 17 | 18 | Your visitor should do something with the data as the various callbacks are 19 | called. See the :class:`SimpleCxxVisitor` for inspiration. 20 | 21 | API 22 | --- 23 | 24 | .. automodule:: cxxheaderparser.parser 25 | :members: 26 | :undoc-members: 27 | 28 | .. automodule:: cxxheaderparser.visitor 29 | :members: 30 | :undoc-members: 31 | 32 | Parser state 33 | ------------ 34 | 35 | .. automodule:: cxxheaderparser.parserstate 36 | :members: 37 | :undoc-members: 38 | 39 | Preprocessor 40 | ------------ 41 | 42 | .. automodule:: cxxheaderparser.preprocessor 43 | :members: 44 | :undoc-members: 45 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "hatch-vcs"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "cxxheaderparser" 7 | dynamic = ["version"] 8 | description = "Modern C++ header parser" 9 | readme = "README.md" 10 | requires-python = ">=3.6" 11 | license = "BSD-3-Clause" 12 | license-files = ["LICENSE.txt"] 13 | authors = [ 14 | {name = "Dustin Spicuzza", email = "robotpy@googlegroups.com"}, 15 | ] 16 | maintainers = [ 17 | { name = "RobotPy Development Team", email = "robotpy@googlegroups.com" }, 18 | ] 19 | classifiers = [ 20 | "Development Status :: 4 - Beta", 21 | "Operating System :: OS Independent", 22 | "Programming Language :: Python", 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: C++", 25 | "License :: OSI Approved :: BSD License", 26 | "Intended Audience :: Developers", 27 | "Topic :: Software Development", 28 | "Topic :: Software Development :: Code Generators", 29 | "Topic :: Software Development :: Compilers", 30 | ] 31 | dependencies = [ 32 | "dataclasses; python_version < '3.7'", 33 | ] 34 | 35 | [project.optional-dependencies] 36 | pcpp = ["pcpp~=1.30"] 37 | 38 | [project.urls] 39 | "Source code" = "https://github.com/robotpy/cxxheaderparser" 40 | 41 | [tool.hatch.version] 42 | source = "vcs" 43 | 44 | [tool.hatch.build.targets.sdist.hooks.vcs] 45 | version-file = "cxxheaderparser/version.py" 46 | 47 | [tool.hatch.build.targets.sdist] 48 | packages = ["cxxheaderparser"] 49 | 50 | [tool.black] 51 | target-version = ["py36"] 52 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | import os 10 | import pkg_resources 11 | 12 | # -- Project information ----------------------------------------------------- 13 | 14 | project = "cxxheaderparser" 15 | copyright = "2020-2023, Dustin Spicuzza" 16 | author = "Dustin Spicuzza" 17 | 18 | # The full version, including alpha/beta/rc tags 19 | release = pkg_resources.get_distribution("cxxheaderparser").version 20 | 21 | 22 | # -- General configuration --------------------------------------------------- 23 | 24 | # Add any Sphinx extension module names here, as strings. They can be 25 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 26 | # ones. 27 | extensions = ["sphinx.ext.autodoc", "sphinx_autodoc_typehints", "sphinx_rtd_theme"] 28 | 29 | # Add any paths that contain templates here, relative to this directory. 30 | templates_path = ["_templates"] 31 | 32 | # List of patterns, relative to source directory, that match files and 33 | # directories to ignore when looking for source files. 34 | # This pattern also affects html_static_path and html_extra_path. 35 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 36 | 37 | 38 | # -- Options for HTML output ------------------------------------------------- 39 | 40 | # The theme to use for HTML and HTML Help pages. See the documentation for 41 | # a list of builtin themes. 42 | 43 | html_theme = "sphinx_rtd_theme" 44 | 45 | always_document_param_types = True 46 | -------------------------------------------------------------------------------- /tests/test_tokfmt.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from cxxheaderparser.lexer import PlyLexer, LexerTokenStream 4 | from cxxheaderparser.tokfmt import tokfmt 5 | from cxxheaderparser.types import Token 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "instr", 10 | [ 11 | "int", 12 | "unsigned int", 13 | "::uint8_t", 14 | "void *", 15 | "void * *", 16 | "const char *", 17 | "const char[]", 18 | "void * (*)()", 19 | "void (*)(void * buf, int buflen)", 20 | "void (* fnType)(void * buf, int buflen)", 21 | "TypeName& x", 22 | "vector&", 23 | "std::vector *", 24 | "Alpha::Omega", 25 | "Convoluted::Nested::Mixin", 26 | "std::function", 27 | "std::shared_ptr>", 28 | "tr1::shared_ptr>", 29 | "std::map>>", 30 | "std::is_base_of::value", 31 | "const char&&", 32 | "something{1, 2, 3}", 33 | "operator-=", 34 | "operator[]", 35 | "operator*", 36 | "operator>=", 37 | ], 38 | ) 39 | def test_tokfmt(instr: str) -> None: 40 | """ 41 | Each input string is exactly what the output of tokfmt should be 42 | """ 43 | toks = [] 44 | lexer = PlyLexer("") 45 | lexer.input(instr) 46 | 47 | while True: 48 | tok = lexer.token() 49 | if not tok: 50 | break 51 | 52 | if tok.type not in LexerTokenStream._discard_types: 53 | toks.append(Token(tok.value, tok.type)) 54 | 55 | assert tokfmt(toks) == instr 56 | -------------------------------------------------------------------------------- /tests/test_numeric_literals.py: -------------------------------------------------------------------------------- 1 | # Note: testcases generated via `python -m cxxheaderparser.gentest` 2 | 3 | from cxxheaderparser.types import ( 4 | FundamentalSpecifier, 5 | NameSpecifier, 6 | PQName, 7 | Token, 8 | Type, 9 | Value, 10 | Variable, 11 | ) 12 | from cxxheaderparser.simple import ( 13 | Include, 14 | NamespaceScope, 15 | Pragma, 16 | parse_string, 17 | ParsedData, 18 | ) 19 | 20 | 21 | def test_numeric_literals() -> None: 22 | content = """ 23 | #pragma once 24 | #include 25 | 26 | int test_binary = 0b01'10'01; 27 | int test_decimal = 123'456'789u; 28 | int test_octal = 012'42'11l; 29 | """ 30 | data = parse_string(content, cleandoc=True) 31 | 32 | assert data == ParsedData( 33 | namespace=NamespaceScope( 34 | variables=[ 35 | Variable( 36 | name=PQName(segments=[NameSpecifier(name="test_binary")]), 37 | type=Type( 38 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 39 | ), 40 | value=Value(tokens=[Token(value="0b01'10'01")]), 41 | ), 42 | Variable( 43 | name=PQName(segments=[NameSpecifier(name="test_decimal")]), 44 | type=Type( 45 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 46 | ), 47 | value=Value(tokens=[Token(value="123'456'789u")]), 48 | ), 49 | Variable( 50 | name=PQName(segments=[NameSpecifier(name="test_octal")]), 51 | type=Type( 52 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 53 | ), 54 | value=Value(tokens=[Token(value="012'42'11l")]), 55 | ), 56 | ] 57 | ), 58 | pragmas=[Pragma(content=Value(tokens=[Token(value="once")]))], 59 | includes=[Include(filename="")], 60 | ) 61 | -------------------------------------------------------------------------------- /cxxheaderparser/tokfmt.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | import typing 3 | 4 | from .lexer import LexToken, PlyLexer, LexerTokenStream 5 | 6 | # key: token type, value: (left spacing, right spacing) 7 | _want_spacing = { 8 | "FLOAT_CONST": (2, 2), 9 | "HEX_FLOAT_CONST": (2, 2), 10 | "INT_CONST_HEX": (2, 2), 11 | "INT_CONST_BIN": (2, 2), 12 | "INT_CONST_OCT": (2, 2), 13 | "INT_CONST_DEC": (2, 2), 14 | "INT_CONST_CHAR": (2, 2), 15 | "NAME": (2, 2), 16 | "CHAR_CONST": (2, 2), 17 | "WCHAR_CONST": (2, 2), 18 | "U8CHAR_CONST": (2, 2), 19 | "U16CHAR_CONST": (2, 2), 20 | "U32CHAR_CONST": (2, 2), 21 | "STRING_LITERAL": (2, 2), 22 | "WSTRING_LITERAL": (2, 2), 23 | "U8STRING_LITERAL": (2, 2), 24 | "U16STRING_LITERAL": (2, 2), 25 | "U32STRING_LITERAL": (2, 2), 26 | "ELLIPSIS": (2, 2), 27 | ">": (0, 2), 28 | ")": (0, 1), 29 | "(": (1, 0), 30 | ",": (0, 3), 31 | "*": (1, 2), 32 | "&": (0, 2), 33 | } 34 | 35 | _want_spacing.update(dict.fromkeys(PlyLexer.keywords, (2, 2))) 36 | 37 | 38 | @dataclass 39 | class Token: 40 | """ 41 | In an ideal world, this Token class would not be exposed via the user 42 | visible API. Unfortunately, getting to that point would take a significant 43 | amount of effort. 44 | 45 | It is not expected that these will change, but they might. 46 | 47 | At the moment, the only supported use of Token objects are in conjunction 48 | with the ``tokfmt`` function. As this library matures, we'll try to clarify 49 | the expectations around these. File an issue on github if you have ideas! 50 | """ 51 | 52 | #: Raw value of the token 53 | value: str 54 | 55 | #: Lex type of the token 56 | type: str = field(repr=False, compare=False, default="") 57 | 58 | 59 | def tokfmt(toks: typing.List[Token]) -> str: 60 | """ 61 | Helper function that takes a list of tokens and converts them to a string 62 | """ 63 | last = 0 64 | vals = [] 65 | default = (0, 0) 66 | ws = _want_spacing 67 | 68 | for tok in toks: 69 | value = tok.value 70 | # special case 71 | if value == "operator": 72 | l, r = 2, 0 73 | else: 74 | l, r = ws.get(tok.type, default) 75 | if l + last >= 3: 76 | vals.append(" ") 77 | 78 | last = r 79 | vals.append(value) 80 | 81 | return "".join(vals) 82 | 83 | 84 | if __name__ == "__main__": # pragma: no cover 85 | import argparse 86 | 87 | parser = argparse.ArgumentParser() 88 | parser.add_argument("header") 89 | args = parser.parse_args() 90 | 91 | filename: str = args.header 92 | with open(filename) as fp: 93 | lexer = LexerTokenStream(filename, fp.read()) 94 | 95 | toks: typing.List[Token] = [] 96 | while True: 97 | tok = lexer.token_eof_ok() 98 | if not tok: 99 | break 100 | if tok.type == ";": 101 | print(toks) 102 | print(tokfmt(toks)) 103 | toks = [] 104 | else: 105 | toks.append(Token(tok.value, tok.type)) 106 | 107 | print(toks) 108 | print(tokfmt(toks)) 109 | -------------------------------------------------------------------------------- /cxxheaderparser/dump.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import dataclasses 3 | import json 4 | import pathlib 5 | import pprint 6 | import subprocess 7 | import sys 8 | 9 | from .options import ParserOptions 10 | from .simple import parse_file 11 | 12 | 13 | def dumpmain() -> None: 14 | parser = argparse.ArgumentParser() 15 | parser.add_argument("header") 16 | parser.add_argument( 17 | "-w", 18 | "--width", 19 | default=80, 20 | type=int, 21 | help="Width of output when in pprint mode", 22 | ) 23 | parser.add_argument("-v", "--verbose", default=False, action="store_true") 24 | parser.add_argument( 25 | "--mode", 26 | choices=["json", "pprint", "repr", "brepr", "pponly"], 27 | default="pprint", 28 | ) 29 | parser.add_argument( 30 | "--pcpp", default=False, action="store_true", help="Use pcpp preprocessor" 31 | ) 32 | parser.add_argument( 33 | "--gcc", default=False, action="store_true", help="Use GCC as preprocessor" 34 | ) 35 | parser.add_argument( 36 | "--depfile", 37 | default=None, 38 | type=pathlib.Path, 39 | help="Generate a depfile (requires preprocessor)", 40 | ) 41 | parser.add_argument( 42 | "--deptarget", default=[], action="append", help="depfile target" 43 | ) 44 | parser.add_argument( 45 | "--encoding", default=None, help="Use this encoding to open the file" 46 | ) 47 | 48 | args = parser.parse_args() 49 | 50 | pp_kwargs = dict(encoding=args.encoding) 51 | 52 | if args.depfile: 53 | if not (args.pcpp or args.gcc): 54 | parser.error("--depfile requires either --pcpp or --gcc") 55 | 56 | pp_kwargs["depfile"] = args.depfile 57 | pp_kwargs["deptarget"] = args.deptarget 58 | 59 | preprocessor = None 60 | if args.gcc: 61 | from .preprocessor import make_gcc_preprocessor 62 | 63 | preprocessor = make_gcc_preprocessor(**pp_kwargs) 64 | 65 | if args.pcpp or (args.mode == "pponly" and preprocessor is None): 66 | from .preprocessor import make_pcpp_preprocessor 67 | 68 | preprocessor = make_pcpp_preprocessor(**pp_kwargs) 69 | 70 | if args.mode == "pponly": 71 | assert preprocessor is not None 72 | with open(args.header, "r", encoding=args.encoding) as fp: 73 | pp_content = preprocessor(args.header, fp.read()) 74 | sys.stdout.write(pp_content) 75 | sys.exit(0) 76 | 77 | options = ParserOptions(verbose=args.verbose, preprocessor=preprocessor) 78 | data = parse_file(args.header, encoding=args.encoding, options=options) 79 | 80 | if args.mode == "pprint": 81 | ddata = dataclasses.asdict(data) 82 | pprint.pprint(ddata, width=args.width, compact=True) 83 | 84 | elif args.mode == "json": 85 | ddata = dataclasses.asdict(data) 86 | json.dump(ddata, sys.stdout, indent=2) 87 | 88 | elif args.mode == "brepr": 89 | stmt = repr(data) 90 | stmt = subprocess.check_output( 91 | ["black", "-", "-q"], input=stmt.encode("utf-8") 92 | ).decode("utf-8") 93 | 94 | print(stmt) 95 | 96 | elif args.mode == "repr": 97 | print(data) 98 | 99 | else: 100 | parser.error("Invalid mode") 101 | -------------------------------------------------------------------------------- /.github/workflows/dist.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: dist 3 | 4 | on: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | tags: 10 | - '*' 11 | 12 | concurrency: 13 | group: ${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | check: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: psf/black@stable 22 | 23 | check-mypy: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v3 27 | # - uses: jpetrucciani/mypy-check@0.930 28 | # .. can't use that because we need to install pytest 29 | - uses: actions/setup-python@v4 30 | with: 31 | python-version: 3.8 32 | - name: Install requirements 33 | run: | 34 | pip --disable-pip-version-check install mypy pytest pcpp 35 | - name: Run mypy 36 | run: | 37 | mypy . 38 | 39 | check-doc: 40 | runs-on: ubuntu-22.04 41 | 42 | steps: 43 | - uses: actions/checkout@v3 44 | with: 45 | submodules: recursive 46 | fetch-depth: 0 47 | 48 | - uses: actions/setup-python@v4 49 | with: 50 | python-version: 3.8 51 | - name: Sphinx 52 | run: | 53 | pip --disable-pip-version-check install -e . 54 | pip --disable-pip-version-check install -r docs/requirements.txt 55 | cd docs && make clean html SPHINXOPTS="-W --keep-going" 56 | 57 | # 58 | # Build a wheel 59 | # 60 | 61 | build: 62 | runs-on: ubuntu-22.04 63 | steps: 64 | - uses: actions/checkout@v3 65 | with: 66 | submodules: recursive 67 | fetch-depth: 0 68 | 69 | - uses: actions/setup-python@v4 70 | with: 71 | python-version: 3.8 72 | 73 | - run: pipx run build 74 | 75 | - name: Upload build artifacts 76 | uses: actions/upload-artifact@v4 77 | with: 78 | name: dist 79 | path: dist 80 | 81 | test: 82 | needs: [build] 83 | runs-on: ${{ matrix.os }} 84 | strategy: 85 | matrix: 86 | os: 87 | - windows-latest 88 | - macos-15-intel 89 | - ubuntu-22.04 90 | python_version: 91 | - 3.7 92 | - 3.8 93 | - 3.9 94 | - "3.10" 95 | - "3.11" 96 | - "3.12" 97 | - "3.13" 98 | architecture: [x86, x64] 99 | exclude: 100 | - os: macos-15-intel 101 | architecture: x86 102 | - os: ubuntu-22.04 103 | architecture: x86 104 | 105 | steps: 106 | - uses: actions/checkout@v3 107 | with: 108 | submodules: recursive 109 | fetch-depth: 0 110 | 111 | - uses: actions/setup-python@v4 112 | with: 113 | python-version: ${{ matrix.python_version }} 114 | architecture: ${{ matrix.architecture }} 115 | 116 | - name: Download build artifacts 117 | uses: actions/download-artifact@v4 118 | with: 119 | name: dist 120 | path: dist 121 | 122 | - name: Install test dependencies 123 | run: python -m pip --disable-pip-version-check install -r tests/requirements.txt 124 | 125 | - name: Setup MSVC compiler 126 | uses: ilammy/msvc-dev-cmd@v1 127 | if: matrix.os == 'windows-latest' 128 | 129 | - name: Test wheel 130 | shell: bash 131 | run: | 132 | cd dist 133 | python -m pip --disable-pip-version-check install *.whl 134 | cd ../tests 135 | python -m pytest 136 | 137 | publish: 138 | runs-on: ubuntu-latest 139 | needs: [check, check-mypy, check-doc, test] 140 | permissions: 141 | id-token: write 142 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 143 | 144 | steps: 145 | - name: Download build artifacts 146 | uses: actions/download-artifact@v4 147 | with: 148 | name: dist 149 | path: dist 150 | 151 | - name: Publish to PyPI 152 | uses: pypa/gh-action-pypi-publish@release/v1 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cxxheaderparser 2 | =============== 3 | 4 | A pure python C++ header parser that parses C++ headers in a mildly naive 5 | manner that allows it to handle many C++ constructs, including many modern 6 | (C++11 and beyond) features. 7 | 8 | This is a complete rewrite of the `CppHeaderParser` library. `CppHeaderParser` 9 | is really useful for some tasks, but it's implementation is a truly terrible 10 | ugly hack built on top of other terrible hacks. This rewrite tries to learn 11 | from `CppHeaderParser` and leave its ugly baggage behind. 12 | 13 | Goals: 14 | 15 | * Parse syntatically valid C++ and provide a useful (and documented!) pure 16 | python API to work with the parsed data 17 | * Process incomplete headers (doesn't need to process includes) 18 | * Provide enough information for binding generators to wrap C++ code 19 | * Handle common C++ features, but it may struggle with obscure or overly 20 | complex things (feel free to make a PR to fix it!) 21 | 22 | Non-goals: 23 | 24 | * **Does not produce a full AST**, use Clang if you need that 25 | * **Not intended to validate C++**, which means this will not reject all 26 | invalid C++ headers! Use a compiler if you need that 27 | * **Parser requires a C++ preprocessor**. If you are parsing 28 | headers that contain macros, you should preprocess your code using the 29 | excellent pure python preprocessor [pcpp](https://github.com/ned14/pcpp) 30 | or your favorite compiler 31 | * We have implemented support for PCPP, GCC, and MSVC -- but it is not enabled 32 | by default. See the `cxxheaderparser.preprocessor` module for how to enable it. 33 | * Probably won't be able to parse most IOCCC entries 34 | 35 | There are two APIs available: 36 | 37 | * A visitor-style interface to build up your own custom data structures 38 | * A simple visitor that stores everything in a giant data structure 39 | 40 | Live Demo 41 | --------- 42 | 43 | A pyodide-powered interactive demo is at https://robotpy.github.io/cxxheaderparser/ 44 | 45 | Documentation 46 | ------------- 47 | 48 | Documentation can be found at https://cxxheaderparser.readthedocs.io 49 | 50 | Install 51 | ------- 52 | 53 | Requires Python 3.6+, no non-stdlib dependencies if using Python 3.7+. 54 | 55 | ``` 56 | pip install cxxheaderparser 57 | ``` 58 | 59 | Usage 60 | ----- 61 | 62 | To see a dump of the data parsed from a header: 63 | 64 | ``` 65 | # pprint format 66 | python -m cxxheaderparser myheader.h 67 | 68 | # JSON format 69 | python -m cxxheaderparser --mode=json myheader.h 70 | 71 | # dataclasses repr format 72 | python -m cxxheaderparser --mode=repr myheader.h 73 | 74 | # dataclasses repr format (formatted with black) 75 | python -m cxxheaderparser --mode=brepr myheader.h 76 | ``` 77 | 78 | See the documentation for anything more complex. 79 | 80 | Bugs 81 | ---- 82 | 83 | This should handle even complex C++ code with few problems, but there are 84 | almost certainly weird edge cases that it doesn't handle. Additionally, 85 | not all C++17/20 constructs are supported yet (but contributions welcome!). 86 | 87 | If you find an bug, we encourage you to submit a pull request! New 88 | changes will only be accepted if there are tests to cover the change you 89 | made (and if they don’t break existing tests). 90 | 91 | It's really easy to add new tests, see the [README in the tests directory](tests/README.md). 92 | 93 | Author 94 | ------ 95 | 96 | cxxheaderparser was created by Dustin Spicuzza 97 | 98 | Credit 99 | ------ 100 | 101 | * Partially derived from and inspired by the `CppHeaderParser` project 102 | originally developed by Jashua Cloutier 103 | * An embedded version of PLY is used for lexing tokens 104 | * Portions of the lexer grammar and other ideas were derived from pycparser 105 | * The source code is liberally sprinkled with comments containing C++ parsing 106 | grammar mostly derived from the [Hyperlinked C++ BNF Grammar](https://www.nongnu.org/hcb/) 107 | * cppreference.com has been invaluable for understanding many of the weird 108 | quirks of C++, and some of the unit tests use examples from there 109 | * [Compiler Explorer](godbolt.org) has been invaluable for validating my 110 | understanding of C++ by allowing me to quickly type in quirky C++ 111 | constructs to see if they actually compile 112 | 113 | License 114 | ------- 115 | 116 | BSD License 117 | -------------------------------------------------------------------------------- /cxxheaderparser/parserstate.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | if typing.TYPE_CHECKING: 4 | from .visitor import CxxVisitor # pragma: nocover 5 | 6 | from .errors import CxxParseError 7 | from .lexer import LexToken, Location 8 | from .types import ClassDecl, NamespaceDecl 9 | 10 | 11 | class ParsedTypeModifiers(typing.NamedTuple): 12 | vars: typing.Dict[str, LexToken] # only found on variables 13 | both: typing.Dict[str, LexToken] # found on either variables or functions 14 | meths: typing.Dict[str, LexToken] # only found on methods 15 | 16 | def validate(self, *, var_ok: bool, meth_ok: bool, msg: str) -> None: 17 | # Almost there! Do any checks the caller asked for 18 | if not var_ok and self.vars: 19 | for tok in self.vars.values(): 20 | raise CxxParseError(f"{msg}: unexpected '{tok.value}'") 21 | 22 | if not meth_ok and self.meths: 23 | for tok in self.meths.values(): 24 | raise CxxParseError(f"{msg}: unexpected '{tok.value}'") 25 | 26 | if not meth_ok and not var_ok and self.both: 27 | for tok in self.both.values(): 28 | raise CxxParseError(f"{msg}: unexpected '{tok.value}'") 29 | 30 | 31 | #: custom user data for this state type 32 | T = typing.TypeVar("T") 33 | 34 | #: type of custom user data for a parent state 35 | PT = typing.TypeVar("PT") 36 | 37 | 38 | class BaseState(typing.Generic[T, PT]): 39 | #: Uninitialized user data available for use by visitor implementations. You 40 | #: should set this in a ``*_start`` method. 41 | user_data: T 42 | 43 | #: parent state 44 | parent: typing.Optional["State"] 45 | 46 | #: Approximate location that the parsed element was found at 47 | location: Location 48 | 49 | #: internal detail used by parser 50 | _prior_visitor: "CxxVisitor" 51 | 52 | def __init__(self, parent: typing.Optional["State"], location: Location) -> None: 53 | self.parent = parent 54 | self.location = location 55 | 56 | def _finish(self, visitor: "CxxVisitor") -> None: 57 | pass 58 | 59 | 60 | class ExternBlockState(BaseState[T, PT]): 61 | parent: "NonClassBlockState" 62 | 63 | #: The linkage for this extern block 64 | linkage: str 65 | 66 | def __init__( 67 | self, parent: "NonClassBlockState", location: Location, linkage: str 68 | ) -> None: 69 | super().__init__(parent, location) 70 | self.linkage = linkage 71 | 72 | def _finish(self, visitor: "CxxVisitor") -> None: 73 | visitor.on_extern_block_end(self) 74 | 75 | 76 | class NamespaceBlockState(BaseState[T, PT]): 77 | parent: "NonClassBlockState" 78 | 79 | #: The incremental namespace for this block 80 | namespace: NamespaceDecl 81 | 82 | def __init__( 83 | self, 84 | parent: typing.Optional["NonClassBlockState"], 85 | location: Location, 86 | namespace: NamespaceDecl, 87 | ) -> None: 88 | super().__init__(parent, location) 89 | self.namespace = namespace 90 | 91 | def _finish(self, visitor: "CxxVisitor") -> None: 92 | visitor.on_namespace_end(self) 93 | 94 | 95 | class ClassBlockState(BaseState[T, PT]): 96 | parent: "State" 97 | 98 | #: class decl block being processed 99 | class_decl: ClassDecl 100 | 101 | #: Current access level for items encountered 102 | access: str 103 | 104 | #: Currently parsing as a typedef 105 | typedef: bool 106 | 107 | #: modifiers to apply to following variables 108 | mods: ParsedTypeModifiers 109 | 110 | def __init__( 111 | self, 112 | parent: typing.Optional["State"], 113 | location: Location, 114 | class_decl: ClassDecl, 115 | access: str, 116 | typedef: bool, 117 | mods: ParsedTypeModifiers, 118 | ) -> None: 119 | super().__init__(parent, location) 120 | self.class_decl = class_decl 121 | self.access = access 122 | self.typedef = typedef 123 | self.mods = mods 124 | 125 | def _set_access(self, access: str) -> None: 126 | self.access = access 127 | 128 | def _finish(self, visitor: "CxxVisitor") -> None: 129 | visitor.on_class_end(self) 130 | 131 | 132 | State = typing.Union[ 133 | NamespaceBlockState[T, PT], ExternBlockState[T, PT], ClassBlockState[T, PT] 134 | ] 135 | NonClassBlockState = typing.Union[ExternBlockState[T, PT], NamespaceBlockState[T, PT]] 136 | -------------------------------------------------------------------------------- /cxxheaderparser/gentest.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import dataclasses 3 | import inspect 4 | import subprocess 5 | import typing 6 | 7 | from .errors import CxxParseError 8 | from .preprocessor import make_pcpp_preprocessor 9 | from .options import ParserOptions 10 | from .simple import parse_string, ParsedData 11 | 12 | 13 | def nondefault_repr(data: ParsedData) -> str: 14 | """ 15 | Similar to the default dataclass repr, but exclude any 16 | default parameters or parameters with compare=False 17 | """ 18 | 19 | is_dataclass = dataclasses.is_dataclass 20 | get_fields = dataclasses.fields 21 | MISSING = dataclasses.MISSING 22 | 23 | def _inner_repr(o: typing.Any) -> str: 24 | if is_dataclass(o): 25 | vals = [] 26 | for f in get_fields(o): 27 | if f.repr and f.compare: 28 | v = getattr(o, f.name) 29 | if f.default_factory is not MISSING: 30 | default = f.default_factory() 31 | else: 32 | default = f.default 33 | 34 | if v != default: 35 | vals.append(f"{f.name}={_inner_repr(v)}") 36 | 37 | return f"{o.__class__.__qualname__ }({', '.join(vals)})" 38 | 39 | elif isinstance(o, list): 40 | return f"[{','.join(_inner_repr(l) for l in o)}]" 41 | elif isinstance(o, dict): 42 | vals = [] 43 | for k, v in o.items(): 44 | vals.append(f'"{k}": {_inner_repr(v)}') 45 | return "{" + ",".join(vals) + "}" 46 | else: 47 | return repr(o) 48 | 49 | return _inner_repr(data) 50 | 51 | 52 | def gentest( 53 | infile: str, name: str, outfile: str, verbose: bool, fail: bool, pcpp: bool 54 | ) -> None: 55 | # Goal is to allow making a unit test as easy as running this dumper 56 | # on a file and copy/pasting this into a test 57 | 58 | with open(infile, "r") as fp: 59 | content = fp.read() 60 | 61 | maybe_options = "" 62 | popt = "" 63 | 64 | options = ParserOptions(verbose=verbose) 65 | if pcpp: 66 | options.preprocessor = make_pcpp_preprocessor() 67 | maybe_options = "options = ParserOptions(preprocessor=make_pcpp_preprocessor())" 68 | popt = ", options=options" 69 | 70 | try: 71 | data = parse_string(content, options=options) 72 | if fail: 73 | raise ValueError("did not fail") 74 | except CxxParseError: 75 | if not fail: 76 | raise 77 | # do it again, but strip the content so the error message matches 78 | try: 79 | parse_string(content.strip(), options=options) 80 | except CxxParseError as e2: 81 | err = str(e2) 82 | 83 | if not fail: 84 | stmt = nondefault_repr(data) 85 | stmt = f""" 86 | {maybe_options} 87 | data = parse_string(content, cleandoc=True{popt}) 88 | 89 | assert data == {stmt} 90 | """ 91 | else: 92 | stmt = f""" 93 | {maybe_options} 94 | err = {repr(err)} 95 | with pytest.raises(CxxParseError, match=re.escape(err)): 96 | parse_string(content, cleandoc=True{popt}) 97 | """ 98 | 99 | content = ("\n" + content.strip()).replace("\n", "\n ") 100 | content = "\n".join(l.rstrip() for l in content.splitlines()) 101 | 102 | stmt = inspect.cleandoc( 103 | f''' 104 | def test_{name}() -> None: 105 | content = """{content} 106 | """ 107 | {stmt.strip()} 108 | ''' 109 | ) 110 | 111 | # format it with black 112 | stmt = subprocess.check_output( 113 | ["black", "-", "-q"], input=stmt.encode("utf-8") 114 | ).decode("utf-8") 115 | 116 | if outfile == "-": 117 | print(stmt) 118 | else: 119 | with open(outfile, "w") as fp: 120 | fp.write(stmt) 121 | 122 | 123 | if __name__ == "__main__": 124 | parser = argparse.ArgumentParser() 125 | parser.add_argument("header") 126 | parser.add_argument("name", nargs="?", default="TODO") 127 | parser.add_argument("--pcpp", default=False, action="store_true") 128 | parser.add_argument("-v", "--verbose", default=False, action="store_true") 129 | parser.add_argument("-o", "--output", default="-") 130 | parser.add_argument( 131 | "-x", "--fail", default=False, action="store_true", help="Expect failure" 132 | ) 133 | args = parser.parse_args() 134 | 135 | gentest(args.header, args.name, args.output, args.verbose, args.fail, args.pcpp) 136 | -------------------------------------------------------------------------------- /tests/test_union.py: -------------------------------------------------------------------------------- 1 | # Note: testcases generated via `python -m cxxheaderparser.gentest` 2 | 3 | from cxxheaderparser.types import ( 4 | AnonymousName, 5 | ClassDecl, 6 | Field, 7 | FundamentalSpecifier, 8 | NameSpecifier, 9 | PQName, 10 | Type, 11 | ) 12 | from cxxheaderparser.simple import ( 13 | ClassScope, 14 | NamespaceScope, 15 | parse_string, 16 | ParsedData, 17 | ) 18 | 19 | 20 | def test_union_basic() -> None: 21 | content = """ 22 | 23 | struct HAL_Value { 24 | union { 25 | int v_int; 26 | HAL_Bool v_boolean; 27 | } data; 28 | }; 29 | """ 30 | data = parse_string(content, cleandoc=True) 31 | 32 | assert data == ParsedData( 33 | namespace=NamespaceScope( 34 | classes=[ 35 | ClassScope( 36 | class_decl=ClassDecl( 37 | typename=PQName( 38 | segments=[NameSpecifier(name="HAL_Value")], 39 | classkey="struct", 40 | ) 41 | ), 42 | classes=[ 43 | ClassScope( 44 | class_decl=ClassDecl( 45 | typename=PQName( 46 | segments=[AnonymousName(id=1)], classkey="union" 47 | ), 48 | access="public", 49 | ), 50 | fields=[ 51 | Field( 52 | access="public", 53 | type=Type( 54 | typename=PQName( 55 | segments=[FundamentalSpecifier(name="int")] 56 | ) 57 | ), 58 | name="v_int", 59 | ), 60 | Field( 61 | access="public", 62 | type=Type( 63 | typename=PQName( 64 | segments=[NameSpecifier(name="HAL_Bool")] 65 | ) 66 | ), 67 | name="v_boolean", 68 | ), 69 | ], 70 | ) 71 | ], 72 | fields=[ 73 | Field( 74 | access="public", 75 | type=Type( 76 | typename=PQName( 77 | segments=[AnonymousName(id=1)], classkey="union" 78 | ) 79 | ), 80 | name="data", 81 | ) 82 | ], 83 | ) 84 | ] 85 | ) 86 | ) 87 | 88 | 89 | def test_union_anon_in_struct() -> None: 90 | content = """ 91 | struct Outer { 92 | union { 93 | int x; 94 | int y; 95 | }; 96 | int z; 97 | }; 98 | """ 99 | data = parse_string(content, cleandoc=True) 100 | 101 | assert data == ParsedData( 102 | namespace=NamespaceScope( 103 | classes=[ 104 | ClassScope( 105 | class_decl=ClassDecl( 106 | typename=PQName( 107 | segments=[NameSpecifier(name="Outer")], classkey="struct" 108 | ) 109 | ), 110 | classes=[ 111 | ClassScope( 112 | class_decl=ClassDecl( 113 | typename=PQName( 114 | segments=[AnonymousName(id=1)], classkey="union" 115 | ), 116 | access="public", 117 | ), 118 | fields=[ 119 | Field( 120 | access="public", 121 | type=Type( 122 | typename=PQName( 123 | segments=[FundamentalSpecifier(name="int")] 124 | ) 125 | ), 126 | name="x", 127 | ), 128 | Field( 129 | access="public", 130 | type=Type( 131 | typename=PQName( 132 | segments=[FundamentalSpecifier(name="int")] 133 | ) 134 | ), 135 | name="y", 136 | ), 137 | ], 138 | ) 139 | ], 140 | fields=[ 141 | Field( 142 | access="public", 143 | type=Type( 144 | typename=PQName( 145 | segments=[AnonymousName(id=1)], classkey="union" 146 | ) 147 | ), 148 | ), 149 | Field( 150 | access="public", 151 | type=Type( 152 | typename=PQName( 153 | segments=[FundamentalSpecifier(name="int")] 154 | ) 155 | ), 156 | name="z", 157 | ), 158 | ], 159 | ) 160 | ] 161 | ) 162 | ) 163 | -------------------------------------------------------------------------------- /tests/test_skip.py: -------------------------------------------------------------------------------- 1 | # Note: testcases generated via `python -m cxxheaderparser.gentest` 2 | # .. and modified 3 | 4 | import inspect 5 | import typing 6 | 7 | from cxxheaderparser.parser import CxxParser 8 | from cxxheaderparser.simple import ( 9 | ClassScope, 10 | NamespaceScope, 11 | ParsedData, 12 | SClassBlockState, 13 | SExternBlockState, 14 | SNamespaceBlockState, 15 | SimpleCxxVisitor, 16 | ) 17 | 18 | from cxxheaderparser.types import ( 19 | ClassDecl, 20 | Function, 21 | FundamentalSpecifier, 22 | Method, 23 | NameSpecifier, 24 | PQName, 25 | Type, 26 | ) 27 | 28 | # 29 | # ensure extern block is skipped 30 | # 31 | 32 | 33 | class SkipExtern(SimpleCxxVisitor): 34 | def on_extern_block_start(self, state: SExternBlockState) -> typing.Optional[bool]: 35 | return False 36 | 37 | 38 | def test_skip_extern(): 39 | content = """ 40 | void fn1(); 41 | 42 | extern "C" { 43 | void fn2(); 44 | } 45 | 46 | void fn3(); 47 | """ 48 | 49 | v = SkipExtern() 50 | parser = CxxParser("", inspect.cleandoc(content), v) 51 | parser.parse() 52 | data = v.data 53 | 54 | assert data == ParsedData( 55 | namespace=NamespaceScope( 56 | functions=[ 57 | Function( 58 | return_type=Type( 59 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 60 | ), 61 | name=PQName(segments=[NameSpecifier(name="fn1")]), 62 | parameters=[], 63 | ), 64 | Function( 65 | return_type=Type( 66 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 67 | ), 68 | name=PQName(segments=[NameSpecifier(name="fn3")]), 69 | parameters=[], 70 | ), 71 | ] 72 | ) 73 | ) 74 | 75 | 76 | # 77 | # ensure class block is skipped 78 | # 79 | 80 | 81 | class SkipClass(SimpleCxxVisitor): 82 | def on_class_start(self, state: SClassBlockState) -> typing.Optional[bool]: 83 | if getattr(state.class_decl.typename.segments[0], "name", None) == "Skip": 84 | return False 85 | return super().on_class_start(state) 86 | 87 | 88 | def test_skip_class() -> None: 89 | content = """ 90 | void fn1(); 91 | 92 | class Skip { 93 | void fn2(); 94 | }; 95 | 96 | class Yup { 97 | void fn3(); 98 | }; 99 | 100 | void fn5(); 101 | """ 102 | v = SkipClass() 103 | parser = CxxParser("", inspect.cleandoc(content), v) 104 | parser.parse() 105 | data = v.data 106 | 107 | assert data == ParsedData( 108 | namespace=NamespaceScope( 109 | classes=[ 110 | ClassScope( 111 | class_decl=ClassDecl( 112 | typename=PQName( 113 | segments=[NameSpecifier(name="Yup")], classkey="class" 114 | ) 115 | ), 116 | methods=[ 117 | Method( 118 | return_type=Type( 119 | typename=PQName( 120 | segments=[FundamentalSpecifier(name="void")] 121 | ) 122 | ), 123 | name=PQName(segments=[NameSpecifier(name="fn3")]), 124 | parameters=[], 125 | access="private", 126 | ) 127 | ], 128 | ), 129 | ], 130 | functions=[ 131 | Function( 132 | return_type=Type( 133 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 134 | ), 135 | name=PQName(segments=[NameSpecifier(name="fn1")]), 136 | parameters=[], 137 | ), 138 | Function( 139 | return_type=Type( 140 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 141 | ), 142 | name=PQName(segments=[NameSpecifier(name="fn5")]), 143 | parameters=[], 144 | ), 145 | ], 146 | ) 147 | ) 148 | 149 | 150 | # 151 | # ensure namespace 'skip' is skipped 152 | # 153 | 154 | 155 | class SkipNamespace(SimpleCxxVisitor): 156 | def on_namespace_start(self, state: SNamespaceBlockState) -> typing.Optional[bool]: 157 | if "skip" in state.namespace.names[0]: 158 | return False 159 | 160 | return super().on_namespace_start(state) 161 | 162 | 163 | def test_skip_namespace(): 164 | content = """ 165 | void fn1(); 166 | 167 | namespace skip { 168 | void fn2(); 169 | 170 | namespace thistoo { 171 | void fn3(); 172 | } 173 | } 174 | 175 | namespace ok { 176 | void fn4(); 177 | } 178 | 179 | void fn5(); 180 | """ 181 | v = SkipNamespace() 182 | parser = CxxParser("", inspect.cleandoc(content), v) 183 | parser.parse() 184 | data = v.data 185 | 186 | assert data == ParsedData( 187 | namespace=NamespaceScope( 188 | functions=[ 189 | Function( 190 | return_type=Type( 191 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 192 | ), 193 | name=PQName(segments=[NameSpecifier(name="fn1")]), 194 | parameters=[], 195 | ), 196 | Function( 197 | return_type=Type( 198 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 199 | ), 200 | name=PQName(segments=[NameSpecifier(name="fn5")]), 201 | parameters=[], 202 | ), 203 | ], 204 | namespaces={ 205 | "ok": NamespaceScope( 206 | name="ok", 207 | functions=[ 208 | Function( 209 | return_type=Type( 210 | typename=PQName( 211 | segments=[FundamentalSpecifier(name="void")] 212 | ) 213 | ), 214 | name=PQName(segments=[NameSpecifier(name="fn4")]), 215 | parameters=[], 216 | ) 217 | ], 218 | ), 219 | }, 220 | ) 221 | ) 222 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | cxxheaderparser license: 2 | 3 | Copyright (c) 2020-2022 Dustin Spicuzza 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its contributors 17 | may be used to endorse or promote products derived from this software 18 | without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | ----------------------------------------------------------------------------- 32 | 33 | CppHeaderParser license: 34 | 35 | Copyright (C) 2011, Jashua R. Cloutier 36 | All rights reserved. 37 | 38 | Redistribution and use in source and binary forms, with or without 39 | modification, are permitted provided that the following conditions 40 | are met: 41 | 42 | * Redistributions of source code must retain the above copyright 43 | notice, this list of conditions and the following disclaimer. 44 | 45 | * Redistributions in binary form must reproduce the above copyright 46 | notice, this list of conditions and the following disclaimer in 47 | the documentation and/or other materials provided with the 48 | distribution. 49 | 50 | * Neither the name of Jashua R. Cloutier nor the names of its 51 | contributors may be used to endorse or promote products derived from 52 | this software without specific prior written permission. Stories, 53 | blog entries etc making reference to this project may mention the 54 | name Jashua R. Cloutier in terms of project originator/creator etc. 55 | 56 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 57 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 58 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 59 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 60 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 61 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 62 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 63 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 64 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 65 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 66 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 67 | POSSIBILITY OF SUCH DAMAGE. 68 | 69 | ----------------------------------------------------------------------------- 70 | 71 | PLY license: 72 | 73 | Copyright (C) 2001-2020 74 | David M. Beazley (Dabeaz LLC) 75 | All rights reserved. 76 | 77 | Latest version: https://github.com/dabeaz/ply 78 | 79 | Redistribution and use in source and binary forms, with or without 80 | modification, are permitted provided that the following conditions are 81 | met: 82 | 83 | * Redistributions of source code must retain the above copyright notice, 84 | this list of conditions and the following disclaimer. 85 | * Redistributions in binary form must reproduce the above copyright notice, 86 | this list of conditions and the following disclaimer in the documentation 87 | and/or other materials provided with the distribution. 88 | * Neither the name of David Beazley or Dabeaz LLC may be used to 89 | endorse or promote products derived from this software without 90 | specific prior written permission. 91 | 92 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 93 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 94 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 95 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 96 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 97 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 98 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 99 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 100 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 101 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 102 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 103 | 104 | ----------------------------------------------------------------------------- 105 | 106 | pycparser -- A C parser in Python 107 | 108 | Copyright (c) 2008-2022, Eli Bendersky 109 | All rights reserved. 110 | 111 | Redistribution and use in source and binary forms, with or without modification, 112 | are permitted provided that the following conditions are met: 113 | 114 | * Redistributions of source code must retain the above copyright notice, this 115 | list of conditions and the following disclaimer. 116 | * Redistributions in binary form must reproduce the above copyright notice, 117 | this list of conditions and the following disclaimer in the documentation 118 | and/or other materials provided with the distribution. 119 | * Neither the name of the copyright holder nor the names of its contributors may 120 | be used to endorse or promote products derived from this software without 121 | specific prior written permission. 122 | 123 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 124 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 125 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 126 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 127 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 128 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 129 | GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 130 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 131 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 132 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 133 | -------------------------------------------------------------------------------- /tests/test_namespaces.py: -------------------------------------------------------------------------------- 1 | # Note: testcases generated via `python -m cxxheaderparser.gentest` 2 | 3 | from cxxheaderparser.errors import CxxParseError 4 | from cxxheaderparser.types import ( 5 | ForwardDecl, 6 | FundamentalSpecifier, 7 | NamespaceAlias, 8 | NameSpecifier, 9 | PQName, 10 | Token, 11 | Type, 12 | Value, 13 | Variable, 14 | ) 15 | from cxxheaderparser.simple import ( 16 | NamespaceScope, 17 | parse_string, 18 | ParsedData, 19 | ) 20 | 21 | import pytest 22 | import re 23 | 24 | 25 | def test_dups_in_different_ns() -> None: 26 | content = """ 27 | 28 | namespace { 29 | int x = 4; 30 | } 31 | 32 | int x = 5; 33 | 34 | """ 35 | data = parse_string(content, cleandoc=True) 36 | 37 | assert data == ParsedData( 38 | namespace=NamespaceScope( 39 | variables=[ 40 | Variable( 41 | name=PQName(segments=[NameSpecifier(name="x")]), 42 | type=Type( 43 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 44 | ), 45 | value=Value(tokens=[Token(value="5")]), 46 | ) 47 | ], 48 | namespaces={ 49 | "": NamespaceScope( 50 | variables=[ 51 | Variable( 52 | name=PQName(segments=[NameSpecifier(name="x")]), 53 | type=Type( 54 | typename=PQName( 55 | segments=[FundamentalSpecifier(name="int")] 56 | ) 57 | ), 58 | value=Value(tokens=[Token(value="4")]), 59 | ) 60 | ] 61 | ) 62 | }, 63 | ) 64 | ) 65 | 66 | 67 | def test_correct_ns() -> None: 68 | content = """ 69 | namespace a::b::c { 70 | int i1; 71 | } 72 | 73 | namespace a { 74 | namespace b { 75 | namespace c { 76 | int i2; 77 | } 78 | } 79 | } 80 | """ 81 | data = parse_string(content, cleandoc=True) 82 | 83 | assert data == ParsedData( 84 | namespace=NamespaceScope( 85 | namespaces={ 86 | "a": NamespaceScope( 87 | name="a", 88 | namespaces={ 89 | "b": NamespaceScope( 90 | name="b", 91 | namespaces={ 92 | "c": NamespaceScope( 93 | name="c", 94 | variables=[ 95 | Variable( 96 | name=PQName( 97 | segments=[NameSpecifier(name="i1")] 98 | ), 99 | type=Type( 100 | typename=PQName( 101 | segments=[ 102 | FundamentalSpecifier(name="int") 103 | ] 104 | ) 105 | ), 106 | ), 107 | Variable( 108 | name=PQName( 109 | segments=[NameSpecifier(name="i2")] 110 | ), 111 | type=Type( 112 | typename=PQName( 113 | segments=[ 114 | FundamentalSpecifier(name="int") 115 | ] 116 | ) 117 | ), 118 | ), 119 | ], 120 | ) 121 | }, 122 | ) 123 | }, 124 | ) 125 | } 126 | ) 127 | ) 128 | 129 | 130 | def test_inline_namespace() -> None: 131 | content = """ 132 | namespace Lib { 133 | inline namespace Lib_1 { 134 | class A; 135 | } 136 | } 137 | """ 138 | data = parse_string(content, cleandoc=True) 139 | 140 | assert data == ParsedData( 141 | namespace=NamespaceScope( 142 | namespaces={ 143 | "Lib": NamespaceScope( 144 | name="Lib", 145 | namespaces={ 146 | "Lib_1": NamespaceScope( 147 | name="Lib_1", 148 | inline=True, 149 | forward_decls=[ 150 | ForwardDecl( 151 | typename=PQName( 152 | segments=[NameSpecifier(name="A")], 153 | classkey="class", 154 | ) 155 | ) 156 | ], 157 | ) 158 | }, 159 | ) 160 | } 161 | ) 162 | ) 163 | 164 | 165 | def test_invalid_inline_namespace() -> None: 166 | content = """ 167 | inline namespace a::b {} 168 | """ 169 | err = ":1: parse error evaluating 'inline': a nested namespace definition cannot be inline" 170 | with pytest.raises(CxxParseError, match=re.escape(err)): 171 | parse_string(content, cleandoc=True) 172 | 173 | 174 | def test_ns_alias() -> None: 175 | content = """ 176 | namespace ANS = my::ns; 177 | """ 178 | data = parse_string(content, cleandoc=True) 179 | 180 | assert data == ParsedData( 181 | namespace=NamespaceScope( 182 | ns_alias=[NamespaceAlias(alias="ANS", names=["my", "ns"])] 183 | ) 184 | ) 185 | 186 | 187 | def test_ns_alias_global() -> None: 188 | content = """ 189 | namespace ANS = ::my::ns; 190 | """ 191 | data = parse_string(content, cleandoc=True) 192 | 193 | assert data == ParsedData( 194 | namespace=NamespaceScope( 195 | ns_alias=[NamespaceAlias(alias="ANS", names=["::", "my", "ns"])] 196 | ) 197 | ) 198 | 199 | 200 | def test_ns_attr() -> None: 201 | content = """ 202 | namespace n __attribute__((visibility("hidden"))) {} 203 | """ 204 | data = parse_string(content, cleandoc=True) 205 | 206 | assert data == ParsedData( 207 | namespace=NamespaceScope(namespaces={"n": NamespaceScope(name="n")}) 208 | ) 209 | -------------------------------------------------------------------------------- /tests/test_preprocessor.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import pytest 4 | import re 5 | import shutil 6 | import subprocess 7 | import typing 8 | 9 | from cxxheaderparser.options import ParserOptions, PreprocessorFunction 10 | from cxxheaderparser import preprocessor 11 | from cxxheaderparser.simple import ( 12 | NamespaceScope, 13 | ParsedData, 14 | parse_file, 15 | parse_string, 16 | Include, 17 | ) 18 | from cxxheaderparser.types import ( 19 | FundamentalSpecifier, 20 | NameSpecifier, 21 | PQName, 22 | Token, 23 | Type, 24 | Value, 25 | Variable, 26 | ) 27 | 28 | 29 | @pytest.fixture(params=["gcc", "msvc", "pcpp"]) 30 | def make_pp(request) -> typing.Callable[..., PreprocessorFunction]: 31 | param = request.param 32 | if param == "gcc": 33 | gcc_path = shutil.which("g++") 34 | if not gcc_path: 35 | pytest.skip("g++ not found") 36 | 37 | subprocess.run([gcc_path, "--version"]) 38 | return preprocessor.make_gcc_preprocessor 39 | elif param == "msvc": 40 | gcc_path = shutil.which("cl.exe") 41 | if not gcc_path: 42 | pytest.skip("cl.exe not found") 43 | 44 | return preprocessor.make_msvc_preprocessor 45 | elif param == "pcpp": 46 | if preprocessor.pcpp is None: 47 | pytest.skip("pcpp not installed") 48 | return preprocessor.make_pcpp_preprocessor 49 | else: 50 | assert False 51 | 52 | 53 | def test_basic_preprocessor( 54 | make_pp: typing.Callable[..., PreprocessorFunction], 55 | ) -> None: 56 | content = """ 57 | #define X 1 58 | int x = X; 59 | """ 60 | 61 | options = ParserOptions(preprocessor=make_pp()) 62 | data = parse_string(content, cleandoc=True, options=options) 63 | 64 | assert data == ParsedData( 65 | namespace=NamespaceScope( 66 | variables=[ 67 | Variable( 68 | name=PQName(segments=[NameSpecifier(name="x")]), 69 | type=Type( 70 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 71 | ), 72 | value=Value(tokens=[Token(value="1")]), 73 | ) 74 | ] 75 | ) 76 | ) 77 | 78 | 79 | def test_preprocessor_omit_content( 80 | make_pp: typing.Callable[..., PreprocessorFunction], 81 | tmp_path: pathlib.Path, 82 | ) -> None: 83 | """Ensure that content in other headers is omitted""" 84 | h_content = '#include "t2.h"' "\n" "int x = X;\n" 85 | h2_content = "#define X 2\n" "int omitted = 1;\n" 86 | 87 | with open(tmp_path / "t1.h", "w") as fp: 88 | fp.write(h_content) 89 | 90 | with open(tmp_path / "t2.h", "w") as fp: 91 | fp.write(h2_content) 92 | 93 | options = ParserOptions(preprocessor=make_pp()) 94 | data = parse_file(tmp_path / "t1.h", options=options) 95 | 96 | assert data == ParsedData( 97 | namespace=NamespaceScope( 98 | variables=[ 99 | Variable( 100 | name=PQName(segments=[NameSpecifier(name="x")]), 101 | type=Type( 102 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 103 | ), 104 | value=Value(tokens=[Token(value="2")]), 105 | ) 106 | ] 107 | ) 108 | ) 109 | 110 | 111 | def test_preprocessor_omit_content2( 112 | make_pp: typing.Callable[..., PreprocessorFunction], 113 | tmp_path: pathlib.Path, 114 | ) -> None: 115 | """ 116 | Ensure that content in other headers is omitted while handling pcpp 117 | relative path quirk 118 | """ 119 | h_content = '#include "t2.h"' "\n" "int x = X;\n" 120 | h2_content = "#define X 2\n" "int omitted = 1;\n" 121 | 122 | tmp_path2 = tmp_path / "l1" 123 | tmp_path2.mkdir() 124 | 125 | with open(tmp_path2 / "t1.h", "w") as fp: 126 | fp.write(h_content) 127 | 128 | with open(tmp_path2 / "t2.h", "w") as fp: 129 | fp.write(h2_content) 130 | 131 | options = ParserOptions(preprocessor=make_pp(include_paths=[str(tmp_path)])) 132 | 133 | # Weirdness happens here 134 | os.chdir(tmp_path) 135 | data = parse_file(tmp_path2 / "t1.h", options=options) 136 | 137 | assert data == ParsedData( 138 | namespace=NamespaceScope( 139 | variables=[ 140 | Variable( 141 | name=PQName(segments=[NameSpecifier(name="x")]), 142 | type=Type( 143 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 144 | ), 145 | value=Value(tokens=[Token(value="2")]), 146 | ) 147 | ] 148 | ) 149 | ) 150 | 151 | 152 | def test_preprocessor_encoding( 153 | make_pp: typing.Callable[..., PreprocessorFunction], tmp_path: pathlib.Path 154 | ) -> None: 155 | """Ensure we can handle alternate encodings""" 156 | h_content = b"// \xa9 2023 someone\n" b'#include "t2.h"' b"\n" b"int x = X;\n" 157 | 158 | h2_content = b"// \xa9 2023 someone\n" b"#define X 3\n" b"int omitted = 1;\n" 159 | 160 | with open(tmp_path / "t1.h", "wb") as fp: 161 | fp.write(h_content) 162 | 163 | with open(tmp_path / "t2.h", "wb") as fp: 164 | fp.write(h2_content) 165 | 166 | options = ParserOptions(preprocessor=make_pp(encoding="cp1252")) 167 | data = parse_file(tmp_path / "t1.h", options=options, encoding="cp1252") 168 | 169 | assert data == ParsedData( 170 | namespace=NamespaceScope( 171 | variables=[ 172 | Variable( 173 | name=PQName(segments=[NameSpecifier(name="x")]), 174 | type=Type( 175 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 176 | ), 177 | value=Value(tokens=[Token(value="3")]), 178 | ) 179 | ] 180 | ) 181 | ) 182 | 183 | 184 | @pytest.mark.skipif(preprocessor.pcpp is None, reason="pcpp not installed") 185 | def test_preprocessor_passthru_includes(tmp_path: pathlib.Path) -> None: 186 | """Ensure that all #include pass through""" 187 | h_content = '#include "t2.h"\n' 188 | 189 | with open(tmp_path / "t1.h", "w") as fp: 190 | fp.write(h_content) 191 | 192 | with open(tmp_path / "t2.h", "w") as fp: 193 | fp.write("") 194 | 195 | options = ParserOptions( 196 | preprocessor=preprocessor.make_pcpp_preprocessor( 197 | passthru_includes=re.compile(".+") 198 | ) 199 | ) 200 | data = parse_file(tmp_path / "t1.h", options=options) 201 | 202 | assert data == ParsedData( 203 | namespace=NamespaceScope(), includes=[Include(filename='"t2.h"')] 204 | ) 205 | 206 | 207 | def test_preprocessor_depfile( 208 | make_pp: typing.Callable[..., PreprocessorFunction], 209 | tmp_path: pathlib.Path, 210 | ) -> None: 211 | 212 | tmp_path = tmp_path / "hard path" 213 | tmp_path.mkdir(parents=True, exist_ok=True) 214 | 215 | # not supported 216 | if make_pp is preprocessor.make_msvc_preprocessor: 217 | return 218 | 219 | h_content = '#include "t2.h"' "\n" "int x = X;\n" 220 | h2_content = '#include "t3.h"\n' "#define X 2\n" "int omitted = 1;\n" 221 | h3_content = "int h3;" 222 | 223 | with open(tmp_path / "t1.h", "w") as fp: 224 | fp.write(h_content) 225 | 226 | with open(tmp_path / "t2.h", "w") as fp: 227 | fp.write(h2_content) 228 | 229 | with open(tmp_path / "t3.h", "w") as fp: 230 | fp.write(h3_content) 231 | 232 | depfile = tmp_path / "t1.d" 233 | deptarget = ["tgt"] 234 | 235 | options = ParserOptions(preprocessor=make_pp(depfile=depfile, deptarget=deptarget)) 236 | parse_file(tmp_path / "t1.h", options=options) 237 | 238 | with open(depfile) as fp: 239 | depcontent = fp.read() 240 | 241 | assert depcontent.startswith("tgt:") 242 | deps = [d.strip() for d in depcontent[4:].strip().split("\\\n")] 243 | deps = [d.replace("\\ ", " ").replace("\\\\", "\\") for d in deps if d] 244 | 245 | # gcc will insert extra paths of predefined stuff, so just make sure this is sane 246 | assert str(tmp_path / "t1.h") in deps 247 | assert str(tmp_path / "t2.h") in deps 248 | assert str(tmp_path / "t3.h") in deps 249 | -------------------------------------------------------------------------------- /tests/test_attributes.py: -------------------------------------------------------------------------------- 1 | # Note: testcases generated via `python -m cxxheaderparser.gentest` 2 | 3 | from cxxheaderparser.types import ( 4 | ClassDecl, 5 | EnumDecl, 6 | Enumerator, 7 | Field, 8 | FriendDecl, 9 | Function, 10 | FundamentalSpecifier, 11 | Method, 12 | NameSpecifier, 13 | PQName, 14 | Pointer, 15 | TemplateDecl, 16 | TemplateTypeParam, 17 | Token, 18 | Type, 19 | Typedef, 20 | Value, 21 | Variable, 22 | ) 23 | from cxxheaderparser.simple import ( 24 | ClassScope, 25 | NamespaceScope, 26 | parse_string, 27 | ParsedData, 28 | ) 29 | 30 | 31 | def test_attributes_everywhere() -> None: 32 | # TODO: someday we'll actually support storing attributes, 33 | # but for now just make sure they don't get in the way 34 | 35 | content = """ 36 | struct [[deprecated]] S {}; 37 | [[deprecated]] typedef S *PS; 38 | 39 | [[deprecated]] int x; 40 | union U { 41 | [[deprecated]] int n; 42 | }; 43 | [[deprecated]] void f(); 44 | 45 | enum [[deprecated]] E{A [[deprecated]], B [[deprecated]] = 42}; 46 | 47 | struct alignas(8) AS {}; 48 | """ 49 | data = parse_string(content, cleandoc=True) 50 | 51 | assert data == ParsedData( 52 | namespace=NamespaceScope( 53 | classes=[ 54 | ClassScope( 55 | class_decl=ClassDecl( 56 | typename=PQName( 57 | segments=[NameSpecifier(name="S")], classkey="struct" 58 | ) 59 | ) 60 | ), 61 | ClassScope( 62 | class_decl=ClassDecl( 63 | typename=PQName( 64 | segments=[NameSpecifier(name="U")], classkey="union" 65 | ) 66 | ), 67 | fields=[ 68 | Field( 69 | access="public", 70 | type=Type( 71 | typename=PQName( 72 | segments=[FundamentalSpecifier(name="int")] 73 | ) 74 | ), 75 | name="n", 76 | ) 77 | ], 78 | ), 79 | ClassScope( 80 | class_decl=ClassDecl( 81 | typename=PQName( 82 | segments=[NameSpecifier(name="AS")], classkey="struct" 83 | ) 84 | ) 85 | ), 86 | ], 87 | enums=[ 88 | EnumDecl( 89 | typename=PQName( 90 | segments=[NameSpecifier(name="E")], classkey="enum" 91 | ), 92 | values=[ 93 | Enumerator(name="A"), 94 | Enumerator(name="B", value=Value(tokens=[Token(value="42")])), 95 | ], 96 | ) 97 | ], 98 | functions=[ 99 | Function( 100 | return_type=Type( 101 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 102 | ), 103 | name=PQName(segments=[NameSpecifier(name="f")]), 104 | parameters=[], 105 | ) 106 | ], 107 | typedefs=[ 108 | Typedef( 109 | type=Pointer( 110 | ptr_to=Type(typename=PQName(segments=[NameSpecifier(name="S")])) 111 | ), 112 | name="PS", 113 | ) 114 | ], 115 | variables=[ 116 | Variable( 117 | name=PQName(segments=[NameSpecifier(name="x")]), 118 | type=Type( 119 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 120 | ), 121 | ) 122 | ], 123 | ) 124 | ) 125 | 126 | 127 | def test_attributes_gcc_enum_packed() -> None: 128 | content = """ 129 | enum Wheat { 130 | w1, 131 | w2, 132 | w3, 133 | } __attribute__((packed)); 134 | """ 135 | data = parse_string(content, cleandoc=True) 136 | 137 | assert data == ParsedData( 138 | namespace=NamespaceScope( 139 | enums=[ 140 | EnumDecl( 141 | typename=PQName( 142 | segments=[NameSpecifier(name="Wheat")], classkey="enum" 143 | ), 144 | values=[ 145 | Enumerator(name="w1"), 146 | Enumerator(name="w2"), 147 | Enumerator(name="w3"), 148 | ], 149 | ) 150 | ] 151 | ) 152 | ) 153 | 154 | 155 | def test_friendly_declspec() -> None: 156 | content = """ 157 | struct D { 158 | friend __declspec(dllexport) void my_friend(); 159 | static __declspec(dllexport) void static_declspec(); 160 | }; 161 | """ 162 | data = parse_string(content, cleandoc=True) 163 | 164 | assert data == ParsedData( 165 | namespace=NamespaceScope( 166 | classes=[ 167 | ClassScope( 168 | class_decl=ClassDecl( 169 | typename=PQName( 170 | segments=[NameSpecifier(name="D")], classkey="struct" 171 | ) 172 | ), 173 | friends=[ 174 | FriendDecl( 175 | fn=Method( 176 | return_type=Type( 177 | typename=PQName( 178 | segments=[FundamentalSpecifier(name="void")] 179 | ) 180 | ), 181 | name=PQName(segments=[NameSpecifier(name="my_friend")]), 182 | parameters=[], 183 | access="public", 184 | ) 185 | ) 186 | ], 187 | methods=[ 188 | Method( 189 | return_type=Type( 190 | typename=PQName( 191 | segments=[FundamentalSpecifier(name="void")] 192 | ) 193 | ), 194 | name=PQName( 195 | segments=[NameSpecifier(name="static_declspec")] 196 | ), 197 | parameters=[], 198 | static=True, 199 | access="public", 200 | ) 201 | ], 202 | ) 203 | ] 204 | ) 205 | ) 206 | 207 | 208 | def test_declspec_template() -> None: 209 | content = """ 210 | template 211 | __declspec(deprecated("message")) 212 | static T2 fn() { return T2(); } 213 | """ 214 | data = parse_string(content, cleandoc=True) 215 | 216 | assert data == ParsedData( 217 | namespace=NamespaceScope( 218 | functions=[ 219 | Function( 220 | return_type=Type( 221 | typename=PQName(segments=[NameSpecifier(name="T2")]) 222 | ), 223 | name=PQName(segments=[NameSpecifier(name="fn")]), 224 | parameters=[], 225 | static=True, 226 | has_body=True, 227 | template=TemplateDecl( 228 | params=[TemplateTypeParam(typekey="class", name="T2")] 229 | ), 230 | ) 231 | ] 232 | ) 233 | ) 234 | 235 | 236 | def test_multiple_attributes() -> None: 237 | content = """ 238 | extern const unsigned short int **__ctype_b_loc (void) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__const__)); 239 | """ 240 | data = parse_string(content, cleandoc=True) 241 | 242 | assert data == ParsedData( 243 | namespace=NamespaceScope( 244 | functions=[ 245 | Function( 246 | return_type=Pointer( 247 | ptr_to=Pointer( 248 | ptr_to=Type( 249 | typename=PQName( 250 | segments=[ 251 | FundamentalSpecifier(name="unsigned short int") 252 | ] 253 | ), 254 | const=True, 255 | ) 256 | ) 257 | ), 258 | name=PQName(segments=[NameSpecifier(name="__ctype_b_loc")]), 259 | parameters=[], 260 | extern=True, 261 | ) 262 | ] 263 | ) 264 | ) 265 | -------------------------------------------------------------------------------- /tests/test_class_base.py: -------------------------------------------------------------------------------- 1 | # Note: testcases generated via `python -m cxxheaderparser.gentest` 2 | 3 | from cxxheaderparser.types import ( 4 | BaseClass, 5 | ClassDecl, 6 | Field, 7 | FundamentalSpecifier, 8 | Method, 9 | NameSpecifier, 10 | PQName, 11 | TemplateArgument, 12 | TemplateSpecialization, 13 | Token, 14 | Type, 15 | ) 16 | from cxxheaderparser.simple import ( 17 | ClassScope, 18 | NamespaceScope, 19 | parse_string, 20 | ParsedData, 21 | ) 22 | 23 | 24 | def test_class_private_base() -> None: 25 | content = """ 26 | namespace Citrus 27 | { 28 | class BloodOrange { }; 29 | } 30 | 31 | class Bananna: public Citrus::BloodOrange 32 | { 33 | }; 34 | 35 | class ExcellentCake: private Citrus::BloodOrange, Convoluted::Nested::Mixin 36 | { 37 | }; 38 | """ 39 | data = parse_string(content, cleandoc=True) 40 | 41 | assert data == ParsedData( 42 | namespace=NamespaceScope( 43 | classes=[ 44 | ClassScope( 45 | class_decl=ClassDecl( 46 | typename=PQName( 47 | segments=[NameSpecifier(name="Bananna")], classkey="class" 48 | ), 49 | bases=[ 50 | BaseClass( 51 | access="public", 52 | typename=PQName( 53 | segments=[ 54 | NameSpecifier(name="Citrus"), 55 | NameSpecifier(name="BloodOrange"), 56 | ] 57 | ), 58 | ) 59 | ], 60 | ) 61 | ), 62 | ClassScope( 63 | class_decl=ClassDecl( 64 | typename=PQName( 65 | segments=[NameSpecifier(name="ExcellentCake")], 66 | classkey="class", 67 | ), 68 | bases=[ 69 | BaseClass( 70 | access="private", 71 | typename=PQName( 72 | segments=[ 73 | NameSpecifier(name="Citrus"), 74 | NameSpecifier(name="BloodOrange"), 75 | ] 76 | ), 77 | ), 78 | BaseClass( 79 | access="private", 80 | typename=PQName( 81 | segments=[ 82 | NameSpecifier(name="Convoluted"), 83 | NameSpecifier(name="Nested"), 84 | NameSpecifier(name="Mixin"), 85 | ] 86 | ), 87 | ), 88 | ], 89 | ) 90 | ), 91 | ], 92 | namespaces={ 93 | "Citrus": NamespaceScope( 94 | name="Citrus", 95 | classes=[ 96 | ClassScope( 97 | class_decl=ClassDecl( 98 | typename=PQName( 99 | segments=[NameSpecifier(name="BloodOrange")], 100 | classkey="class", 101 | ) 102 | ) 103 | ) 104 | ], 105 | ) 106 | }, 107 | ) 108 | ) 109 | 110 | 111 | def test_class_virtual_base() -> None: 112 | content = """ 113 | class BaseMangoClass {}; 114 | class MangoClass : virtual public BaseMangoClass {}; 115 | """ 116 | data = parse_string(content, cleandoc=True) 117 | 118 | assert data == ParsedData( 119 | namespace=NamespaceScope( 120 | classes=[ 121 | ClassScope( 122 | class_decl=ClassDecl( 123 | typename=PQName( 124 | segments=[NameSpecifier(name="BaseMangoClass")], 125 | classkey="class", 126 | ) 127 | ) 128 | ), 129 | ClassScope( 130 | class_decl=ClassDecl( 131 | typename=PQName( 132 | segments=[NameSpecifier(name="MangoClass")], 133 | classkey="class", 134 | ), 135 | bases=[ 136 | BaseClass( 137 | access="public", 138 | typename=PQName( 139 | segments=[NameSpecifier(name="BaseMangoClass")] 140 | ), 141 | virtual=True, 142 | ) 143 | ], 144 | ) 145 | ), 146 | ] 147 | ) 148 | ) 149 | 150 | 151 | def test_class_multiple_base_with_virtual() -> None: 152 | content = """ 153 | class BlueJay : public Bird, public virtual Food { 154 | public: 155 | BlueJay() {} 156 | }; 157 | """ 158 | data = parse_string(content, cleandoc=True) 159 | 160 | assert data == ParsedData( 161 | namespace=NamespaceScope( 162 | classes=[ 163 | ClassScope( 164 | class_decl=ClassDecl( 165 | typename=PQName( 166 | segments=[NameSpecifier(name="BlueJay")], classkey="class" 167 | ), 168 | bases=[ 169 | BaseClass( 170 | access="public", 171 | typename=PQName(segments=[NameSpecifier(name="Bird")]), 172 | ), 173 | BaseClass( 174 | access="public", 175 | typename=PQName(segments=[NameSpecifier(name="Food")]), 176 | virtual=True, 177 | ), 178 | ], 179 | ), 180 | methods=[ 181 | Method( 182 | return_type=None, 183 | name=PQName(segments=[NameSpecifier(name="BlueJay")]), 184 | parameters=[], 185 | has_body=True, 186 | access="public", 187 | constructor=True, 188 | ) 189 | ], 190 | ) 191 | ] 192 | ) 193 | ) 194 | 195 | 196 | def test_class_base_specialized() -> None: 197 | content = """ 198 | class Pea : public Vegetable { 199 | int i; 200 | }; 201 | 202 | """ 203 | data = parse_string(content, cleandoc=True) 204 | 205 | assert data == ParsedData( 206 | namespace=NamespaceScope( 207 | classes=[ 208 | ClassScope( 209 | class_decl=ClassDecl( 210 | typename=PQName( 211 | segments=[NameSpecifier(name="Pea")], classkey="class" 212 | ), 213 | bases=[ 214 | BaseClass( 215 | access="public", 216 | typename=PQName( 217 | segments=[ 218 | NameSpecifier( 219 | name="Vegetable", 220 | specialization=TemplateSpecialization( 221 | args=[ 222 | TemplateArgument( 223 | arg=Type( 224 | typename=PQName( 225 | segments=[ 226 | NameSpecifier( 227 | name="Green" 228 | ) 229 | ] 230 | ) 231 | ) 232 | ) 233 | ] 234 | ), 235 | ) 236 | ] 237 | ), 238 | ) 239 | ], 240 | ), 241 | fields=[ 242 | Field( 243 | access="private", 244 | type=Type( 245 | typename=PQName( 246 | segments=[FundamentalSpecifier(name="int")] 247 | ) 248 | ), 249 | name="i", 250 | ) 251 | ], 252 | ) 253 | ] 254 | ) 255 | ) 256 | -------------------------------------------------------------------------------- /cxxheaderparser/visitor.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import typing 3 | 4 | if sys.version_info >= (3, 8): 5 | from typing import Protocol 6 | else: 7 | Protocol = object # pragma: no cover 8 | 9 | 10 | from .types import ( 11 | Concept, 12 | DeductionGuide, 13 | EnumDecl, 14 | Field, 15 | ForwardDecl, 16 | FriendDecl, 17 | Function, 18 | Method, 19 | NamespaceAlias, 20 | TemplateInst, 21 | Typedef, 22 | UsingAlias, 23 | UsingDecl, 24 | Variable, 25 | Value, 26 | ) 27 | 28 | from .parserstate import ( 29 | State, 30 | ClassBlockState, 31 | ExternBlockState, 32 | NamespaceBlockState, 33 | NonClassBlockState, 34 | ) 35 | 36 | 37 | class CxxVisitor(Protocol): 38 | """ 39 | Defines the interface used by the parser to emit events 40 | """ 41 | 42 | def on_parse_start(self, state: NamespaceBlockState) -> None: 43 | """ 44 | Called when parsing begins 45 | """ 46 | 47 | def on_pragma(self, state: State, content: Value) -> None: 48 | """ 49 | Called once for each ``#pragma`` directive encountered 50 | """ 51 | 52 | def on_include(self, state: State, filename: str) -> None: 53 | """ 54 | Called once for each ``#include`` directive encountered 55 | """ 56 | 57 | def on_extern_block_start(self, state: ExternBlockState) -> typing.Optional[bool]: 58 | """ 59 | .. code-block:: c++ 60 | 61 | extern "C" { 62 | 63 | } 64 | 65 | If this function returns False, the visitor will not be called for any 66 | items inside this block (including on_extern_block_end) 67 | """ 68 | 69 | def on_extern_block_end(self, state: ExternBlockState) -> None: 70 | """ 71 | Called when an extern block ends 72 | """ 73 | 74 | def on_namespace_start(self, state: NamespaceBlockState) -> typing.Optional[bool]: 75 | """ 76 | Called when a ``namespace`` directive is encountered 77 | 78 | If this function returns False, the visitor will not be called for any 79 | items inside this namespace (including on_namespace_end) 80 | """ 81 | 82 | def on_namespace_end(self, state: NamespaceBlockState) -> None: 83 | """ 84 | Called at the end of a ``namespace`` block 85 | """ 86 | 87 | def on_namespace_alias( 88 | self, state: NonClassBlockState, alias: NamespaceAlias 89 | ) -> None: 90 | """ 91 | Called when a ``namespace`` alias is encountered 92 | """ 93 | 94 | def on_concept(self, state: NonClassBlockState, concept: Concept) -> None: 95 | """ 96 | .. code-block:: c++ 97 | 98 | template 99 | concept Meowable = is_meowable; 100 | """ 101 | 102 | def on_forward_decl(self, state: State, fdecl: ForwardDecl) -> None: 103 | """ 104 | Called when a forward declaration is encountered 105 | """ 106 | 107 | def on_template_inst(self, state: State, inst: TemplateInst) -> None: 108 | """ 109 | Called when an explicit template instantiation is encountered 110 | """ 111 | 112 | def on_variable(self, state: State, v: Variable) -> None: 113 | """ 114 | Called when a global variable is encountered 115 | """ 116 | 117 | def on_function(self, state: NonClassBlockState, fn: Function) -> None: 118 | """ 119 | Called when a function is encountered that isn't part of a class 120 | """ 121 | 122 | def on_method_impl(self, state: NonClassBlockState, method: Method) -> None: 123 | """ 124 | Called when a method implementation is encountered outside of a class 125 | declaration. For example: 126 | 127 | .. code-block:: c++ 128 | 129 | void MyClass::fn() { 130 | // does something 131 | } 132 | 133 | .. note:: The above implementation is ambiguous, as it technically could 134 | be a function in a namespace. We emit this instead as it's 135 | more likely to be the case in common code. 136 | """ 137 | 138 | def on_typedef(self, state: State, typedef: Typedef) -> None: 139 | """ 140 | Called for each typedef instance encountered. For example: 141 | 142 | .. code-block:: c++ 143 | 144 | typedef int T, *PT; 145 | 146 | Will result in ``on_typedef`` being called twice, once for ``T`` and 147 | once for ``*PT`` 148 | """ 149 | 150 | def on_using_namespace( 151 | self, state: NonClassBlockState, namespace: typing.List[str] 152 | ) -> None: 153 | """ 154 | .. code-block:: c++ 155 | 156 | using namespace std; 157 | """ 158 | 159 | def on_using_alias(self, state: State, using: UsingAlias) -> None: 160 | """ 161 | .. code-block:: c++ 162 | 163 | using foo = int; 164 | 165 | template 166 | using VectorT = std::vector; 167 | 168 | """ 169 | 170 | def on_using_declaration(self, state: State, using: UsingDecl) -> None: 171 | """ 172 | .. code-block:: c++ 173 | 174 | using NS::ClassName; 175 | 176 | """ 177 | 178 | # 179 | # Enums 180 | # 181 | 182 | def on_enum(self, state: State, enum: EnumDecl) -> None: 183 | """ 184 | Called after an enum is encountered 185 | """ 186 | 187 | # 188 | # Class/union/struct 189 | # 190 | 191 | def on_class_start(self, state: ClassBlockState) -> typing.Optional[bool]: 192 | """ 193 | Called when a class/struct/union is encountered 194 | 195 | When part of a typedef: 196 | 197 | .. code-block:: c++ 198 | 199 | typedef struct { } X; 200 | 201 | This is called first, followed by on_typedef for each typedef instance 202 | encountered. The compound type object is passed as the type to the 203 | typedef. 204 | 205 | If this function returns False, the visitor will not be called for any 206 | items inside this class (including on_class_end) 207 | """ 208 | 209 | def on_class_field(self, state: ClassBlockState, f: Field) -> None: 210 | """ 211 | Called when a field of a class is encountered 212 | """ 213 | 214 | def on_class_friend(self, state: ClassBlockState, friend: FriendDecl) -> None: 215 | """ 216 | Called when a friend declaration is encountered 217 | """ 218 | 219 | def on_class_method(self, state: ClassBlockState, method: Method) -> None: 220 | """ 221 | Called when a method of a class is encountered inside of a class 222 | """ 223 | 224 | def on_class_end(self, state: ClassBlockState) -> None: 225 | """ 226 | Called when the end of a class/struct/union is encountered. 227 | 228 | When a variable like this is declared: 229 | 230 | .. code-block:: c++ 231 | 232 | struct X { 233 | 234 | } x; 235 | 236 | Then ``on_class_start``, .. ``on_class_end`` are emitted, along with 237 | ``on_variable`` for each instance declared. 238 | """ 239 | 240 | def on_deduction_guide( 241 | self, state: NonClassBlockState, guide: DeductionGuide 242 | ) -> None: 243 | """ 244 | Called when a deduction guide is encountered 245 | """ 246 | 247 | 248 | class NullVisitor: 249 | """ 250 | This visitor does nothing 251 | """ 252 | 253 | def on_parse_start(self, state: NamespaceBlockState) -> None: 254 | return None 255 | 256 | def on_pragma(self, state: State, content: Value) -> None: 257 | return None 258 | 259 | def on_include(self, state: State, filename: str) -> None: 260 | return None 261 | 262 | def on_extern_block_start(self, state: ExternBlockState) -> typing.Optional[bool]: 263 | return None 264 | 265 | def on_extern_block_end(self, state: ExternBlockState) -> None: 266 | return None 267 | 268 | def on_namespace_start(self, state: NamespaceBlockState) -> typing.Optional[bool]: 269 | return None 270 | 271 | def on_namespace_end(self, state: NamespaceBlockState) -> None: 272 | return None 273 | 274 | def on_concept(self, state: NonClassBlockState, concept: Concept) -> None: 275 | return None 276 | 277 | def on_namespace_alias( 278 | self, state: NonClassBlockState, alias: NamespaceAlias 279 | ) -> None: 280 | return None 281 | 282 | def on_forward_decl(self, state: State, fdecl: ForwardDecl) -> None: 283 | return None 284 | 285 | def on_template_inst(self, state: State, inst: TemplateInst) -> None: 286 | return None 287 | 288 | def on_variable(self, state: State, v: Variable) -> None: 289 | return None 290 | 291 | def on_function(self, state: NonClassBlockState, fn: Function) -> None: 292 | return None 293 | 294 | def on_method_impl(self, state: NonClassBlockState, method: Method) -> None: 295 | return None 296 | 297 | def on_typedef(self, state: State, typedef: Typedef) -> None: 298 | return None 299 | 300 | def on_using_namespace( 301 | self, state: NonClassBlockState, namespace: typing.List[str] 302 | ) -> None: 303 | return None 304 | 305 | def on_using_alias(self, state: State, using: UsingAlias) -> None: 306 | return None 307 | 308 | def on_using_declaration(self, state: State, using: UsingDecl) -> None: 309 | return None 310 | 311 | def on_enum(self, state: State, enum: EnumDecl) -> None: 312 | return None 313 | 314 | def on_class_start(self, state: ClassBlockState) -> typing.Optional[bool]: 315 | return None 316 | 317 | def on_class_field(self, state: ClassBlockState, f: Field) -> None: 318 | return None 319 | 320 | def on_class_friend(self, state: ClassBlockState, friend: FriendDecl) -> None: 321 | return None 322 | 323 | def on_class_method(self, state: ClassBlockState, method: Method) -> None: 324 | return None 325 | 326 | def on_class_end(self, state: ClassBlockState) -> None: 327 | return None 328 | 329 | def on_deduction_guide( 330 | self, state: NonClassBlockState, guide: DeductionGuide 331 | ) -> None: 332 | return None 333 | 334 | 335 | null_visitor = NullVisitor() 336 | -------------------------------------------------------------------------------- /tests/test_misc.py: -------------------------------------------------------------------------------- 1 | # Note: testcases generated via `python -m cxxheaderparser.gentest` 2 | 3 | from cxxheaderparser.errors import CxxParseError 4 | from cxxheaderparser.types import ( 5 | BaseClass, 6 | ClassDecl, 7 | Function, 8 | FundamentalSpecifier, 9 | NameSpecifier, 10 | PQName, 11 | Parameter, 12 | Token, 13 | Type, 14 | Value, 15 | Variable, 16 | ) 17 | from cxxheaderparser.simple import ( 18 | ClassScope, 19 | Include, 20 | NamespaceScope, 21 | Pragma, 22 | parse_string, 23 | ParsedData, 24 | ) 25 | 26 | import pytest 27 | 28 | # 29 | # minimal preprocessor support 30 | # 31 | 32 | 33 | def test_includes() -> None: 34 | content = """ 35 | #include 36 | #include "local.h" 37 | # include "space.h" 38 | """ 39 | data = parse_string(content, cleandoc=True) 40 | 41 | assert data == ParsedData( 42 | includes=[Include(""), Include('"local.h"'), Include('"space.h"')] 43 | ) 44 | 45 | 46 | def test_pragma() -> None: 47 | content = """ 48 | 49 | #pragma once 50 | 51 | """ 52 | data = parse_string(content, cleandoc=True) 53 | 54 | assert data == ParsedData( 55 | pragmas=[Pragma(content=Value(tokens=[Token(value="once")]))] 56 | ) 57 | 58 | 59 | def test_pragma_more() -> None: 60 | content = """ 61 | 62 | #pragma (some content here) 63 | #pragma (even \ 64 | more \ 65 | content here) 66 | 67 | """ 68 | data = parse_string(content, cleandoc=True) 69 | 70 | assert data == ParsedData( 71 | pragmas=[ 72 | Pragma( 73 | content=Value( 74 | tokens=[ 75 | Token(value="("), 76 | Token(value="some"), 77 | Token(value="content"), 78 | Token(value="here"), 79 | Token(value=")"), 80 | ] 81 | ) 82 | ), 83 | Pragma( 84 | content=Value( 85 | tokens=[ 86 | Token(value="("), 87 | Token(value="even"), 88 | Token(value="more"), 89 | Token(value="content"), 90 | Token(value="here"), 91 | Token(value=")"), 92 | ] 93 | ) 94 | ), 95 | ] 96 | ) 97 | 98 | 99 | def test_line_and_define() -> None: 100 | content = """ 101 | // this should work + change line number of error 102 | #line 40 "filename.h" 103 | // this should fail 104 | #define 1 105 | """ 106 | with pytest.raises(CxxParseError) as e: 107 | parse_string(content, cleandoc=True) 108 | 109 | assert "filename.h:41" in str(e.value) 110 | 111 | 112 | # 113 | # extern "C" 114 | # 115 | 116 | 117 | def test_extern_c() -> None: 118 | content = """ 119 | extern "C" { 120 | int x; 121 | }; 122 | 123 | int y; 124 | """ 125 | data = parse_string(content, cleandoc=True) 126 | 127 | assert data == ParsedData( 128 | namespace=NamespaceScope( 129 | variables=[ 130 | Variable( 131 | name=PQName(segments=[NameSpecifier(name="x")]), 132 | type=Type( 133 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 134 | ), 135 | ), 136 | Variable( 137 | name=PQName(segments=[NameSpecifier(name="y")]), 138 | type=Type( 139 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 140 | ), 141 | ), 142 | ] 143 | ) 144 | ) 145 | 146 | 147 | def test_misc_extern_inline() -> None: 148 | content = """ 149 | extern "C++" { 150 | inline HAL_Value HAL_GetSimValue(HAL_SimValueHandle handle) { 151 | HAL_Value v; 152 | return v; 153 | } 154 | } // extern "C++" 155 | """ 156 | data = parse_string(content, cleandoc=True) 157 | 158 | assert data == ParsedData( 159 | namespace=NamespaceScope( 160 | functions=[ 161 | Function( 162 | return_type=Type( 163 | typename=PQName(segments=[NameSpecifier(name="HAL_Value")]) 164 | ), 165 | name=PQName(segments=[NameSpecifier(name="HAL_GetSimValue")]), 166 | parameters=[ 167 | Parameter( 168 | type=Type( 169 | typename=PQName( 170 | segments=[NameSpecifier(name="HAL_SimValueHandle")] 171 | ) 172 | ), 173 | name="handle", 174 | ) 175 | ], 176 | inline=True, 177 | has_body=True, 178 | ) 179 | ] 180 | ) 181 | ) 182 | 183 | 184 | # 185 | # Misc 186 | # 187 | 188 | 189 | def test_static_assert_1() -> None: 190 | # static_assert should be ignored 191 | content = """ 192 | static_assert(x == 1); 193 | """ 194 | data = parse_string(content, cleandoc=True) 195 | 196 | assert data == ParsedData() 197 | 198 | 199 | def test_static_assert_2() -> None: 200 | # static_assert should be ignored 201 | content = """ 202 | static_assert(sizeof(int) == 4, 203 | "integer size is wrong" 204 | "for some reason"); 205 | """ 206 | data = parse_string(content, cleandoc=True) 207 | 208 | assert data == ParsedData() 209 | 210 | 211 | def test_comment_eof() -> None: 212 | content = """ 213 | namespace a {} // namespace a""" 214 | data = parse_string(content, cleandoc=True) 215 | 216 | assert data == ParsedData( 217 | namespace=NamespaceScope(namespaces={"a": NamespaceScope(name="a")}) 218 | ) 219 | 220 | 221 | def test_final() -> None: 222 | content = """ 223 | // ok here 224 | int fn(const int final); 225 | 226 | // ok here 227 | int final = 2; 228 | 229 | // but it's a keyword here 230 | struct B final : A {}; 231 | """ 232 | data = parse_string(content, cleandoc=True) 233 | 234 | assert data == ParsedData( 235 | namespace=NamespaceScope( 236 | classes=[ 237 | ClassScope( 238 | class_decl=ClassDecl( 239 | typename=PQName( 240 | segments=[NameSpecifier(name="B")], classkey="struct" 241 | ), 242 | bases=[ 243 | BaseClass( 244 | access="public", 245 | typename=PQName(segments=[NameSpecifier(name="A")]), 246 | ) 247 | ], 248 | final=True, 249 | ) 250 | ) 251 | ], 252 | functions=[ 253 | Function( 254 | return_type=Type( 255 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 256 | ), 257 | name=PQName(segments=[NameSpecifier(name="fn")]), 258 | parameters=[ 259 | Parameter( 260 | type=Type( 261 | typename=PQName( 262 | segments=[FundamentalSpecifier(name="int")] 263 | ), 264 | const=True, 265 | ), 266 | name="final", 267 | ) 268 | ], 269 | ) 270 | ], 271 | variables=[ 272 | Variable( 273 | name=PQName(segments=[NameSpecifier(name="final")]), 274 | type=Type( 275 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 276 | ), 277 | value=Value(tokens=[Token(value="2")]), 278 | ) 279 | ], 280 | ) 281 | ) 282 | 283 | 284 | # 285 | # User defined literals 286 | # 287 | 288 | 289 | def test_user_defined_literal() -> None: 290 | content = """ 291 | units::volt_t v = 1_V; 292 | """ 293 | data = parse_string(content, cleandoc=True) 294 | 295 | assert data == ParsedData( 296 | namespace=NamespaceScope( 297 | variables=[ 298 | Variable( 299 | name=PQName(segments=[NameSpecifier(name="v")]), 300 | type=Type( 301 | typename=PQName( 302 | segments=[ 303 | NameSpecifier(name="units"), 304 | NameSpecifier(name="volt_t"), 305 | ] 306 | ) 307 | ), 308 | value=Value(tokens=[Token(value="1_V")]), 309 | ) 310 | ] 311 | ) 312 | ) 313 | 314 | 315 | # 316 | # Line continuation 317 | # 318 | 319 | 320 | def test_line_continuation() -> None: 321 | content = """ 322 | static int \ 323 | variable; 324 | """ 325 | data = parse_string(content, cleandoc=True) 326 | 327 | assert data == ParsedData( 328 | namespace=NamespaceScope( 329 | variables=[ 330 | Variable( 331 | name=PQName(segments=[NameSpecifier(name="variable")]), 332 | type=Type( 333 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 334 | ), 335 | static=True, 336 | ) 337 | ] 338 | ) 339 | ) 340 | 341 | 342 | # 343 | # #warning (C++23) 344 | # 345 | 346 | 347 | def test_warning_directive() -> None: 348 | content = """ 349 | #warning "this is a warning" 350 | """ 351 | data = parse_string(content, cleandoc=True) 352 | 353 | assert data == ParsedData() 354 | -------------------------------------------------------------------------------- /tests/test_typefmt.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import pytest 4 | 5 | from cxxheaderparser.tokfmt import Token 6 | from cxxheaderparser.types import ( 7 | Array, 8 | DecoratedType, 9 | FunctionType, 10 | FundamentalSpecifier, 11 | Method, 12 | MoveReference, 13 | NameSpecifier, 14 | PQName, 15 | Parameter, 16 | Pointer, 17 | Reference, 18 | TemplateArgument, 19 | TemplateSpecialization, 20 | TemplateDecl, 21 | Type, 22 | Value, 23 | ) 24 | 25 | 26 | @pytest.mark.parametrize( 27 | "pytype,typestr,declstr", 28 | [ 29 | ( 30 | Type(typename=PQName(segments=[FundamentalSpecifier(name="int")])), 31 | "int", 32 | "int name", 33 | ), 34 | ( 35 | Type( 36 | typename=PQName(segments=[FundamentalSpecifier(name="int")]), const=True 37 | ), 38 | "const int", 39 | "const int name", 40 | ), 41 | ( 42 | Type( 43 | typename=PQName(segments=[NameSpecifier(name="S")], classkey="struct") 44 | ), 45 | "struct S", 46 | "struct S name", 47 | ), 48 | ( 49 | Pointer( 50 | ptr_to=Type( 51 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 52 | ) 53 | ), 54 | "int*", 55 | "int* name", 56 | ), 57 | ( 58 | Pointer( 59 | ptr_to=Pointer( 60 | ptr_to=Type( 61 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 62 | ) 63 | ) 64 | ), 65 | "int**", 66 | "int** name", 67 | ), 68 | ( 69 | Reference( 70 | ref_to=Type( 71 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 72 | ) 73 | ), 74 | "int&", 75 | "int& name", 76 | ), 77 | ( 78 | Reference( 79 | ref_to=Array( 80 | array_of=Type( 81 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 82 | ), 83 | size=Value(tokens=[Token(value="3")]), 84 | ) 85 | ), 86 | "int (&)[3]", 87 | "int (& name)[3]", 88 | ), 89 | ( 90 | MoveReference( 91 | moveref_to=Type( 92 | typename=PQName( 93 | segments=[NameSpecifier(name="T"), NameSpecifier(name="T")] 94 | ) 95 | ) 96 | ), 97 | "T::T&&", 98 | "T::T&& name", 99 | ), 100 | ( 101 | Pointer( 102 | ptr_to=Array( 103 | array_of=Type( 104 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 105 | ), 106 | size=Value(tokens=[Token(value="3")]), 107 | ) 108 | ), 109 | "int (*)[3]", 110 | "int (* name)[3]", 111 | ), 112 | ( 113 | Pointer( 114 | ptr_to=Array( 115 | array_of=Type( 116 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 117 | ), 118 | size=Value(tokens=[Token(value="3")]), 119 | ), 120 | const=True, 121 | ), 122 | "int (* const)[3]", 123 | "int (* const name)[3]", 124 | ), 125 | ( 126 | FunctionType( 127 | return_type=Type( 128 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 129 | ), 130 | parameters=[ 131 | Parameter( 132 | type=Type( 133 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 134 | ) 135 | ) 136 | ], 137 | ), 138 | "int (int)", 139 | "int name(int)", 140 | ), 141 | ( 142 | FunctionType( 143 | return_type=Type( 144 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 145 | ), 146 | parameters=[ 147 | Parameter( 148 | type=Type( 149 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 150 | ) 151 | ) 152 | ], 153 | has_trailing_return=True, 154 | ), 155 | "auto (int) -> int", 156 | "auto name(int) -> int", 157 | ), 158 | ( 159 | FunctionType( 160 | return_type=Type( 161 | typename=PQName( 162 | segments=[FundamentalSpecifier(name="void")], 163 | ), 164 | ), 165 | parameters=[ 166 | Parameter( 167 | type=Type( 168 | typename=PQName( 169 | segments=[FundamentalSpecifier(name="int")], 170 | ), 171 | ), 172 | name="a", 173 | ), 174 | Parameter( 175 | type=Type( 176 | typename=PQName( 177 | segments=[FundamentalSpecifier(name="int")], 178 | ), 179 | ), 180 | name="b", 181 | ), 182 | ], 183 | ), 184 | "void (int a, int b)", 185 | "void name(int a, int b)", 186 | ), 187 | ( 188 | Pointer( 189 | ptr_to=FunctionType( 190 | return_type=Type( 191 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 192 | ), 193 | parameters=[ 194 | Parameter( 195 | type=Type( 196 | typename=PQName( 197 | segments=[FundamentalSpecifier(name="int")] 198 | ) 199 | ) 200 | ) 201 | ], 202 | ) 203 | ), 204 | "int (*)(int)", 205 | "int (* name)(int)", 206 | ), 207 | ( 208 | Type( 209 | typename=PQName( 210 | segments=[ 211 | NameSpecifier(name="std"), 212 | NameSpecifier( 213 | name="function", 214 | specialization=TemplateSpecialization( 215 | args=[ 216 | TemplateArgument( 217 | arg=FunctionType( 218 | return_type=Type( 219 | typename=PQName( 220 | segments=[ 221 | FundamentalSpecifier(name="int") 222 | ] 223 | ) 224 | ), 225 | parameters=[ 226 | Parameter( 227 | type=Type( 228 | typename=PQName( 229 | segments=[ 230 | FundamentalSpecifier( 231 | name="int" 232 | ) 233 | ] 234 | ) 235 | ) 236 | ) 237 | ], 238 | ) 239 | ) 240 | ] 241 | ), 242 | ), 243 | ] 244 | ) 245 | ), 246 | "std::function", 247 | "std::function name", 248 | ), 249 | ( 250 | Type( 251 | typename=PQName( 252 | segments=[ 253 | NameSpecifier( 254 | name="foo", 255 | specialization=TemplateSpecialization( 256 | args=[ 257 | TemplateArgument( 258 | arg=Type( 259 | typename=PQName( 260 | segments=[ 261 | NameSpecifier(name=""), 262 | NameSpecifier(name="T"), 263 | ], 264 | ) 265 | ), 266 | ) 267 | ] 268 | ), 269 | ) 270 | ] 271 | ), 272 | ), 273 | "foo<::T>", 274 | "foo<::T> name", 275 | ), 276 | ( 277 | Type( 278 | typename=PQName( 279 | segments=[ 280 | NameSpecifier( 281 | name="foo", 282 | specialization=TemplateSpecialization( 283 | args=[ 284 | TemplateArgument( 285 | arg=Type( 286 | typename=PQName( 287 | segments=[ 288 | NameSpecifier(name=""), 289 | NameSpecifier(name="T"), 290 | ], 291 | has_typename=True, 292 | ) 293 | ), 294 | ) 295 | ] 296 | ), 297 | ) 298 | ] 299 | ), 300 | ), 301 | "foo", 302 | "foo name", 303 | ), 304 | ], 305 | ) 306 | def test_typefmt( 307 | pytype: typing.Union[DecoratedType, FunctionType], typestr: str, declstr: str 308 | ): 309 | # basic formatting 310 | assert pytype.format() == typestr 311 | 312 | # as a type declaration 313 | assert pytype.format_decl("name") == declstr 314 | -------------------------------------------------------------------------------- /cxxheaderparser/simple.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | The simple parser/collector iterates over the C++ file and returns a data 4 | structure with all elements in it. Not quite as flexible as implementing 5 | your own parser listener, but you can accomplish most things with it. 6 | 7 | cxxheaderparser's unit tests predominantly use the simple API for parsing, 8 | so you can expect it to be pretty stable. 9 | 10 | The :func:`parse_string` and :func:`parse_file` functions are a great place 11 | to start: 12 | 13 | .. code-block:: python 14 | 15 | from cxxheaderparser.simple import parse_string 16 | 17 | content = ''' 18 | int x; 19 | ''' 20 | 21 | parsed_data = parse_string(content) 22 | 23 | See below for the contents of the returned :class:`ParsedData`. 24 | 25 | """ 26 | 27 | import os 28 | import sys 29 | import inspect 30 | import typing 31 | 32 | 33 | from dataclasses import dataclass, field 34 | 35 | from .types import ( 36 | ClassDecl, 37 | Concept, 38 | DeductionGuide, 39 | EnumDecl, 40 | Field, 41 | ForwardDecl, 42 | FriendDecl, 43 | Function, 44 | Method, 45 | NamespaceAlias, 46 | TemplateInst, 47 | Typedef, 48 | UsingAlias, 49 | UsingDecl, 50 | Variable, 51 | Value, 52 | ) 53 | 54 | from .parserstate import ( 55 | ClassBlockState, 56 | ExternBlockState, 57 | NamespaceBlockState, 58 | ) 59 | from .parser import CxxParser 60 | from .options import ParserOptions 61 | 62 | # 63 | # Data structure 64 | # 65 | 66 | 67 | @dataclass 68 | class ClassScope: 69 | """ 70 | Contains all data collected for a single C++ class 71 | """ 72 | 73 | #: Information about the class declaration is here 74 | class_decl: ClassDecl 75 | 76 | #: Nested classes 77 | classes: typing.List["ClassScope"] = field(default_factory=list) 78 | enums: typing.List[EnumDecl] = field(default_factory=list) 79 | fields: typing.List[Field] = field(default_factory=list) 80 | friends: typing.List[FriendDecl] = field(default_factory=list) 81 | methods: typing.List[Method] = field(default_factory=list) 82 | typedefs: typing.List[Typedef] = field(default_factory=list) 83 | 84 | forward_decls: typing.List[ForwardDecl] = field(default_factory=list) 85 | using: typing.List[UsingDecl] = field(default_factory=list) 86 | using_alias: typing.List[UsingAlias] = field(default_factory=list) 87 | 88 | 89 | @dataclass 90 | class NamespaceScope: 91 | """ 92 | Contains all data collected for a single namespace. Content for child 93 | namespaces are found in the ``namespaces`` attribute. 94 | """ 95 | 96 | name: str = "" 97 | inline: bool = False 98 | doxygen: typing.Optional[str] = None 99 | 100 | classes: typing.List["ClassScope"] = field(default_factory=list) 101 | enums: typing.List[EnumDecl] = field(default_factory=list) 102 | 103 | #: Function declarations (with or without body) 104 | functions: typing.List[Function] = field(default_factory=list) 105 | 106 | #: Method implementations outside of a class (must have a body) 107 | method_impls: typing.List[Method] = field(default_factory=list) 108 | 109 | typedefs: typing.List[Typedef] = field(default_factory=list) 110 | variables: typing.List[Variable] = field(default_factory=list) 111 | 112 | forward_decls: typing.List[ForwardDecl] = field(default_factory=list) 113 | using: typing.List[UsingDecl] = field(default_factory=list) 114 | using_ns: typing.List["UsingNamespace"] = field(default_factory=list) 115 | using_alias: typing.List[UsingAlias] = field(default_factory=list) 116 | ns_alias: typing.List[NamespaceAlias] = field(default_factory=list) 117 | 118 | #: Concepts 119 | concepts: typing.List[Concept] = field(default_factory=list) 120 | 121 | #: Explicit template instantiations 122 | template_insts: typing.List[TemplateInst] = field(default_factory=list) 123 | 124 | #: Child namespaces 125 | namespaces: typing.Dict[str, "NamespaceScope"] = field(default_factory=dict) 126 | 127 | #: Deduction guides 128 | deduction_guides: typing.List[DeductionGuide] = field(default_factory=list) 129 | 130 | 131 | Block = typing.Union[ClassScope, NamespaceScope] 132 | 133 | 134 | @dataclass 135 | class Pragma: 136 | content: Value 137 | 138 | 139 | @dataclass 140 | class Include: 141 | #: The filename includes the surrounding ``<>`` or ``"`` 142 | filename: str 143 | 144 | 145 | @dataclass 146 | class UsingNamespace: 147 | ns: str 148 | 149 | 150 | @dataclass 151 | class ParsedData: 152 | """ 153 | Container for information parsed by the :func:`parse_file` and 154 | :func:`parse_string` functions. 155 | 156 | .. warning:: Names are not resolved, so items are stored in the scope that 157 | they are found. For example: 158 | 159 | .. code-block:: c++ 160 | 161 | namespace N { 162 | class C; 163 | } 164 | 165 | class N::C { 166 | void fn(); 167 | }; 168 | 169 | The 'C' class would be a forward declaration in the 'N' namespace, 170 | but the ClassDecl for 'C' would be stored in the global 171 | namespace instead of the 'N' namespace. 172 | """ 173 | 174 | #: Global namespace 175 | namespace: NamespaceScope = field(default_factory=lambda: NamespaceScope()) 176 | 177 | #: Any ``#pragma`` directives encountered 178 | pragmas: typing.List[Pragma] = field(default_factory=list) 179 | 180 | #: Any ``#include`` directives encountered 181 | includes: typing.List[Include] = field(default_factory=list) 182 | 183 | 184 | # 185 | # Visitor implementation 186 | # 187 | 188 | # define what user data we store in each state type 189 | SClassBlockState = ClassBlockState[ClassScope, Block] 190 | SExternBlockState = ExternBlockState[NamespaceScope, NamespaceScope] 191 | SNamespaceBlockState = NamespaceBlockState[NamespaceScope, NamespaceScope] 192 | 193 | SState = typing.Union[SClassBlockState, SExternBlockState, SNamespaceBlockState] 194 | SNonClassBlockState = typing.Union[SExternBlockState, SNamespaceBlockState] 195 | 196 | 197 | class SimpleCxxVisitor: 198 | """ 199 | A simple visitor that stores all of the C++ elements passed to it 200 | in an "easy" to use data structure 201 | 202 | You probably don't want to use this directly, use :func:`parse_file` 203 | or :func:`parse_string` instead. 204 | """ 205 | 206 | data: ParsedData 207 | 208 | def on_parse_start(self, state: SNamespaceBlockState) -> None: 209 | ns = NamespaceScope("") 210 | self.data = ParsedData(ns) 211 | state.user_data = ns 212 | 213 | def on_pragma(self, state: SState, content: Value) -> None: 214 | self.data.pragmas.append(Pragma(content)) 215 | 216 | def on_include(self, state: SState, filename: str) -> None: 217 | self.data.includes.append(Include(filename)) 218 | 219 | def on_extern_block_start(self, state: SExternBlockState) -> typing.Optional[bool]: 220 | state.user_data = state.parent.user_data 221 | return None 222 | 223 | def on_extern_block_end(self, state: SExternBlockState) -> None: 224 | pass 225 | 226 | def on_namespace_start(self, state: SNamespaceBlockState) -> typing.Optional[bool]: 227 | parent_ns = state.parent.user_data 228 | 229 | ns = None 230 | names = state.namespace.names 231 | if not names: 232 | # all anonymous namespaces in a translation unit are the same 233 | names = [""] 234 | 235 | for name in names: 236 | ns = parent_ns.namespaces.get(name) 237 | if ns is None: 238 | ns = NamespaceScope(name) 239 | parent_ns.namespaces[name] = ns 240 | parent_ns = ns 241 | 242 | assert ns is not None 243 | 244 | # only set inline/doxygen on inner namespace 245 | ns.inline = state.namespace.inline 246 | ns.doxygen = state.namespace.doxygen 247 | 248 | state.user_data = ns 249 | return None 250 | 251 | def on_namespace_end(self, state: SNamespaceBlockState) -> None: 252 | pass 253 | 254 | def on_concept(self, state: SNonClassBlockState, concept: Concept) -> None: 255 | state.user_data.concepts.append(concept) 256 | 257 | def on_namespace_alias( 258 | self, state: SNonClassBlockState, alias: NamespaceAlias 259 | ) -> None: 260 | state.user_data.ns_alias.append(alias) 261 | 262 | def on_forward_decl(self, state: SState, fdecl: ForwardDecl) -> None: 263 | state.user_data.forward_decls.append(fdecl) 264 | 265 | def on_template_inst(self, state: SState, inst: TemplateInst) -> None: 266 | assert isinstance(state.user_data, NamespaceScope) 267 | state.user_data.template_insts.append(inst) 268 | 269 | def on_variable(self, state: SState, v: Variable) -> None: 270 | assert isinstance(state.user_data, NamespaceScope) 271 | state.user_data.variables.append(v) 272 | 273 | def on_function(self, state: SNonClassBlockState, fn: Function) -> None: 274 | state.user_data.functions.append(fn) 275 | 276 | def on_method_impl(self, state: SNonClassBlockState, method: Method) -> None: 277 | state.user_data.method_impls.append(method) 278 | 279 | def on_typedef(self, state: SState, typedef: Typedef) -> None: 280 | state.user_data.typedefs.append(typedef) 281 | 282 | def on_using_namespace( 283 | self, state: SNonClassBlockState, namespace: typing.List[str] 284 | ) -> None: 285 | ns = UsingNamespace("::".join(namespace)) 286 | state.user_data.using_ns.append(ns) 287 | 288 | def on_using_alias(self, state: SState, using: UsingAlias) -> None: 289 | state.user_data.using_alias.append(using) 290 | 291 | def on_using_declaration(self, state: SState, using: UsingDecl) -> None: 292 | state.user_data.using.append(using) 293 | 294 | # 295 | # Enums 296 | # 297 | 298 | def on_enum(self, state: SState, enum: EnumDecl) -> None: 299 | state.user_data.enums.append(enum) 300 | 301 | # 302 | # Class/union/struct 303 | # 304 | 305 | def on_class_start(self, state: SClassBlockState) -> typing.Optional[bool]: 306 | parent = state.parent.user_data 307 | block = ClassScope(state.class_decl) 308 | parent.classes.append(block) 309 | state.user_data = block 310 | return None 311 | 312 | def on_class_field(self, state: SClassBlockState, f: Field) -> None: 313 | state.user_data.fields.append(f) 314 | 315 | def on_class_method(self, state: SClassBlockState, method: Method) -> None: 316 | state.user_data.methods.append(method) 317 | 318 | def on_class_friend(self, state: SClassBlockState, friend: FriendDecl) -> None: 319 | state.user_data.friends.append(friend) 320 | 321 | def on_class_end(self, state: SClassBlockState) -> None: 322 | pass 323 | 324 | def on_deduction_guide( 325 | self, state: SNonClassBlockState, guide: DeductionGuide 326 | ) -> None: 327 | state.user_data.deduction_guides.append(guide) 328 | 329 | 330 | def parse_string( 331 | content: str, 332 | *, 333 | filename: str = "", 334 | options: typing.Optional[ParserOptions] = None, 335 | cleandoc: bool = False, 336 | ) -> ParsedData: 337 | """ 338 | Simple function to parse a header and return a data structure 339 | """ 340 | if cleandoc: 341 | content = inspect.cleandoc(content) 342 | 343 | visitor = SimpleCxxVisitor() 344 | parser = CxxParser(filename, content, visitor, options) 345 | parser.parse() 346 | 347 | return visitor.data 348 | 349 | 350 | def parse_file( 351 | filename: typing.Union[str, os.PathLike], 352 | encoding: typing.Optional[str] = None, 353 | *, 354 | options: typing.Optional[ParserOptions] = None, 355 | ) -> ParsedData: 356 | """ 357 | Simple function to parse a header from a file and return a data structure 358 | """ 359 | filename = os.fsdecode(filename) 360 | 361 | if encoding is None: 362 | encoding = "utf-8-sig" 363 | 364 | if filename == "-": 365 | content = sys.stdin.read() 366 | else: 367 | content = None 368 | 369 | visitor = SimpleCxxVisitor() 370 | parser = CxxParser(filename, content, visitor, options) 371 | parser.parse() 372 | 373 | return visitor.data 374 | -------------------------------------------------------------------------------- /tests/test_class_bitfields.py: -------------------------------------------------------------------------------- 1 | # Note: testcases generated via `python -m cxxheaderparser.gentest` 2 | 3 | from cxxheaderparser.types import ( 4 | ClassDecl, 5 | Field, 6 | Function, 7 | FundamentalSpecifier, 8 | NameSpecifier, 9 | PQName, 10 | Parameter, 11 | Pointer, 12 | Token, 13 | Type, 14 | Typedef, 15 | Value, 16 | Variable, 17 | ) 18 | from cxxheaderparser.simple import ( 19 | ClassScope, 20 | NamespaceScope, 21 | parse_string, 22 | ParsedData, 23 | ) 24 | 25 | 26 | def test_class_bitfield_1() -> None: 27 | content = """ 28 | struct S { 29 | // will usually occupy 2 bytes: 30 | // 3 bits: value of b1 31 | // 2 bits: unused 32 | // 6 bits: value of b2 33 | // 2 bits: value of b3 34 | // 3 bits: unused 35 | unsigned char b1 : 3, : 2, b2 : 6, b3 : 2; 36 | }; 37 | """ 38 | data = parse_string(content, cleandoc=True) 39 | 40 | assert data == ParsedData( 41 | namespace=NamespaceScope( 42 | classes=[ 43 | ClassScope( 44 | class_decl=ClassDecl( 45 | typename=PQName( 46 | segments=[NameSpecifier(name="S")], classkey="struct" 47 | ) 48 | ), 49 | fields=[ 50 | Field( 51 | name="b1", 52 | type=Type( 53 | typename=PQName( 54 | segments=[ 55 | FundamentalSpecifier(name="unsigned char") 56 | ] 57 | ) 58 | ), 59 | access="public", 60 | bits=3, 61 | ), 62 | Field( 63 | type=Type( 64 | typename=PQName( 65 | segments=[ 66 | FundamentalSpecifier(name="unsigned char") 67 | ] 68 | ) 69 | ), 70 | access="public", 71 | bits=2, 72 | ), 73 | Field( 74 | name="b2", 75 | type=Type( 76 | typename=PQName( 77 | segments=[ 78 | FundamentalSpecifier(name="unsigned char") 79 | ] 80 | ) 81 | ), 82 | access="public", 83 | bits=6, 84 | ), 85 | Field( 86 | name="b3", 87 | type=Type( 88 | typename=PQName( 89 | segments=[ 90 | FundamentalSpecifier(name="unsigned char") 91 | ] 92 | ) 93 | ), 94 | access="public", 95 | bits=2, 96 | ), 97 | ], 98 | ) 99 | ] 100 | ) 101 | ) 102 | 103 | 104 | def test_class_bitfield_2() -> None: 105 | content = """ 106 | struct HAL_ControlWord { 107 | int x : 1; 108 | int y : 1; 109 | }; 110 | typedef struct HAL_ControlWord HAL_ControlWord; 111 | int HAL_GetControlWord(HAL_ControlWord *controlWord); 112 | """ 113 | data = parse_string(content, cleandoc=True) 114 | 115 | assert data == ParsedData( 116 | namespace=NamespaceScope( 117 | classes=[ 118 | ClassScope( 119 | class_decl=ClassDecl( 120 | typename=PQName( 121 | segments=[NameSpecifier(name="HAL_ControlWord")], 122 | classkey="struct", 123 | ) 124 | ), 125 | fields=[ 126 | Field( 127 | name="x", 128 | type=Type( 129 | typename=PQName( 130 | segments=[FundamentalSpecifier(name="int")] 131 | ) 132 | ), 133 | access="public", 134 | bits=1, 135 | ), 136 | Field( 137 | name="y", 138 | type=Type( 139 | typename=PQName( 140 | segments=[FundamentalSpecifier(name="int")] 141 | ) 142 | ), 143 | access="public", 144 | bits=1, 145 | ), 146 | ], 147 | ) 148 | ], 149 | functions=[ 150 | Function( 151 | return_type=Type( 152 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 153 | ), 154 | name=PQName(segments=[NameSpecifier(name="HAL_GetControlWord")]), 155 | parameters=[ 156 | Parameter( 157 | type=Pointer( 158 | ptr_to=Type( 159 | typename=PQName( 160 | segments=[NameSpecifier(name="HAL_ControlWord")] 161 | ) 162 | ) 163 | ), 164 | name="controlWord", 165 | ) 166 | ], 167 | ) 168 | ], 169 | typedefs=[ 170 | Typedef( 171 | type=Type( 172 | typename=PQName( 173 | segments=[NameSpecifier(name="HAL_ControlWord")], 174 | classkey="struct", 175 | ) 176 | ), 177 | name="HAL_ControlWord", 178 | ) 179 | ], 180 | ) 181 | ) 182 | 183 | 184 | def test_class_bitfield_constexpr_1() -> None: 185 | content = """ 186 | static constexpr int BITS = 3; 187 | 188 | struct MyStruct { 189 | int field_one: 1; 190 | int field_two: BITS; 191 | }; 192 | """ 193 | data = parse_string(content, cleandoc=True) 194 | 195 | assert data == ParsedData( 196 | namespace=NamespaceScope( 197 | classes=[ 198 | ClassScope( 199 | class_decl=ClassDecl( 200 | typename=PQName( 201 | segments=[NameSpecifier(name="MyStruct")], classkey="struct" 202 | ) 203 | ), 204 | fields=[ 205 | Field( 206 | access="public", 207 | type=Type( 208 | typename=PQName( 209 | segments=[FundamentalSpecifier(name="int")] 210 | ) 211 | ), 212 | name="field_one", 213 | bits=1, 214 | ), 215 | Field( 216 | access="public", 217 | type=Type( 218 | typename=PQName( 219 | segments=[FundamentalSpecifier(name="int")] 220 | ) 221 | ), 222 | name="field_two", 223 | bits=Value(tokens=[Token(value="BITS")]), 224 | ), 225 | ], 226 | ) 227 | ], 228 | variables=[ 229 | Variable( 230 | name=PQName(segments=[NameSpecifier(name="BITS")]), 231 | type=Type( 232 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 233 | ), 234 | value=Value(tokens=[Token(value="3")]), 235 | constexpr=True, 236 | static=True, 237 | ) 238 | ], 239 | ) 240 | ) 241 | 242 | 243 | def test_class_bitfield_constexpr_fn() -> None: 244 | content = """ 245 | constexpr int f() { return sizeof("duck"); } 246 | 247 | struct MyStruct { 248 | unsigned quack : f(); // 5 bits 249 | }; 250 | """ 251 | data = parse_string(content, cleandoc=True) 252 | 253 | assert data == ParsedData( 254 | namespace=NamespaceScope( 255 | classes=[ 256 | ClassScope( 257 | class_decl=ClassDecl( 258 | typename=PQName( 259 | segments=[NameSpecifier(name="MyStruct")], classkey="struct" 260 | ) 261 | ), 262 | fields=[ 263 | Field( 264 | access="public", 265 | type=Type( 266 | typename=PQName( 267 | segments=[FundamentalSpecifier(name="unsigned")] 268 | ) 269 | ), 270 | name="quack", 271 | bits=Value( 272 | tokens=[ 273 | Token(value="f"), 274 | Token(value="("), 275 | Token(value=")"), 276 | ] 277 | ), 278 | ) 279 | ], 280 | ) 281 | ], 282 | functions=[ 283 | Function( 284 | return_type=Type( 285 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 286 | ), 287 | name=PQName(segments=[NameSpecifier(name="f")]), 288 | parameters=[], 289 | constexpr=True, 290 | has_body=True, 291 | ) 292 | ], 293 | ) 294 | ) 295 | 296 | 297 | def test_class_bitfield_constexpr_expr() -> None: 298 | content = """ 299 | struct MyStruct { 300 | unsigned yes : (true && false) + 7; // 7 bits 301 | unsigned no : (true || false); // 1 bit 302 | }; 303 | """ 304 | data = parse_string(content, cleandoc=True) 305 | 306 | assert data == ParsedData( 307 | namespace=NamespaceScope( 308 | classes=[ 309 | ClassScope( 310 | class_decl=ClassDecl( 311 | typename=PQName( 312 | segments=[NameSpecifier(name="MyStruct")], classkey="struct" 313 | ) 314 | ), 315 | fields=[ 316 | Field( 317 | access="public", 318 | type=Type( 319 | typename=PQName( 320 | segments=[FundamentalSpecifier(name="unsigned")] 321 | ) 322 | ), 323 | name="yes", 324 | bits=Value( 325 | tokens=[ 326 | Token(value="("), 327 | Token(value="true"), 328 | Token(value="&&"), 329 | Token(value="false"), 330 | Token(value=")"), 331 | Token(value="+"), 332 | Token(value="7"), 333 | ] 334 | ), 335 | ), 336 | Field( 337 | access="public", 338 | type=Type( 339 | typename=PQName( 340 | segments=[FundamentalSpecifier(name="unsigned")] 341 | ) 342 | ), 343 | name="no", 344 | bits=Value( 345 | tokens=[ 346 | Token(value="("), 347 | Token(value="true"), 348 | Token(value="||"), 349 | Token(value="false"), 350 | Token(value=")"), 351 | ] 352 | ), 353 | ), 354 | ], 355 | ) 356 | ] 357 | ) 358 | ) 359 | -------------------------------------------------------------------------------- /cxxheaderparser/preprocessor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains optional preprocessor support functions 3 | """ 4 | 5 | import io 6 | import pathlib 7 | import re 8 | import os 9 | import os.path 10 | import subprocess 11 | import sys 12 | import tempfile 13 | import typing 14 | 15 | from .options import PreprocessorFunction 16 | 17 | 18 | class PreprocessorError(Exception): 19 | pass 20 | 21 | 22 | # 23 | # GCC preprocessor support 24 | # 25 | 26 | 27 | def _gcc_filter(fname: str, fp: typing.TextIO) -> str: 28 | new_output = io.StringIO() 29 | keep = True 30 | fname = fname.replace("\\", "\\\\") 31 | 32 | for line in fp: 33 | if line.startswith("# "): 34 | last_quote = line.rfind('"') 35 | if last_quote != -1: 36 | keep = line[:last_quote].endswith(fname) 37 | 38 | if keep: 39 | new_output.write(line) 40 | 41 | new_output.seek(0) 42 | return new_output.read() 43 | 44 | 45 | def make_gcc_preprocessor( 46 | *, 47 | defines: typing.List[str] = [], 48 | include_paths: typing.List[str] = [], 49 | retain_all_content: bool = False, 50 | encoding: typing.Optional[str] = None, 51 | gcc_args: typing.List[str] = ["g++"], 52 | print_cmd: bool = True, 53 | depfile: typing.Optional[pathlib.Path] = None, 54 | deptarget: typing.Optional[typing.List[str]] = None, 55 | ) -> PreprocessorFunction: 56 | """ 57 | Creates a preprocessor function that uses g++ to preprocess the input text. 58 | 59 | gcc is a high performance and accurate precompiler, but if an #include 60 | directive can't be resolved or other oddity exists in your input it will 61 | throw an error. 62 | 63 | :param defines: list of #define macros specified as "key value" 64 | :param include_paths: list of directories to search for included files 65 | :param retain_all_content: If False, only the parsed file content will be retained 66 | :param encoding: If specified any include files are opened with this encoding 67 | :param gcc_args: This is the path to G++ and any extra args you might want 68 | :param print_cmd: Prints the gcc command as its executed 69 | :param depfile: If specified, will generate a preprocessor depfile that contains 70 | a list of include files that were parsed. Must also specify deptarget. 71 | :param deptarget: List of targets to put in the depfile 72 | 73 | .. code-block:: python 74 | 75 | pp = make_gcc_preprocessor() 76 | options = ParserOptions(preprocessor=pp) 77 | 78 | parse_file(content, options=options) 79 | 80 | """ 81 | 82 | if not encoding: 83 | encoding = "utf-8" 84 | 85 | def _preprocess_file(filename: str, content: typing.Optional[str]) -> str: 86 | cmd = gcc_args + ["-w", "-E", "-C"] 87 | 88 | for p in include_paths: 89 | cmd.append(f"-I{p}") 90 | for d in defines: 91 | cmd.append(f"-D{d.replace(' ', '=')}") 92 | 93 | kwargs = {"encoding": encoding} 94 | if filename == "": 95 | cmd.append("-") 96 | filename = "" 97 | if content is None: 98 | raise PreprocessorError("no content specified for stdin") 99 | kwargs["input"] = content 100 | else: 101 | cmd.append(filename) 102 | 103 | if depfile is not None: 104 | if deptarget is None: 105 | raise PreprocessorError( 106 | "must specify deptarget if depfile is specified" 107 | ) 108 | cmd.append("-MD") 109 | for target in deptarget: 110 | cmd += ["-MQ", target] 111 | cmd += ["-MF", str(depfile)] 112 | 113 | if print_cmd: 114 | print("+", " ".join(cmd), file=sys.stderr) 115 | 116 | result: str = subprocess.check_output(cmd, **kwargs) # type: ignore 117 | if not retain_all_content: 118 | result = _gcc_filter(filename, io.StringIO(result)) 119 | 120 | return result 121 | 122 | return _preprocess_file 123 | 124 | 125 | # 126 | # Microsoft Visual Studio preprocessor support 127 | # 128 | 129 | 130 | def _msvc_filter(fp: typing.TextIO) -> str: 131 | # MSVC outputs the original file as the very first #line directive 132 | # so we just use that 133 | new_output = io.StringIO() 134 | keep = True 135 | 136 | first = fp.readline() 137 | assert first.startswith("#line") 138 | fname = first[first.find('"') :] 139 | 140 | for line in fp: 141 | if line.startswith("#line"): 142 | keep = line.endswith(fname) 143 | 144 | if keep: 145 | new_output.write(line) 146 | 147 | new_output.seek(0) 148 | return new_output.read() 149 | 150 | 151 | def make_msvc_preprocessor( 152 | *, 153 | defines: typing.List[str] = [], 154 | include_paths: typing.List[str] = [], 155 | retain_all_content: bool = False, 156 | encoding: typing.Optional[str] = None, 157 | msvc_args: typing.List[str] = ["cl.exe"], 158 | print_cmd: bool = True, 159 | ) -> PreprocessorFunction: 160 | """ 161 | Creates a preprocessor function that uses cl.exe from Microsoft Visual Studio 162 | to preprocess the input text. cl.exe is not typically on the path, so you 163 | may need to open the correct developer tools shell or pass in the correct path 164 | to cl.exe in the `msvc_args` parameter. 165 | 166 | cl.exe will throw an error if a file referenced by an #include directive is not found. 167 | 168 | :param defines: list of #define macros specified as "key value" 169 | :param include_paths: list of directories to search for included files 170 | :param retain_all_content: If False, only the parsed file content will be retained 171 | :param encoding: If specified any include files are opened with this encoding 172 | :param msvc_args: This is the path to cl.exe and any extra args you might want 173 | :param print_cmd: Prints the command as its executed 174 | 175 | .. code-block:: python 176 | 177 | pp = make_msvc_preprocessor() 178 | options = ParserOptions(preprocessor=pp) 179 | 180 | parse_file(content, options=options) 181 | 182 | """ 183 | 184 | if not encoding: 185 | encoding = "utf-8" 186 | 187 | def _preprocess_file(filename: str, content: typing.Optional[str]) -> str: 188 | cmd = msvc_args + ["/nologo", "/E", "/C"] 189 | 190 | for p in include_paths: 191 | cmd.append(f"/I{p}") 192 | for d in defines: 193 | cmd.append(f"/D{d.replace(' ', '=')}") 194 | 195 | tfpname = None 196 | 197 | try: 198 | kwargs = {"encoding": encoding} 199 | if filename == "": 200 | if content is None: 201 | raise PreprocessorError("no content specified for stdin") 202 | 203 | tfp = tempfile.NamedTemporaryFile( 204 | mode="w", encoding=encoding, suffix=".h", delete=False 205 | ) 206 | tfpname = tfp.name 207 | tfp.write(content) 208 | tfp.close() 209 | 210 | cmd.append(tfpname) 211 | else: 212 | cmd.append(filename) 213 | 214 | if print_cmd: 215 | print("+", " ".join(cmd), file=sys.stderr) 216 | 217 | result: str = subprocess.check_output(cmd, **kwargs) # type: ignore 218 | if not retain_all_content: 219 | result = _msvc_filter(io.StringIO(result)) 220 | finally: 221 | if tfpname: 222 | os.unlink(tfpname) 223 | 224 | return result 225 | 226 | return _preprocess_file 227 | 228 | 229 | # 230 | # PCPP preprocessor support (not installed by default) 231 | # 232 | 233 | 234 | try: 235 | import pcpp 236 | from pcpp import Preprocessor, OutputDirective, Action 237 | 238 | class _CustomPreprocessor(Preprocessor): 239 | def __init__( 240 | self, 241 | encoding: typing.Optional[str], 242 | passthru_includes: typing.Optional["re.Pattern"], 243 | ): 244 | Preprocessor.__init__(self) 245 | self.errors: typing.List[str] = [] 246 | self.assume_encoding = encoding 247 | self.passthru_includes = passthru_includes 248 | 249 | def on_error(self, file, line, msg): 250 | self.errors.append(f"{file}:{line} error: {msg}") 251 | 252 | def on_include_not_found(self, *ignored): 253 | raise OutputDirective(Action.IgnoreAndPassThrough) 254 | 255 | def on_comment(self, *ignored): 256 | return True 257 | 258 | except ImportError: 259 | pcpp = None 260 | 261 | 262 | def _pcpp_filter( 263 | fname: str, fp: typing.TextIO, deps: typing.Optional[typing.Dict[str, bool]] 264 | ) -> str: 265 | # the output of pcpp includes the contents of all the included files, which 266 | # isn't what a typical user of cxxheaderparser would want, so we strip out 267 | # the line directives and any content that isn't in our original file 268 | 269 | line_ending = f'{fname}"\n' 270 | 271 | new_output = io.StringIO() 272 | keep = True 273 | 274 | for line in fp: 275 | if line.startswith("#line"): 276 | keep = line.endswith(line_ending) 277 | if deps is not None: 278 | start = line.find('"') 279 | deps[line[start + 1 : -2]] = True 280 | 281 | if keep: 282 | new_output.write(line) 283 | 284 | new_output.seek(0) 285 | return new_output.read() 286 | 287 | 288 | def make_pcpp_preprocessor( 289 | *, 290 | defines: typing.List[str] = [], 291 | include_paths: typing.List[str] = [], 292 | retain_all_content: bool = False, 293 | encoding: typing.Optional[str] = None, 294 | passthru_includes: typing.Optional["re.Pattern"] = None, 295 | depfile: typing.Optional[pathlib.Path] = None, 296 | deptarget: typing.Optional[typing.List[str]] = None, 297 | ) -> PreprocessorFunction: 298 | """ 299 | Creates a preprocessor function that uses pcpp (which must be installed 300 | separately) to preprocess the input text. 301 | 302 | If missing #include files are encountered, this preprocessor will ignore the 303 | error. This preprocessor is pure python so it's very portable, and is a good 304 | choice if performance isn't critical. 305 | 306 | :param defines: list of #define macros specified as "key value" 307 | :param include_paths: list of directories to search for included files 308 | :param retain_all_content: If False, only the parsed file content will be retained 309 | :param encoding: If specified any include files are opened with this encoding 310 | :param passthru_includes: If specified any #include directives that match the 311 | compiled regex pattern will be part of the output. 312 | :param depfile: If specified, will generate a preprocessor depfile that contains 313 | a list of include files that were parsed. Must also specify deptarget. 314 | Not compatible with retain_all_content 315 | :param deptarget: List of targets to put in the depfile 316 | 317 | .. code-block:: python 318 | 319 | pp = make_pcpp_preprocessor() 320 | options = ParserOptions(preprocessor=pp) 321 | 322 | parse_file(content, options=options) 323 | 324 | """ 325 | 326 | if pcpp is None: 327 | raise PreprocessorError("pcpp is not installed") 328 | 329 | def _preprocess_file(filename: str, content: typing.Optional[str]) -> str: 330 | pp = _CustomPreprocessor(encoding, passthru_includes) 331 | if include_paths: 332 | for p in include_paths: 333 | pp.add_path(p) 334 | 335 | for define in defines: 336 | pp.define(define) 337 | 338 | if not retain_all_content: 339 | pp.line_directive = "#line" 340 | elif depfile: 341 | raise PreprocessorError("retain_all_content and depfile not compatible") 342 | 343 | if content is None: 344 | with open(filename, "r", encoding=encoding) as cfp: 345 | content = cfp.read() 346 | 347 | pp.parse(content, filename) 348 | 349 | if pp.errors: 350 | raise PreprocessorError("\n".join(pp.errors)) 351 | elif pp.return_code: 352 | raise PreprocessorError("failed with exit code %d" % pp.return_code) 353 | 354 | fp = io.StringIO() 355 | pp.write(fp) 356 | fp.seek(0) 357 | if retain_all_content: 358 | return fp.read() 359 | else: 360 | deps: typing.Optional[typing.Dict[str, bool]] = None 361 | target = None 362 | if depfile: 363 | deps = {} 364 | if not deptarget: 365 | base, _ = os.path.splitext(filename) 366 | target = f"{base}.o" 367 | else: 368 | target = " ".join(deptarget) 369 | 370 | # pcpp emits the #line directive using the filename you pass in 371 | # but will rewrite it if it's on the include path it uses. This 372 | # is copied from pcpp: 373 | abssource = os.path.abspath(filename) 374 | for rewrite in pp.rewrite_paths: 375 | temp = re.sub(rewrite[0], rewrite[1], abssource) 376 | if temp != abssource: 377 | filename = temp 378 | if os.sep != "/": 379 | filename = filename.replace(os.sep, "/") 380 | break 381 | 382 | filtered = _pcpp_filter(filename, fp, deps) 383 | 384 | if depfile is not None: 385 | assert deps is not None 386 | with open(depfile, "w") as dfp: 387 | dfp.write(f"{target}:") 388 | for dep in reversed(list(deps.keys())): 389 | dep = dep.replace("\\", "\\\\") 390 | dep = dep.replace(" ", "\\ ") 391 | dfp.write(f" \\\n {dep}") 392 | dfp.write("\n") 393 | 394 | return filtered 395 | 396 | return _preprocess_file 397 | -------------------------------------------------------------------------------- /tests/test_friends.py: -------------------------------------------------------------------------------- 1 | # Note: testcases generated via `python -m cxxheaderparser.gentest` 2 | 3 | from cxxheaderparser.types import ( 4 | ClassDecl, 5 | Field, 6 | ForwardDecl, 7 | FriendDecl, 8 | FundamentalSpecifier, 9 | Method, 10 | NameSpecifier, 11 | PQName, 12 | Parameter, 13 | Reference, 14 | TemplateDecl, 15 | TemplateTypeParam, 16 | Type, 17 | ) 18 | from cxxheaderparser.simple import ( 19 | ClassScope, 20 | NamespaceScope, 21 | parse_string, 22 | ParsedData, 23 | ) 24 | 25 | 26 | # friends 27 | def test_various_friends() -> None: 28 | content = """ 29 | class FX { 30 | public: 31 | FX(char); 32 | ~FX(); 33 | void fn() const; 34 | }; 35 | 36 | class FF { 37 | friend class FX; 38 | friend FX::FX(char), FX::~FX(); 39 | friend void FX::fn() const; 40 | }; 41 | """ 42 | data = parse_string(content, cleandoc=True) 43 | 44 | assert data == ParsedData( 45 | namespace=NamespaceScope( 46 | classes=[ 47 | ClassScope( 48 | class_decl=ClassDecl( 49 | typename=PQName( 50 | segments=[NameSpecifier(name="FX")], classkey="class" 51 | ) 52 | ), 53 | methods=[ 54 | Method( 55 | return_type=None, 56 | name=PQName(segments=[NameSpecifier(name="FX")]), 57 | parameters=[ 58 | Parameter( 59 | type=Type( 60 | typename=PQName( 61 | segments=[FundamentalSpecifier(name="char")] 62 | ) 63 | ) 64 | ) 65 | ], 66 | access="public", 67 | constructor=True, 68 | ), 69 | Method( 70 | return_type=None, 71 | name=PQName(segments=[NameSpecifier(name="~FX")]), 72 | parameters=[], 73 | access="public", 74 | destructor=True, 75 | ), 76 | Method( 77 | return_type=Type( 78 | typename=PQName( 79 | segments=[FundamentalSpecifier(name="void")] 80 | ) 81 | ), 82 | name=PQName(segments=[NameSpecifier(name="fn")]), 83 | parameters=[], 84 | access="public", 85 | const=True, 86 | ), 87 | ], 88 | ), 89 | ClassScope( 90 | class_decl=ClassDecl( 91 | typename=PQName( 92 | segments=[NameSpecifier(name="FF")], classkey="class" 93 | ) 94 | ), 95 | friends=[ 96 | FriendDecl( 97 | cls=ForwardDecl( 98 | typename=PQName( 99 | segments=[NameSpecifier(name="FX")], 100 | classkey="class", 101 | ), 102 | access="private", 103 | ) 104 | ), 105 | FriendDecl( 106 | fn=Method( 107 | return_type=None, 108 | name=PQName( 109 | segments=[ 110 | NameSpecifier(name="FX"), 111 | NameSpecifier(name="FX"), 112 | ] 113 | ), 114 | parameters=[ 115 | Parameter( 116 | type=Type( 117 | typename=PQName( 118 | segments=[ 119 | FundamentalSpecifier(name="char") 120 | ] 121 | ) 122 | ) 123 | ) 124 | ], 125 | access="private", 126 | constructor=True, 127 | ) 128 | ), 129 | FriendDecl( 130 | fn=Method( 131 | return_type=Type( 132 | typename=PQName( 133 | segments=[ 134 | NameSpecifier(name="FX"), 135 | NameSpecifier(name="FX"), 136 | ] 137 | ) 138 | ), 139 | name=PQName( 140 | segments=[ 141 | NameSpecifier(name="FX"), 142 | NameSpecifier(name="~FX"), 143 | ] 144 | ), 145 | parameters=[], 146 | access="private", 147 | ) 148 | ), 149 | FriendDecl( 150 | fn=Method( 151 | return_type=Type( 152 | typename=PQName( 153 | segments=[FundamentalSpecifier(name="void")] 154 | ) 155 | ), 156 | name=PQName( 157 | segments=[ 158 | NameSpecifier(name="FX"), 159 | NameSpecifier(name="fn"), 160 | ] 161 | ), 162 | parameters=[], 163 | access="private", 164 | const=True, 165 | ) 166 | ), 167 | ], 168 | ), 169 | ] 170 | ) 171 | ) 172 | 173 | 174 | def test_more_friends() -> None: 175 | content = """ 176 | template struct X { static int x; }; 177 | 178 | struct BFF { 179 | void fn() const; 180 | }; 181 | 182 | struct F { 183 | friend enum B; 184 | friend void BFF::fn() const; 185 | 186 | template friend class X; 187 | }; 188 | """ 189 | data = parse_string(content, cleandoc=True) 190 | 191 | assert data == ParsedData( 192 | namespace=NamespaceScope( 193 | classes=[ 194 | ClassScope( 195 | class_decl=ClassDecl( 196 | typename=PQName( 197 | segments=[NameSpecifier(name="X")], classkey="struct" 198 | ), 199 | template=TemplateDecl( 200 | params=[TemplateTypeParam(typekey="typename", name="T")] 201 | ), 202 | ), 203 | fields=[ 204 | Field( 205 | name="x", 206 | type=Type( 207 | typename=PQName( 208 | segments=[FundamentalSpecifier(name="int")] 209 | ) 210 | ), 211 | access="public", 212 | static=True, 213 | ) 214 | ], 215 | ), 216 | ClassScope( 217 | class_decl=ClassDecl( 218 | typename=PQName( 219 | segments=[NameSpecifier(name="BFF")], classkey="struct" 220 | ) 221 | ), 222 | methods=[ 223 | Method( 224 | return_type=Type( 225 | typename=PQName( 226 | segments=[FundamentalSpecifier(name="void")] 227 | ) 228 | ), 229 | name=PQName(segments=[NameSpecifier(name="fn")]), 230 | parameters=[], 231 | access="public", 232 | const=True, 233 | ) 234 | ], 235 | ), 236 | ClassScope( 237 | class_decl=ClassDecl( 238 | typename=PQName( 239 | segments=[NameSpecifier(name="F")], classkey="struct" 240 | ) 241 | ), 242 | friends=[ 243 | FriendDecl( 244 | cls=ForwardDecl( 245 | typename=PQName( 246 | segments=[NameSpecifier(name="B")], classkey="enum" 247 | ), 248 | access="public", 249 | ) 250 | ), 251 | FriendDecl( 252 | fn=Method( 253 | return_type=Type( 254 | typename=PQName( 255 | segments=[FundamentalSpecifier(name="void")] 256 | ) 257 | ), 258 | name=PQName( 259 | segments=[ 260 | NameSpecifier(name="BFF"), 261 | NameSpecifier(name="fn"), 262 | ] 263 | ), 264 | parameters=[], 265 | access="public", 266 | const=True, 267 | ) 268 | ), 269 | FriendDecl( 270 | cls=ForwardDecl( 271 | typename=PQName( 272 | segments=[NameSpecifier(name="X")], classkey="class" 273 | ), 274 | template=TemplateDecl( 275 | params=[ 276 | TemplateTypeParam(typekey="typename", name="T") 277 | ] 278 | ), 279 | access="public", 280 | ) 281 | ), 282 | ], 283 | ), 284 | ] 285 | ) 286 | ) 287 | 288 | 289 | def test_friend_type_no_class() -> None: 290 | content = """ 291 | class DogClass; 292 | class CatClass { 293 | friend DogClass; 294 | }; 295 | 296 | """ 297 | data = parse_string(content, cleandoc=True) 298 | 299 | assert data == ParsedData( 300 | namespace=NamespaceScope( 301 | classes=[ 302 | ClassScope( 303 | class_decl=ClassDecl( 304 | typename=PQName( 305 | segments=[NameSpecifier(name="CatClass")], classkey="class" 306 | ) 307 | ), 308 | friends=[ 309 | FriendDecl( 310 | cls=ForwardDecl( 311 | typename=PQName( 312 | segments=[NameSpecifier(name="DogClass")] 313 | ), 314 | access="private", 315 | ) 316 | ) 317 | ], 318 | ) 319 | ], 320 | forward_decls=[ 321 | ForwardDecl( 322 | typename=PQName( 323 | segments=[NameSpecifier(name="DogClass")], classkey="class" 324 | ) 325 | ) 326 | ], 327 | ) 328 | ) 329 | 330 | 331 | def test_friend_with_impl() -> None: 332 | content = """ 333 | // clang-format off 334 | class Garlic { 335 | public: 336 | friend int genNum(C& a) 337 | { 338 | return obj.meth().num(); 339 | } 340 | }; 341 | 342 | """ 343 | data = parse_string(content, cleandoc=True) 344 | 345 | assert data == ParsedData( 346 | namespace=NamespaceScope( 347 | classes=[ 348 | ClassScope( 349 | class_decl=ClassDecl( 350 | typename=PQName( 351 | segments=[NameSpecifier(name="Garlic")], classkey="class" 352 | ) 353 | ), 354 | friends=[ 355 | FriendDecl( 356 | fn=Method( 357 | return_type=Type( 358 | typename=PQName( 359 | segments=[FundamentalSpecifier(name="int")] 360 | ) 361 | ), 362 | name=PQName(segments=[NameSpecifier(name="genNum")]), 363 | parameters=[ 364 | Parameter( 365 | type=Reference( 366 | ref_to=Type( 367 | typename=PQName( 368 | segments=[NameSpecifier(name="C")] 369 | ) 370 | ) 371 | ), 372 | name="a", 373 | ) 374 | ], 375 | has_body=True, 376 | access="public", 377 | ) 378 | ) 379 | ], 380 | ) 381 | ] 382 | ) 383 | ) 384 | -------------------------------------------------------------------------------- /tests/test_doxygen.py: -------------------------------------------------------------------------------- 1 | # Note: testcases generated via `python -m cxxheaderparser.gentest` 2 | 3 | from cxxheaderparser.types import ( 4 | AnonymousName, 5 | Array, 6 | BaseClass, 7 | ClassDecl, 8 | EnumDecl, 9 | Enumerator, 10 | Field, 11 | ForwardDecl, 12 | Function, 13 | FundamentalSpecifier, 14 | Method, 15 | MoveReference, 16 | NameSpecifier, 17 | PQName, 18 | Parameter, 19 | Pointer, 20 | Reference, 21 | TemplateArgument, 22 | TemplateDecl, 23 | TemplateSpecialization, 24 | TemplateTypeParam, 25 | Token, 26 | Type, 27 | Typedef, 28 | UsingDecl, 29 | UsingAlias, 30 | Value, 31 | Variable, 32 | ) 33 | from cxxheaderparser.simple import ( 34 | ClassScope, 35 | NamespaceScope, 36 | parse_string, 37 | ParsedData, 38 | ) 39 | 40 | 41 | def test_doxygen_class() -> None: 42 | content = """ 43 | // clang-format off 44 | 45 | /// cls comment 46 | class 47 | C { 48 | /// member comment 49 | void fn(); 50 | 51 | /// var above 52 | int var_above; 53 | 54 | int var_after; /// var after 55 | }; 56 | """ 57 | data = parse_string(content, cleandoc=True) 58 | 59 | assert data == ParsedData( 60 | namespace=NamespaceScope( 61 | classes=[ 62 | ClassScope( 63 | class_decl=ClassDecl( 64 | typename=PQName( 65 | segments=[NameSpecifier(name="C")], classkey="class" 66 | ), 67 | doxygen="/// cls comment", 68 | ), 69 | fields=[ 70 | Field( 71 | access="private", 72 | type=Type( 73 | typename=PQName( 74 | segments=[FundamentalSpecifier(name="int")] 75 | ) 76 | ), 77 | name="var_above", 78 | doxygen="/// var above", 79 | ), 80 | Field( 81 | access="private", 82 | type=Type( 83 | typename=PQName( 84 | segments=[FundamentalSpecifier(name="int")] 85 | ) 86 | ), 87 | name="var_after", 88 | doxygen="/// var after", 89 | ), 90 | ], 91 | methods=[ 92 | Method( 93 | return_type=Type( 94 | typename=PQName( 95 | segments=[FundamentalSpecifier(name="void")] 96 | ) 97 | ), 98 | name=PQName(segments=[NameSpecifier(name="fn")]), 99 | parameters=[], 100 | doxygen="/// member comment", 101 | access="private", 102 | ) 103 | ], 104 | ) 105 | ] 106 | ) 107 | ) 108 | 109 | 110 | def test_doxygen_class_template() -> None: 111 | content = """ 112 | // clang-format off 113 | 114 | /// template comment 115 | template 116 | class C2 {}; 117 | """ 118 | data = parse_string(content, cleandoc=True) 119 | 120 | assert data == ParsedData( 121 | namespace=NamespaceScope( 122 | classes=[ 123 | ClassScope( 124 | class_decl=ClassDecl( 125 | typename=PQName( 126 | segments=[NameSpecifier(name="C2")], classkey="class" 127 | ), 128 | template=TemplateDecl( 129 | params=[TemplateTypeParam(typekey="typename", name="T")] 130 | ), 131 | doxygen="/// template comment", 132 | ) 133 | ) 134 | ] 135 | ) 136 | ) 137 | 138 | 139 | def test_doxygen_enum() -> None: 140 | content = """ 141 | // clang-format off 142 | 143 | /// 144 | /// @brief Rino Numbers, not that that means anything 145 | /// 146 | typedef enum 147 | { 148 | RI_ZERO, /// item zero 149 | RI_ONE, /** item one */ 150 | RI_TWO, //!< item two 151 | RI_THREE, 152 | /// item four 153 | RI_FOUR, 154 | } Rino; 155 | """ 156 | data = parse_string(content, cleandoc=True) 157 | 158 | assert data == ParsedData( 159 | namespace=NamespaceScope( 160 | enums=[ 161 | EnumDecl( 162 | typename=PQName(segments=[AnonymousName(id=1)], classkey="enum"), 163 | values=[ 164 | Enumerator(name="RI_ZERO", doxygen="/// item zero"), 165 | Enumerator(name="RI_ONE", doxygen="/** item one */"), 166 | Enumerator(name="RI_TWO", doxygen="//!< item two"), 167 | Enumerator(name="RI_THREE"), 168 | Enumerator(name="RI_FOUR", doxygen="/// item four"), 169 | ], 170 | doxygen="///\n/// @brief Rino Numbers, not that that means anything\n///", 171 | ) 172 | ], 173 | typedefs=[ 174 | Typedef( 175 | type=Type( 176 | typename=PQName(segments=[AnonymousName(id=1)], classkey="enum") 177 | ), 178 | name="Rino", 179 | ) 180 | ], 181 | ) 182 | ) 183 | 184 | 185 | def test_doxygen_fn_3slash() -> None: 186 | content = """ 187 | // clang-format off 188 | 189 | /// fn comment 190 | void 191 | fn(); 192 | 193 | """ 194 | data = parse_string(content, cleandoc=True) 195 | 196 | assert data == ParsedData( 197 | namespace=NamespaceScope( 198 | functions=[ 199 | Function( 200 | return_type=Type( 201 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 202 | ), 203 | name=PQName(segments=[NameSpecifier(name="fn")]), 204 | parameters=[], 205 | doxygen="/// fn comment", 206 | ) 207 | ] 208 | ) 209 | ) 210 | 211 | 212 | def test_doxygen_fn_cstyle1() -> None: 213 | content = """ 214 | /** 215 | * fn comment 216 | */ 217 | void 218 | fn(); 219 | """ 220 | data = parse_string(content, cleandoc=True) 221 | 222 | assert data == ParsedData( 223 | namespace=NamespaceScope( 224 | functions=[ 225 | Function( 226 | return_type=Type( 227 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 228 | ), 229 | name=PQName(segments=[NameSpecifier(name="fn")]), 230 | parameters=[], 231 | doxygen="/**\n* fn comment\n*/", 232 | ) 233 | ] 234 | ) 235 | ) 236 | 237 | 238 | def test_doxygen_fn_cstyle2() -> None: 239 | content = """ 240 | /*! 241 | * fn comment 242 | */ 243 | void 244 | fn(); 245 | """ 246 | data = parse_string(content, cleandoc=True) 247 | 248 | assert data == ParsedData( 249 | namespace=NamespaceScope( 250 | functions=[ 251 | Function( 252 | return_type=Type( 253 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 254 | ), 255 | name=PQName(segments=[NameSpecifier(name="fn")]), 256 | parameters=[], 257 | doxygen="/*!\n* fn comment\n*/", 258 | ) 259 | ] 260 | ) 261 | ) 262 | 263 | 264 | def test_doxygen_var_above() -> None: 265 | content = """ 266 | // clang-format off 267 | 268 | 269 | /// var comment 270 | int 271 | v1 = 0; 272 | 273 | 274 | """ 275 | data = parse_string(content, cleandoc=True) 276 | 277 | assert data == ParsedData( 278 | namespace=NamespaceScope( 279 | variables=[ 280 | Variable( 281 | name=PQName(segments=[NameSpecifier(name="v1")]), 282 | type=Type( 283 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 284 | ), 285 | value=Value(tokens=[Token(value="0")]), 286 | doxygen="/// var comment", 287 | ) 288 | ] 289 | ) 290 | ) 291 | 292 | 293 | def test_doxygen_var_after() -> None: 294 | content = """ 295 | // clang-format off 296 | 297 | int 298 | v2 = 0; /// var2 comment 299 | """ 300 | data = parse_string(content, cleandoc=True) 301 | 302 | assert data == ParsedData( 303 | namespace=NamespaceScope( 304 | variables=[ 305 | Variable( 306 | name=PQName(segments=[NameSpecifier(name="v2")]), 307 | type=Type( 308 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 309 | ), 310 | value=Value(tokens=[Token(value="0")]), 311 | doxygen="/// var2 comment", 312 | ) 313 | ] 314 | ) 315 | ) 316 | 317 | 318 | def test_doxygen_multiple_variables() -> None: 319 | content = """ 320 | int x; /// this is x 321 | int y; /// this is y 322 | /// this is also y 323 | int z; /// this is z 324 | """ 325 | data = parse_string(content, cleandoc=True) 326 | 327 | assert data == ParsedData( 328 | namespace=NamespaceScope( 329 | variables=[ 330 | Variable( 331 | name=PQName(segments=[NameSpecifier(name="x")]), 332 | type=Type( 333 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 334 | ), 335 | doxygen="/// this is x", 336 | ), 337 | Variable( 338 | name=PQName(segments=[NameSpecifier(name="y")]), 339 | type=Type( 340 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 341 | ), 342 | doxygen="/// this is y\n/// this is also y", 343 | ), 344 | Variable( 345 | name=PQName(segments=[NameSpecifier(name="z")]), 346 | type=Type( 347 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 348 | ), 349 | doxygen="/// this is z", 350 | ), 351 | ] 352 | ) 353 | ) 354 | 355 | 356 | def test_doxygen_namespace() -> None: 357 | content = """ 358 | /** 359 | * x is a mysterious namespace 360 | */ 361 | namespace x {} 362 | 363 | /** 364 | * c is also a mysterious namespace 365 | */ 366 | namespace a::b::c {} 367 | """ 368 | data = parse_string(content, cleandoc=True) 369 | 370 | assert data == ParsedData( 371 | namespace=NamespaceScope( 372 | namespaces={ 373 | "x": NamespaceScope( 374 | name="x", doxygen="/**\n* x is a mysterious namespace\n*/" 375 | ), 376 | "a": NamespaceScope( 377 | name="a", 378 | namespaces={ 379 | "b": NamespaceScope( 380 | name="b", 381 | namespaces={ 382 | "c": NamespaceScope( 383 | name="c", 384 | doxygen="/**\n* c is also a mysterious namespace\n*/", 385 | ) 386 | }, 387 | ) 388 | }, 389 | ), 390 | } 391 | ) 392 | ) 393 | 394 | 395 | def test_doxygen_declspec() -> None: 396 | content = """ 397 | /// declspec comment 398 | __declspec(thread) int i = 1; 399 | """ 400 | data = parse_string(content, cleandoc=True) 401 | 402 | assert data == ParsedData( 403 | namespace=NamespaceScope( 404 | variables=[ 405 | Variable( 406 | name=PQName(segments=[NameSpecifier(name="i")]), 407 | type=Type( 408 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 409 | ), 410 | value=Value(tokens=[Token(value="1")]), 411 | doxygen="/// declspec comment", 412 | ) 413 | ] 414 | ) 415 | ) 416 | 417 | 418 | def test_doxygen_attribute() -> None: 419 | content = """ 420 | /// hasattr comment 421 | [[nodiscard]] 422 | int hasattr(); 423 | """ 424 | data = parse_string(content, cleandoc=True) 425 | 426 | assert data == ParsedData( 427 | namespace=NamespaceScope( 428 | functions=[ 429 | Function( 430 | return_type=Type( 431 | typename=PQName(segments=[FundamentalSpecifier(name="int")]) 432 | ), 433 | name=PQName(segments=[NameSpecifier(name="hasattr")]), 434 | parameters=[], 435 | doxygen="/// hasattr comment", 436 | ) 437 | ] 438 | ) 439 | ) 440 | 441 | 442 | def test_doxygen_using_decl() -> None: 443 | content = """ 444 | // clang-format off 445 | 446 | /// Comment 447 | using ns::ClassName; 448 | """ 449 | data = parse_string(content, cleandoc=True) 450 | 451 | assert data == ParsedData( 452 | namespace=NamespaceScope( 453 | using=[ 454 | UsingDecl( 455 | typename=PQName( 456 | segments=[ 457 | NameSpecifier(name="ns"), 458 | NameSpecifier(name="ClassName"), 459 | ] 460 | ), 461 | doxygen="/// Comment", 462 | ) 463 | ] 464 | ) 465 | ) 466 | 467 | 468 | def test_doxygen_using_alias() -> None: 469 | content = """ 470 | // clang-format off 471 | 472 | /// Comment 473 | using alias = sometype; 474 | """ 475 | data = parse_string(content, cleandoc=True) 476 | 477 | assert data == ParsedData( 478 | namespace=NamespaceScope( 479 | using_alias=[ 480 | UsingAlias( 481 | alias="alias", 482 | type=Type( 483 | typename=PQName(segments=[NameSpecifier(name="sometype")]) 484 | ), 485 | doxygen="/// Comment", 486 | ) 487 | ] 488 | ) 489 | ) 490 | -------------------------------------------------------------------------------- /tests/test_abv_template.py: -------------------------------------------------------------------------------- 1 | # Note: testcases generated via `python -m cxxheaderparser.gentest` 2 | # 3 | # Tests various aspects of abbreviated function templates 4 | # 5 | 6 | from cxxheaderparser.simple import NamespaceScope, ParsedData, parse_string 7 | from cxxheaderparser.types import ( 8 | AutoSpecifier, 9 | Function, 10 | FundamentalSpecifier, 11 | NameSpecifier, 12 | PQName, 13 | Parameter, 14 | Pointer, 15 | Reference, 16 | TemplateDecl, 17 | TemplateNonTypeParam, 18 | Type, 19 | ) 20 | 21 | 22 | def test_abv_template_f1() -> None: 23 | content = """ 24 | void f1(auto); // same as template void f1(T) 25 | void f1p(auto p); 26 | """ 27 | data = parse_string(content, cleandoc=True) 28 | 29 | assert data == ParsedData( 30 | namespace=NamespaceScope( 31 | functions=[ 32 | Function( 33 | return_type=Type( 34 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 35 | ), 36 | name=PQName(segments=[NameSpecifier(name="f1")]), 37 | parameters=[ 38 | Parameter( 39 | type=Type(typename=PQName(segments=[AutoSpecifier()])) 40 | ) 41 | ], 42 | template=TemplateDecl( 43 | params=[ 44 | TemplateNonTypeParam( 45 | type=Type(typename=PQName(segments=[AutoSpecifier()])), 46 | param_idx=0, 47 | ) 48 | ] 49 | ), 50 | ), 51 | Function( 52 | return_type=Type( 53 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 54 | ), 55 | name=PQName(segments=[NameSpecifier(name="f1p")]), 56 | parameters=[ 57 | Parameter( 58 | type=Type(typename=PQName(segments=[AutoSpecifier()])), 59 | name="p", 60 | ) 61 | ], 62 | template=TemplateDecl( 63 | params=[ 64 | TemplateNonTypeParam( 65 | type=Type(typename=PQName(segments=[AutoSpecifier()])), 66 | param_idx=0, 67 | ) 68 | ] 69 | ), 70 | ), 71 | ] 72 | ) 73 | ) 74 | 75 | 76 | def test_abv_template_f2() -> None: 77 | content = """ 78 | void f2(C1 auto); // same as template void f2(T), if C1 is a concept 79 | void f2p(C1 auto p); 80 | """ 81 | data = parse_string(content, cleandoc=True) 82 | 83 | assert data == ParsedData( 84 | namespace=NamespaceScope( 85 | functions=[ 86 | Function( 87 | return_type=Type( 88 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 89 | ), 90 | name=PQName(segments=[NameSpecifier(name="f2")]), 91 | parameters=[ 92 | Parameter( 93 | type=Type(typename=PQName(segments=[AutoSpecifier()])) 94 | ) 95 | ], 96 | template=TemplateDecl( 97 | params=[ 98 | TemplateNonTypeParam( 99 | type=Type( 100 | typename=PQName(segments=[NameSpecifier(name="C1")]) 101 | ), 102 | param_idx=0, 103 | ) 104 | ] 105 | ), 106 | ), 107 | Function( 108 | return_type=Type( 109 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 110 | ), 111 | name=PQName(segments=[NameSpecifier(name="f2p")]), 112 | parameters=[ 113 | Parameter( 114 | type=Type(typename=PQName(segments=[AutoSpecifier()])), 115 | name="p", 116 | ) 117 | ], 118 | template=TemplateDecl( 119 | params=[ 120 | TemplateNonTypeParam( 121 | type=Type( 122 | typename=PQName(segments=[NameSpecifier(name="C1")]) 123 | ), 124 | param_idx=0, 125 | ) 126 | ] 127 | ), 128 | ), 129 | ] 130 | ) 131 | ) 132 | 133 | 134 | def test_abv_template_f3() -> None: 135 | content = """ 136 | void f3(C2 auto...); // same as template void f3(Ts...), if C2 is a 137 | // concept 138 | void f3p(C2 auto p...); 139 | """ 140 | data = parse_string(content, cleandoc=True) 141 | 142 | assert data == ParsedData( 143 | namespace=NamespaceScope( 144 | functions=[ 145 | Function( 146 | return_type=Type( 147 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 148 | ), 149 | name=PQName(segments=[NameSpecifier(name="f3")]), 150 | parameters=[ 151 | Parameter( 152 | type=Type(typename=PQName(segments=[AutoSpecifier()])), 153 | param_pack=True, 154 | ) 155 | ], 156 | template=TemplateDecl( 157 | params=[ 158 | TemplateNonTypeParam( 159 | type=Type( 160 | typename=PQName(segments=[NameSpecifier(name="C2")]) 161 | ), 162 | param_idx=0, 163 | param_pack=True, 164 | ) 165 | ] 166 | ), 167 | ), 168 | Function( 169 | return_type=Type( 170 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 171 | ), 172 | name=PQName(segments=[NameSpecifier(name="f3p")]), 173 | parameters=[ 174 | Parameter( 175 | type=Type(typename=PQName(segments=[AutoSpecifier()])), 176 | name="p", 177 | param_pack=True, 178 | ) 179 | ], 180 | template=TemplateDecl( 181 | params=[ 182 | TemplateNonTypeParam( 183 | type=Type( 184 | typename=PQName(segments=[NameSpecifier(name="C2")]) 185 | ), 186 | param_idx=0, 187 | param_pack=True, 188 | ) 189 | ] 190 | ), 191 | ), 192 | ] 193 | ) 194 | ) 195 | 196 | 197 | def test_abv_template_f4() -> None: 198 | content = """ 199 | void f4(C2 auto, ...); // same as template void f4(T...), if C2 is a concept 200 | void f4p(C2 auto p,...); 201 | """ 202 | data = parse_string(content, cleandoc=True) 203 | 204 | assert data == ParsedData( 205 | namespace=NamespaceScope( 206 | functions=[ 207 | Function( 208 | return_type=Type( 209 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 210 | ), 211 | name=PQName(segments=[NameSpecifier(name="f4")]), 212 | parameters=[ 213 | Parameter( 214 | type=Type(typename=PQName(segments=[AutoSpecifier()])) 215 | ) 216 | ], 217 | vararg=True, 218 | template=TemplateDecl( 219 | params=[ 220 | TemplateNonTypeParam( 221 | type=Type( 222 | typename=PQName(segments=[NameSpecifier(name="C2")]) 223 | ), 224 | param_idx=0, 225 | ) 226 | ] 227 | ), 228 | ), 229 | Function( 230 | return_type=Type( 231 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 232 | ), 233 | name=PQName(segments=[NameSpecifier(name="f4p")]), 234 | parameters=[ 235 | Parameter( 236 | type=Type(typename=PQName(segments=[AutoSpecifier()])), 237 | name="p", 238 | ) 239 | ], 240 | vararg=True, 241 | template=TemplateDecl( 242 | params=[ 243 | TemplateNonTypeParam( 244 | type=Type( 245 | typename=PQName(segments=[NameSpecifier(name="C2")]) 246 | ), 247 | param_idx=0, 248 | ) 249 | ] 250 | ), 251 | ), 252 | ] 253 | ) 254 | ) 255 | 256 | 257 | def test_abv_template_f5() -> None: 258 | content = """ 259 | void f5(const C3 auto *, C4 auto &); // same as template void f5(const T*, U&); 260 | void f5p(const C3 auto * p1, C4 auto &p2); 261 | """ 262 | data = parse_string(content, cleandoc=True) 263 | 264 | assert data == ParsedData( 265 | namespace=NamespaceScope( 266 | functions=[ 267 | Function( 268 | return_type=Type( 269 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 270 | ), 271 | name=PQName(segments=[NameSpecifier(name="f5")]), 272 | parameters=[ 273 | Parameter( 274 | type=Pointer( 275 | ptr_to=Type( 276 | typename=PQName( 277 | segments=[AutoSpecifier()], 278 | ), 279 | const=True, 280 | ) 281 | ) 282 | ), 283 | Parameter( 284 | type=Reference( 285 | ref_to=Type(typename=PQName(segments=[AutoSpecifier()])) 286 | ) 287 | ), 288 | ], 289 | template=TemplateDecl( 290 | params=[ 291 | TemplateNonTypeParam( 292 | type=Type( 293 | typename=PQName( 294 | segments=[NameSpecifier(name="C3")] 295 | ), 296 | ), 297 | param_idx=0, 298 | ), 299 | TemplateNonTypeParam( 300 | type=Type( 301 | typename=PQName(segments=[NameSpecifier(name="C4")]) 302 | ), 303 | param_idx=1, 304 | ), 305 | ] 306 | ), 307 | ), 308 | Function( 309 | return_type=Type( 310 | typename=PQName(segments=[FundamentalSpecifier(name="void")]) 311 | ), 312 | name=PQName(segments=[NameSpecifier(name="f5p")]), 313 | parameters=[ 314 | Parameter( 315 | type=Pointer( 316 | ptr_to=Type( 317 | typename=PQName( 318 | segments=[AutoSpecifier()], 319 | ), 320 | const=True, 321 | ) 322 | ), 323 | name="p1", 324 | ), 325 | Parameter( 326 | type=Reference( 327 | ref_to=Type(typename=PQName(segments=[AutoSpecifier()])) 328 | ), 329 | name="p2", 330 | ), 331 | ], 332 | template=TemplateDecl( 333 | params=[ 334 | TemplateNonTypeParam( 335 | type=Type( 336 | typename=PQName( 337 | segments=[NameSpecifier(name="C3")] 338 | ), 339 | ), 340 | param_idx=0, 341 | ), 342 | TemplateNonTypeParam( 343 | type=Type( 344 | typename=PQName(segments=[NameSpecifier(name="C4")]) 345 | ), 346 | param_idx=1, 347 | ), 348 | ] 349 | ), 350 | ), 351 | ] 352 | ) 353 | ) 354 | 355 | 356 | def test_returned_abv_template() -> None: 357 | content = """ 358 | constexpr std::signed_integral auto FloorDiv(std::signed_integral auto x, 359 | std::signed_integral auto y); 360 | """ 361 | data = parse_string(content, cleandoc=True) 362 | 363 | assert data == ParsedData( 364 | namespace=NamespaceScope( 365 | functions=[ 366 | Function( 367 | return_type=Type( 368 | typename=PQName( 369 | segments=[ 370 | NameSpecifier(name="std"), 371 | NameSpecifier(name="signed_integral"), 372 | ] 373 | ) 374 | ), 375 | name=PQName(segments=[NameSpecifier(name="FloorDiv")]), 376 | parameters=[ 377 | Parameter( 378 | type=Type(typename=PQName(segments=[AutoSpecifier()])), 379 | name="x", 380 | ), 381 | Parameter( 382 | type=Type(typename=PQName(segments=[AutoSpecifier()])), 383 | name="y", 384 | ), 385 | ], 386 | constexpr=True, 387 | template=TemplateDecl( 388 | params=[ 389 | TemplateNonTypeParam( 390 | type=Type( 391 | typename=PQName( 392 | segments=[ 393 | NameSpecifier(name="std"), 394 | NameSpecifier(name="signed_integral"), 395 | ] 396 | ) 397 | ), 398 | param_idx=-1, 399 | ), 400 | TemplateNonTypeParam( 401 | type=Type( 402 | typename=PQName( 403 | segments=[ 404 | NameSpecifier(name="std"), 405 | NameSpecifier(name="signed_integral"), 406 | ] 407 | ) 408 | ), 409 | param_idx=0, 410 | ), 411 | TemplateNonTypeParam( 412 | type=Type( 413 | typename=PQName( 414 | segments=[ 415 | NameSpecifier(name="std"), 416 | NameSpecifier(name="signed_integral"), 417 | ] 418 | ) 419 | ), 420 | param_idx=1, 421 | ), 422 | ] 423 | ), 424 | ) 425 | ] 426 | ) 427 | ) 428 | --------------------------------------------------------------------------------