Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 85 additions & 36 deletions scripts/cxx-api/parser/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@
import argparse
import os
import subprocess
import sys
import tempfile

from .config import parse_config_file
from .main import build_snapshot
from .path_utils import get_react_native_dir
from .snapshot_diff import check_snapshots

DOXYGEN_CONFIG_FILE = ".doxygen.config.generated"

RUN_ON_REACT_NATIVE = False


def build_doxygen_config(
directory: str,
Expand Down Expand Up @@ -75,10 +76,11 @@ def build_snapshot_for_view(
exclude_patterns: list[str],
definitions: dict[str, str | int],
output_dir: str,
verbose: bool = True,
) -> None:
print(f"Generating API view: {api_view}")
# Generate the Doxygen config file
print("Generating Doxygen config file")
if verbose:
print(f"Generating API view: {api_view}")
print("Generating Doxygen config file")

build_doxygen_config(
react_native_dir,
Expand All @@ -89,10 +91,12 @@ def build_snapshot_for_view(

# If there is already a doxygen output directory, delete it
if os.path.exists(os.path.join(react_native_dir, "api")):
print("Deleting existing output directory")
if verbose:
print("Deleting existing output directory")
subprocess.run(["rm", "-rf", os.path.join(react_native_dir, "api")])

print("Running Doxygen")
if verbose:
print("Running Doxygen")

# Run doxygen with the config file
doxygen_bin = os.environ.get("DOXYGEN_BIN", "doxygen")
Expand All @@ -105,12 +109,15 @@ def build_snapshot_for_view(

# Check the result
if result.returncode != 0:
print(f"Doxygen finished with error: {result.stderr}")
if verbose:
print(f"Doxygen finished with error: {result.stderr}")
else:
print("Doxygen finished successfully")
if verbose:
print("Doxygen finished successfully")

# Delete the Doxygen config file
print("Deleting Doxygen config file")
if verbose:
print("Deleting Doxygen config file")
subprocess.run(["rm", DOXYGEN_CONFIG_FILE], cwd=react_native_dir)

# build snapshot, convert to string, and save to file
Expand All @@ -124,10 +131,7 @@ def build_snapshot_for_view(
f.write("// @" + "generated by scripts/cxx-api\n\n")
f.write(snapshot_string)

print(f"Snapshot written to: {os.path.abspath(output_file)}")

if not RUN_ON_REACT_NATIVE:
print(snapshot_string)
return snapshot_string


def main():
Expand All @@ -144,29 +148,46 @@ def main():
type=str,
help="Path to codegen generated code",
)
parser.add_argument(
"--check",
action="store_true",
help="Generate snapshots to a temp directory and compare against committed ones",
)
parser.add_argument(
"--snapshot-dir",
type=str,
help="Directory containing committed snapshots for comparison (used with --check)",
)
parser.add_argument(
"--test",
action="store_true",
help="Run on the local test directory instead of the react-native directory",
)
args = parser.parse_args()

verbose = not args.check

doxygen_bin = os.environ.get("DOXYGEN_BIN", "doxygen")
version_result = subprocess.run(
[doxygen_bin, "--version"],
capture_output=True,
text=True,
)
print(f"Using Doxygen {version_result.stdout.strip()} ({doxygen_bin})")
if verbose:
print(f"Using Doxygen {version_result.stdout.strip()} ({doxygen_bin})")

# Define the path to the react-native directory
react_native_dir = (
react_native_package_dir = (
os.path.join(get_react_native_dir(), "packages", "react-native")
if RUN_ON_REACT_NATIVE
if not args.test
else os.path.join(get_react_native_dir(), "scripts", "cxx-api", "manual_test")
)
print(f"Running in directory: {react_native_dir}")
if verbose:
print(f"Running in directory: {react_native_package_dir}")

if args.codegen_path:
if verbose and args.codegen_path:
print(f"Codegen output path: {os.path.abspath(args.codegen_path)}")

output_dir = args.output_dir if args.output_dir else react_native_dir

# Parse config file
config_path = os.path.join(
get_react_native_dir(), "scripts", "cxx-api", "config.yml"
Expand All @@ -177,25 +198,53 @@ def main():
codegen_path=args.codegen_path,
)

if RUN_ON_REACT_NATIVE:
for config in snapshot_configs:
build_snapshot_for_view(
api_view=config.snapshot_name,
react_native_dir=react_native_dir,
include_directories=config.inputs,
exclude_patterns=config.exclude_patterns,
definitions=config.definitions,
def build_snapshots(output_dir: str, verbose: bool) -> None:
if not args.test:
for config in snapshot_configs:
build_snapshot_for_view(
api_view=config.snapshot_name,
react_native_dir=react_native_package_dir,
include_directories=config.inputs,
exclude_patterns=config.exclude_patterns,
definitions=config.definitions,
output_dir=output_dir,
verbose=verbose,
)
else:
snapshot = build_snapshot_for_view(
api_view="Test",
react_native_dir=react_native_package_dir,
include_directories=[],
exclude_patterns=[],
definitions={},
output_dir=output_dir,
verbose=verbose,
)

if verbose:
print(snapshot)

if args.check:
with tempfile.TemporaryDirectory() as tmpdir:
build_snapshots(tmpdir, verbose=False)

snapshot_dir = args.snapshot_dir or os.path.join(
get_react_native_dir(), "scripts", "cxx-api", "api-snapshots"
)

if not check_snapshots(tmpdir, snapshot_dir):
sys.exit(1)

print("All snapshot checks passed")
else:
build_snapshot_for_view(
api_view="Test",
react_native_dir=react_native_dir,
include_directories=[],
exclude_patterns=[],
definitions={},
output_dir=output_dir,
output_dir = (
args.output_dir
if args.output_dir
else os.path.join(
get_react_native_dir(), "scripts", "cxx-api", "api-snapshots"
)
)
build_snapshots(output_dir, verbose=True)


if __name__ == "__main__":
Expand Down
14 changes: 4 additions & 10 deletions scripts/cxx-api/parser/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,7 @@
TypedefMember,
VariableMember,
)
from .scope import (
CategoryScopeKind,
InterfaceScopeKind,
ProtocolScopeKind,
StructLikeScopeKind,
)
from .scope import InterfaceScopeKind, ProtocolScopeKind, StructLikeScopeKind
from .snapshot import Snapshot
from .template import Template
from .utils import (
Expand All @@ -40,7 +35,6 @@
InitializerType,
parse_qualified_path,
resolve_linked_text_name,
resolve_ref_text_name,
)


Expand Down Expand Up @@ -281,7 +275,7 @@ def get_typedef_member(
typedef_def: compound.memberdefType, visibility: str
) -> TypedefMember:
typedef_name = typedef_def.get_name()
typedef_type = resolve_ref_text_name(typedef_def.get_type())
typedef_type = resolve_linked_text_name(typedef_def.get_type())[0]
typedef_argstring = typedef_def.get_argsstring()
typedef_definition = typedef_def.definition

Expand Down Expand Up @@ -338,7 +332,7 @@ def get_property_member(
Get the property member from a member definition.
"""
property_name = member_def.get_name()
property_type = resolve_ref_text_name(member_def.get_type()).strip()
property_type = resolve_linked_text_name(member_def.get_type())[0].strip()
accessor = member_def.accessor if hasattr(member_def, "accessor") else None
is_readable = getattr(member_def, "readable", "no") == "yes"
is_writable = getattr(member_def, "writable", "no") == "yes"
Expand All @@ -364,7 +358,7 @@ def create_enum_scope(snapshot: Snapshot, enum_def: compound.EnumdefType) -> Non
Create an enum scope in the snapshot.
"""
scope = snapshot.create_enum(enum_def.qualifiedname)
scope.kind.type = resolve_ref_text_name(enum_def.get_type())
scope.kind.type = resolve_linked_text_name(enum_def.get_type())[0]
scope.location = enum_def.location.file

for enum_value_def in enum_def.enumvalue:
Expand Down
73 changes: 73 additions & 0 deletions scripts/cxx-api/parser/snapshot_diff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

"""
Utilities for comparing API snapshots.
"""

import difflib
import os


def check_snapshots(generated_dir: str, committed_dir: str) -> bool:
"""Compare generated snapshots against committed ones.

Returns True if check passes (snapshots match or no committed snapshots).
Returns False if snapshots differ.
"""
if not os.path.isdir(committed_dir):
print(f"No committed snapshots directory found at: {committed_dir}")
print("Skipping comparison (no baseline to compare against)")
return True

committed_files = sorted(f for f in os.listdir(committed_dir) if f.endswith(".api"))
generated_files = sorted(f for f in os.listdir(generated_dir) if f.endswith(".api"))

if not committed_files:
print("No committed snapshot files found")
print("Skipping comparison (no baseline to compare against)")
return True

committed_set = set(committed_files)
generated_set = set(generated_files)
all_passed = True

for filename in sorted(committed_set | generated_set):
committed_path = os.path.join(committed_dir, filename)
generated_path = os.path.join(generated_dir, filename)

if filename not in generated_set:
print(
f"FAIL: {filename} exists in committed snapshots but was not generated"
)
all_passed = False
continue

if filename not in committed_set:
print(f"OK: {filename} generated (no committed baseline)")
continue

with open(committed_path) as f:
committed_content = f.read()
with open(generated_path) as f:
generated_content = f.read()

if committed_content == generated_content:
print(f"OK: {filename} matches committed snapshot")
else:
print(f"FAIL: {filename} differs from committed snapshot")
diff = "\n".join(
difflib.unified_diff(
committed_content.splitlines(),
generated_content.splitlines(),
fromfile=f"committed/{filename}",
tofile=f"generated/{filename}",
lineterm="",
)
)
print(diff)
all_passed = False

return all_passed
2 changes: 0 additions & 2 deletions scripts/cxx-api/parser/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
InitializerType,
normalize_angle_brackets,
resolve_linked_text_name,
resolve_ref_text_name,
)
from .type_qualification import qualify_arguments, qualify_parsed_type, qualify_type_str

Expand All @@ -40,5 +39,4 @@
"qualify_parsed_type",
"qualify_type_str",
"resolve_linked_text_name",
"resolve_ref_text_name",
]
22 changes: 0 additions & 22 deletions scripts/cxx-api/parser/utils/text_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,28 +90,6 @@ def normalize_angle_brackets(text: str) -> str:
return text


def resolve_ref_text_name(type_def: compound.refTextType) -> str:
"""Resolve the text content of a refTextType."""
if hasattr(type_def, "content_") and type_def.content_:
name = ""
for part in type_def.content_:
if part.category == 1: # MixedContainer.CategoryText
name += part.value
elif part.category == 3: # MixedContainer.CategoryComplex (ref element)
if hasattr(part.value, "get_valueOf_"):
name += part.value.get_valueOf_()
elif hasattr(part.value, "valueOf_"):
name += part.value.valueOf_
else:
name += str(part.value)
return normalize_angle_brackets(name)

if type_def.ref:
return normalize_angle_brackets(type_def.ref[0].get_valueOf_())

return normalize_angle_brackets(type_def.get_valueOf_())


class InitializerType(Enum):
NONE = (0,)
ASSIGNMENT = (1,)
Expand Down
Loading