Skip to content

Commit 6f329c1

Browse files
Dawid Małeckimeta-codesync[bot]
authored andcommitted
Detecting references to exluded symbols
Differential Revision: D99417803
1 parent fdd6ca5 commit 6f329c1

File tree

5 files changed

+548
-3
lines changed

5 files changed

+548
-3
lines changed

scripts/cxx-api/parser/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,16 @@
33
# This source code is licensed under the MIT license found in the
44
# LICENSE file in the root directory of this source tree.
55

6-
from .main import build_snapshot
6+
from .main import (
7+
build_snapshot,
8+
ExcludedSymbolReference,
9+
find_excluded_symbol_references,
10+
)
711
from .path_utils import get_repo_root
812

9-
__all__ = ["build_snapshot", "get_repo_root"]
13+
__all__ = [
14+
"build_snapshot",
15+
"ExcludedSymbolReference",
16+
"find_excluded_symbol_references",
17+
"get_repo_root",
18+
]

scripts/cxx-api/parser/__main__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def build_snapshot_for_view(
9696
input_filter: str = None,
9797
work_dir: str | None = None,
9898
exclude_symbols: list[str] | None = None,
99+
warn_excluded_refs: bool = False,
99100
) -> str:
100101
if verbose:
101102
print(f"[{api_view}] Generating API view")
@@ -130,6 +131,23 @@ def build_snapshot_for_view(
130131
snapshot = build_snapshot(
131132
os.path.join(work_dir, "xml"), exclude_symbols=exclude_symbols
132133
)
134+
135+
if warn_excluded_refs and snapshot.excluded_symbol_references:
136+
_YELLOW = "\033[33m"
137+
_RESET = "\033[0m"
138+
refs = snapshot.excluded_symbol_references
139+
print(
140+
f"{_YELLOW}[{api_view}] WARNING: Found {len(refs)} reference(s) "
141+
f"to excluded symbols:{_RESET}",
142+
file=sys.stderr,
143+
)
144+
for ref in refs:
145+
print(
146+
f"{_YELLOW}{ref.scope}: {ref.context} '{ref.symbol}' "
147+
f"matches excluded pattern '{ref.pattern}'{_RESET}",
148+
file=sys.stderr,
149+
)
150+
133151
snapshot_string = snapshot.to_string()
134152

135153
output_file = os.path.join(output_dir, f"{api_view}Cxx.api")
@@ -151,6 +169,7 @@ def build_snapshots(
151169
view_filter: str | None = None,
152170
is_test: bool = False,
153171
keep_xml: bool = False,
172+
warn_excluded_refs: bool = False,
154173
) -> None:
155174
if not is_test:
156175
configs_to_build = [
@@ -195,6 +214,7 @@ def build_snapshots(
195214
input_filter=input_filter if config.input_filter else None,
196215
work_dir=work_dir,
197216
exclude_symbols=config.exclude_symbols,
217+
warn_excluded_refs=warn_excluded_refs,
198218
)
199219
futures[future] = config.snapshot_name
200220

@@ -285,6 +305,11 @@ def main():
285305
action="store_true",
286306
help="Keep the generated Doxygen XML files next to the .api output in a xml/ directory",
287307
)
308+
parser.add_argument(
309+
"--warn-excluded-refs",
310+
action="store_true",
311+
help="Warn when non-excluded symbols reference types matching exclude_symbols patterns",
312+
)
288313
args = parser.parse_args()
289314

290315
verbose = not args.validate
@@ -343,6 +368,7 @@ def main():
343368
view_filter=args.view,
344369
is_test=args.test,
345370
keep_xml=args.xml,
371+
warn_excluded_refs=args.warn_excluded_refs,
346372
)
347373

348374
if args.validate:

scripts/cxx-api/parser/main.py

Lines changed: 230 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from __future__ import annotations
1111

1212
import os
13+
from dataclasses import dataclass
1314

1415
from doxmlparser import compound, index
1516

@@ -24,8 +25,38 @@
2425
get_typedef_member,
2526
get_variable_member,
2627
)
28+
from .member import (
29+
FriendMember,
30+
FunctionMember,
31+
PropertyMember,
32+
TypedefMember,
33+
VariableMember,
34+
)
35+
from .scope import Scope, StructLikeScopeKind
36+
from .scope.extendable import Extendable
2737
from .snapshot import Snapshot
28-
from .utils import has_scope_resolution_outside_angles, parse_qualified_path
38+
from .utils import (
39+
format_parsed_type,
40+
has_scope_resolution_outside_angles,
41+
parse_qualified_path,
42+
)
43+
44+
45+
@dataclass
46+
class ExcludedSymbolReference:
47+
"""A reference to an excluded symbol found in the API snapshot."""
48+
49+
symbol: str
50+
"""The full text containing the reference (e.g., the type string)."""
51+
52+
pattern: str
53+
"""The exclude_symbols pattern that matched."""
54+
55+
scope: str
56+
"""The qualified name of the scope containing the reference."""
57+
58+
context: str
59+
"""Description of where the reference appears (e.g., 'base class', 'return type')."""
2960

3061

3162
def _should_exclude_symbol(name: str, exclude_symbols: list[str]) -> bool:
@@ -170,6 +201,199 @@ def _handle_class_compound(snapshot, compound_object):
170201
)
171202

172203

204+
def _check_text_for_excluded_patterns(
205+
text: str,
206+
scope_name: str,
207+
context: str,
208+
exclude_symbols: list[str],
209+
results: list[ExcludedSymbolReference],
210+
) -> None:
211+
"""Append an ExcludedSymbolReference for each pattern found in *text*."""
212+
for pattern in exclude_symbols:
213+
if pattern in text:
214+
results.append(
215+
ExcludedSymbolReference(
216+
symbol=text,
217+
pattern=pattern,
218+
scope=scope_name,
219+
context=context,
220+
)
221+
)
222+
223+
224+
def _check_arguments_for_excluded_patterns(
225+
arguments: list,
226+
scope_name: str,
227+
context_prefix: str,
228+
exclude_symbols: list[str],
229+
results: list[ExcludedSymbolReference],
230+
) -> None:
231+
"""Check every argument's type string for excluded patterns."""
232+
for arg in arguments:
233+
# Argument is a tuple: (qualifiers, type, name, default_value)
234+
arg_type = arg[1]
235+
if arg_type:
236+
_check_text_for_excluded_patterns(
237+
arg_type,
238+
scope_name,
239+
f"{context_prefix} parameter type",
240+
exclude_symbols,
241+
results,
242+
)
243+
244+
245+
def _check_member_for_excluded_patterns(
246+
member,
247+
scope_name: str,
248+
exclude_symbols: list[str],
249+
results: list[ExcludedSymbolReference],
250+
) -> None:
251+
"""Check a single member for type references matching excluded patterns."""
252+
member_name = f"{scope_name}::{member.name}"
253+
254+
if isinstance(member, FunctionMember):
255+
if member.type:
256+
_check_text_for_excluded_patterns(
257+
member.type,
258+
member_name,
259+
"return type",
260+
exclude_symbols,
261+
results,
262+
)
263+
_check_arguments_for_excluded_patterns(
264+
member.arguments,
265+
member_name,
266+
"function",
267+
exclude_symbols,
268+
results,
269+
)
270+
271+
elif isinstance(member, VariableMember):
272+
type_str = format_parsed_type(member._parsed_type)
273+
if type_str:
274+
_check_text_for_excluded_patterns(
275+
type_str,
276+
member_name,
277+
"variable type",
278+
exclude_symbols,
279+
results,
280+
)
281+
_check_arguments_for_excluded_patterns(
282+
member._fp_arguments,
283+
member_name,
284+
"function pointer",
285+
exclude_symbols,
286+
results,
287+
)
288+
289+
elif isinstance(member, TypedefMember):
290+
value = member.get_value()
291+
if value:
292+
_check_text_for_excluded_patterns(
293+
value,
294+
member_name,
295+
"typedef target type",
296+
exclude_symbols,
297+
results,
298+
)
299+
_check_arguments_for_excluded_patterns(
300+
member._fp_arguments,
301+
member_name,
302+
"function pointer",
303+
exclude_symbols,
304+
results,
305+
)
306+
307+
elif isinstance(member, PropertyMember):
308+
if member.type:
309+
_check_text_for_excluded_patterns(
310+
member.type,
311+
member_name,
312+
"property type",
313+
exclude_symbols,
314+
results,
315+
)
316+
317+
elif isinstance(member, FriendMember):
318+
_check_text_for_excluded_patterns(
319+
member.name,
320+
member_name,
321+
"friend declaration",
322+
exclude_symbols,
323+
results,
324+
)
325+
326+
if member.specialization_args:
327+
for arg in member.specialization_args:
328+
_check_text_for_excluded_patterns(
329+
arg,
330+
member_name,
331+
"member specialization argument",
332+
exclude_symbols,
333+
results,
334+
)
335+
336+
337+
def _walk_scope_for_excluded_patterns(
338+
scope: Scope,
339+
exclude_symbols: list[str],
340+
results: list[ExcludedSymbolReference],
341+
) -> None:
342+
"""Recursively walk a scope tree checking for excluded pattern references."""
343+
scope_name = scope.get_qualified_name() or "(root)"
344+
345+
# Check base classes (StructLikeScopeKind, ProtocolScopeKind, InterfaceScopeKind)
346+
if isinstance(scope.kind, Extendable):
347+
for base in scope.kind.base_classes:
348+
_check_text_for_excluded_patterns(
349+
base.name,
350+
scope_name,
351+
"base class",
352+
exclude_symbols,
353+
results,
354+
)
355+
356+
# Check specialization args
357+
if isinstance(scope.kind, StructLikeScopeKind) and scope.kind.specialization_args:
358+
for arg in scope.kind.specialization_args:
359+
_check_text_for_excluded_patterns(
360+
arg,
361+
scope_name,
362+
"specialization argument",
363+
exclude_symbols,
364+
results,
365+
)
366+
367+
for member in scope.get_members():
368+
_check_member_for_excluded_patterns(
369+
member, scope_name, exclude_symbols, results
370+
)
371+
372+
for inner in scope.inner_scopes.values():
373+
_walk_scope_for_excluded_patterns(inner, exclude_symbols, results)
374+
375+
376+
def find_excluded_symbol_references(
377+
snapshot: Snapshot,
378+
exclude_symbols: list[str],
379+
) -> list[ExcludedSymbolReference]:
380+
"""
381+
Walk the snapshot scope tree after it has been finalized and find
382+
references to excluded symbols in type strings, base classes, and
383+
other type references.
384+
385+
This detects cases where a non-excluded symbol references an excluded
386+
symbol (e.g., a class inherits from an excluded base, a function returns
387+
an excluded type, etc.).
388+
"""
389+
if not exclude_symbols:
390+
return []
391+
392+
results: list[ExcludedSymbolReference] = []
393+
_walk_scope_for_excluded_patterns(snapshot.root_scope, exclude_symbols, results)
394+
return results
395+
396+
173397
def build_snapshot(xml_dir: str, exclude_symbols: list[str] | None = None) -> Snapshot:
174398
"""
175399
Reads the Doxygen XML output and builds a snapshot of the C++ API.
@@ -218,4 +442,9 @@ def build_snapshot(xml_dir: str, exclude_symbols: list[str] | None = None) -> Sn
218442
print(f"Unknown compound kind: {kind}")
219443

220444
snapshot.finish()
445+
446+
snapshot.excluded_symbol_references = find_excluded_symbol_references(
447+
snapshot, exclude_symbols
448+
)
449+
221450
return snapshot

scripts/cxx-api/parser/snapshot.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
class Snapshot:
2222
def __init__(self) -> None:
2323
self.root_scope: Scope = Scope(NamespaceScopeKind())
24+
self.excluded_symbol_references: list = []
2425

2526
def ensure_scope(self, scope_path: list[str]) -> Scope:
2627
"""

0 commit comments

Comments
 (0)