|
10 | 10 | from __future__ import annotations |
11 | 11 |
|
12 | 12 | import os |
| 13 | +from dataclasses import dataclass |
13 | 14 |
|
14 | 15 | from doxmlparser import compound, index |
15 | 16 |
|
|
24 | 25 | get_typedef_member, |
25 | 26 | get_variable_member, |
26 | 27 | ) |
| 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 |
27 | 37 | 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').""" |
29 | 60 |
|
30 | 61 |
|
31 | 62 | def _should_exclude_symbol(name: str, exclude_symbols: list[str]) -> bool: |
@@ -170,6 +201,199 @@ def _handle_class_compound(snapshot, compound_object): |
170 | 201 | ) |
171 | 202 |
|
172 | 203 |
|
| 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 | + |
173 | 397 | def build_snapshot(xml_dir: str, exclude_symbols: list[str] | None = None) -> Snapshot: |
174 | 398 | """ |
175 | 399 | 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 |
218 | 442 | print(f"Unknown compound kind: {kind}") |
219 | 443 |
|
220 | 444 | snapshot.finish() |
| 445 | + |
| 446 | + snapshot.excluded_symbol_references = find_excluded_symbol_references( |
| 447 | + snapshot, exclude_symbols |
| 448 | + ) |
| 449 | + |
221 | 450 | return snapshot |
0 commit comments