Files
Dawid Małecki 0b1ce70b62 Prevent identifiers being qualified to themselves (#56091)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/56091

Changelog: [Internal]

When qualifying a member, prevent taking itself as a target for qualification, to prevent types referencing themselves.

Reviewed By: cipolleschi

Differential Revision: D96455842

fbshipit-source-id: 2ed59a4688d0daa98db603a734283f41ffbe4ead
2026-03-18 09:29:02 -07:00

271 lines
10 KiB
Python

# 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.
from __future__ import annotations
from typing import Generic
from natsort import natsorted
from ..member import FriendMember, Member, TypedefMember
from ..utils import parse_qualified_path, qualify_type_str
from .base_scope_kind import _natsort_key, ScopeKindT
from .enum_scope_kind import EnumScopeKind
from .namespace_scope_kind import NamespaceScopeKind
from .struct_like_scope_kind import StructLikeScopeKind
class Scope(Generic[ScopeKindT]):
def __init__(self, kind: ScopeKindT, name: str | None = None) -> None:
self.name: str | None = name
self.kind: ScopeKindT = kind
self.parent_scope: Scope | None = None
self.inner_scopes: dict[str, Scope] = {}
self.location: str | None = None
self._members: list[Member] = []
self._private_typedefs: dict[str, TypedefMember] = {}
self._qualifying_member: Member | None = None
def get_qualified_name(self) -> str:
"""
Get the qualified name of the scope, with template arguments qualified.
"""
path = []
current_scope = self
while current_scope is not None:
if current_scope.name is not None:
name = current_scope.name
if (
isinstance(current_scope.kind, StructLikeScopeKind)
and current_scope.kind.specialization_args is not None
):
name = (
f"{name}<{', '.join(current_scope.kind.specialization_args)}>"
)
path.append(name)
current_scope = current_scope.parent_scope
path.reverse()
return "::".join(path)
def _get_base_name(self, name: str) -> str:
"""Strip template arguments from a name for scope lookup."""
angle_idx = name.find("<")
return name[:angle_idx] if angle_idx != -1 else name
def qualify_name(self, name: str | None) -> str | None:
"""
Qualify a name with the relevant scope if possible.
Handles template arguments by stripping them for lookup but preserving
them in the output.
"""
if not name:
return None
path = parse_qualified_path(name)
if not path:
return None
current_scope = self
# Walk up to find a scope that contains the first path segment
# Check both inner_scopes AND members (for type aliases, etc.)
base_first = self._get_base_name(path[0])
while current_scope is not None:
# Check if it's an inner scope
if base_first in current_scope.inner_scopes:
break
# Skip self-qualification if name matches current scope's name
if (
current_scope.name
and self._get_base_name(current_scope.name) == base_first
):
current_scope = current_scope.parent_scope
continue
# Check if it's a member (type alias, variable, etc.)
for m in current_scope._members:
if m is current_scope._qualifying_member:
continue
if m.name == base_first and not isinstance(m, FriendMember):
prefix = current_scope.get_qualified_name()
return f"{prefix}::{name}" if prefix else name
# Check private typedefs: substitute with the expanded definition
if len(path) == 1 and base_first in current_scope._private_typedefs:
return current_scope._private_typedefs[base_first].get_value()
current_scope = current_scope.parent_scope
if current_scope is None:
return None
# Remember the scope where we found the first segment — its qualified
# name is the prefix that must precede the matched path segments.
anchor_scope = current_scope
# Walk down through the path, tracking matched segments with original template args
matched_segments: list[str] = []
for i, path_segment in enumerate(path):
base_name = self._get_base_name(path_segment)
if base_name in current_scope.inner_scopes:
matched_segments.append(path_segment)
current_scope = current_scope.inner_scopes[base_name]
elif any(
m.name == base_name
and not isinstance(m, FriendMember)
and m is not current_scope._qualifying_member
for m in current_scope._members
) or any(
any(m.name == base_name for m in inner._members)
for inner in current_scope.inner_scopes.values()
if isinstance(inner.kind, EnumScopeKind)
):
# Found as a member (or as an unscoped enum value accessible
# from the parent scope), assume following segments exist
prefix = "::".join(matched_segments)
suffix = "::".join(path[i:])
anchor_prefix = anchor_scope.get_qualified_name()
if prefix:
if anchor_prefix:
return f"{anchor_prefix}::{prefix}::{suffix}"
return f"{prefix}::{suffix}"
else:
if anchor_prefix:
return f"{anchor_prefix}::{suffix}"
return suffix
else:
# Segment not found as an inner scope or a real member of
# the current scope. When inside a struct-like scope this
# typically means Doxygen's refid-based qualification
# incorrectly placed a type under a compound that does not
# actually contain it — for example a friend declaration or
# an inherited constructor reported as a member ref. Try
# to re-qualify from the remaining unmatched segments so the
# type resolves against the broader scope hierarchy.
if isinstance(current_scope.kind, StructLikeScopeKind):
remaining = "::".join(path[i:])
return self.qualify_name(remaining)
return None
# Return qualified name with preserved template arguments
prefix = anchor_scope.get_qualified_name()
if prefix:
return f"{prefix}::{'::'.join(matched_segments)}"
else:
return "::".join(matched_segments)
def add_private_typedef(self, member: TypedefMember) -> None:
"""
Store a private typedef for use during type resolution.
Private typedefs are not included in the snapshot output, but their
definitions are substituted for references to them in public members.
"""
self._private_typedefs[member.name] = member
def add_member(self, member: Member | None) -> None:
"""
Add a member to the scope.
"""
if member is None:
return
self._members.append(member)
def get_members(self) -> list[Member]:
"""
Get all members of the scope.
"""
return self._members
def close(self) -> None:
"""
Close the scope by setting the kind of all temporary scopes.
"""
# Qualify specialization args early so that members and inner scopes
# see the fully-qualified name when they call get_qualified_name().
if (
isinstance(self.kind, StructLikeScopeKind)
and self.kind.specialization_args is not None
and self.parent_scope is not None
):
self.kind.specialization_args = [
qualify_type_str(arg, self.parent_scope)
for arg in self.kind.specialization_args
]
for typedef in self._private_typedefs.values():
typedef.close(self)
for member in self.get_members():
self._qualifying_member = member
member.close(self)
self._qualifying_member = None
self.kind.close(self)
for _, inner_scope in self.inner_scopes.items():
inner_scope.close()
def to_string(self) -> str:
"""
Get the string representation of the scope.
"""
# Get this scope's content (e.g., class members, free functions, ...)
this_content = self.kind.to_string(self)
# Separate inner scopes into namespaces and non-namespaces
# Keep (scope, string) tuples to sort by scope properties
namespace_scope_items: list[tuple[Scope, str]] = []
non_namespace_scope_items: list[tuple[Scope, str]] = []
for _, inner_scope in self.inner_scopes.items():
if inner_scope.name is None:
continue
inner_str = inner_scope.to_string()
if not inner_str.strip():
continue
if isinstance(inner_scope.kind, NamespaceScopeKind):
namespace_scope_items.append((inner_scope, inner_str))
else:
non_namespace_scope_items.append((inner_scope, inner_str))
# Sort non-namespace scopes by depth (fewer :: first) then by string
def scope_sort_key(item: tuple[Scope, str]) -> tuple:
scope, string = item
depth = scope.get_qualified_name().count("::")
return (depth, _natsort_key(string))
non_namespace_scope_items.sort(key=scope_sort_key)
non_namespace_scope_strings = [s for _, s in non_namespace_scope_items]
namespace_scope_strings = [s for _, s in namespace_scope_items]
# Build result:
# 1. Free members / this scope's content first
# 2. Non-namespace inner scopes (classes, structs, enums), sorted by depth
# 3. Namespace inner scopes, each separated by "\n\n\n" (two blank lines)
local_parts = []
if this_content.strip():
local_parts.append(this_content)
local_parts.extend(non_namespace_scope_strings)
# NOTE: Don't sort local_parts together - free members should come first
local_block = "\n\n".join(local_parts)
# Combine with namespace scopes using one more blank line for clearer separation
all_blocks = []
if local_block.strip():
all_blocks.append(local_block)
all_blocks.extend(natsorted(namespace_scope_strings))
return "\n\n\n".join(all_blocks).strip()
def print(self):
"""
Print a scope and its contents.
"""
print(self.to_string())