Skip to content

Support name mangling #16715

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d93ee65
suggest attributes of anchestors
tyralla Dec 29, 2023
5695d25
add a note to `is_private`
tyralla Dec 29, 2023
c54fa1f
handle enum.__member
tyralla Dec 29, 2023
d56c4af
implement and apply `NameMangler`
tyralla Dec 29, 2023
35b228e
fix `testSelfTypeReallyTrickyExample`
tyralla Dec 29, 2023
b41f095
fix stubtest `test_name_mangling`
tyralla Dec 29, 2023
6530bf1
add name mangling test cases
tyralla Dec 29, 2023
8016d33
use `/` instead of `__` to mark positional-only method parameters
tyralla Dec 29, 2023
cf554fb
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 29, 2023
b00ea24
improve auto fix
tyralla Dec 30, 2023
a6eb9e4
first hacky step/suggestion for introducing a flag to avoid method pa…
tyralla Dec 30, 2023
bacd994
mangle strings in __slots__
tyralla Jan 2, 2024
1c3cba7
different annotation handling when using `from __future__ import anno…
tyralla Jan 2, 2024
89bd158
improve visit_ClassDef
tyralla Jan 2, 2024
8f5f187
improve visit_FunctionDef
tyralla Jan 2, 2024
2bd7626
refactor visit_FunctionDef and visit_AsyncFunctionDef
tyralla Jan 3, 2024
77513a6
different return type annotation handling when using `from __future__…
tyralla Jan 3, 2024
74b46c2
fix
tyralla Jan 3, 2024
9a96ff9
support decorators
tyralla Jan 4, 2024
29f186c
fix
tyralla Jan 4, 2024
292890a
Add TypeVar tests
tyralla Jan 4, 2024
e6904d8
for testing: do not mangle annotations when defined in stub files
tyralla Jan 4, 2024
cfaddb3
refactor
tyralla Jan 4, 2024
a1eb0dd
remove method `is_private` by mangling "__mypy-replace" and "__mypy-p…
tyralla Jan 5, 2024
b744349
fix
tyralla Jan 5, 2024
9892dc2
add missing [builtins fixtures/tuple.pyi]
tyralla Jan 30, 2025
aec6a87
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 30, 2025
d8c7c3e
after rebasing: adjust `infer_variance` to mangled `__mypy-replace` m…
tyralla Jan 31, 2025
a04ca0a
fix format
tyralla Jan 31, 2025
fd68e7b
add function `maybe_mangled` mainly for fixing the handling of "priva…
tyralla Feb 1, 2025
b2a63ad
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 1, 2025
8e8c129
add the module path to the mangled name of an internal dataclass symbol
tyralla Feb 2, 2025
4a01786
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 2, 2025
384b0ee
add the module path to the mangled name of an internal dataclass symbol
tyralla Feb 2, 2025
44a9e32
add the module path to the mangled name of an internal dataclass symbol
tyralla Feb 2, 2025
2fa302b
Merge branch 'master' into feature/name_mangling
tyralla Feb 16, 2025
0acbd5a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 16, 2025
89d11a6
remove `_MANGLE_ARGS` -> never mangle parameter names
tyralla Feb 20, 2025
eb92319
move the new test cases from `check-classes.test` to `check-mangling.…
tyralla Feb 21, 2025
ec4409b
do not mangle TypedDict keywords
tyralla Feb 23, 2025
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
41 changes: 12 additions & 29 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@
from mypy.types_utils import is_overlapping_none, remove_optional, store_argument_type, strip_type
from mypy.typetraverser import TypeTraverserVisitor
from mypy.typevars import fill_typevars, fill_typevars_with_any, has_no_typevars
from mypy.util import is_dunder, is_sunder
from mypy.util import is_dunder, is_sunder, maybe_mangled
from mypy.visitor import NodeVisitor

T = TypeVar("T")
Expand Down Expand Up @@ -1350,7 +1350,7 @@ def check_func_def(
if (
arg_type.variance == COVARIANT
and defn.name not in ("__init__", "__new__", "__post_init__")
and not is_private(defn.name) # private methods are not inherited
and "mypy-" not in defn.name # skip internally added methods
):
ctx: Context = arg_type
if ctx.line < 0:
Expand Down Expand Up @@ -1993,7 +1993,6 @@ def check_explicit_override_decorator(
and found_method_base_classes
and not defn.is_explicit_override
and defn.name not in ("__init__", "__new__")
and not is_private(defn.name)
):
self.msg.explicit_override_decorator_missing(
defn.name, found_method_base_classes[0].fullname, context or defn
Expand Down Expand Up @@ -2050,7 +2049,7 @@ def check_method_or_accessor_override_for_base(
base_attr = base.names.get(name)
if base_attr:
# First, check if we override a final (always an error, even with Any types).
if is_final_node(base_attr.node) and not is_private(name):
if is_final_node(base_attr.node):
self.msg.cant_override_final(name, base.name, defn)
# Second, final can't override anything writeable independently of types.
if defn.is_final:
Expand Down Expand Up @@ -2415,9 +2414,6 @@ def check_override(
if original.type_is is not None and override.type_is is None:
fail = True

if is_private(name):
fail = False

if fail:
emitted_msg = False

Expand Down Expand Up @@ -2720,25 +2716,26 @@ def check_enum(self, defn: ClassDef) -> None:

def check_final_enum(self, defn: ClassDef, base: TypeInfo) -> None:
for sym in base.names.values():
if self.is_final_enum_value(sym):
if self.is_final_enum_value(sym, base):
self.fail(f'Cannot extend enum with existing members: "{base.name}"', defn)
break

def is_final_enum_value(self, sym: SymbolTableNode) -> bool:
def is_final_enum_value(self, sym: SymbolTableNode, base: TypeInfo) -> bool:
if isinstance(sym.node, (FuncBase, Decorator)):
return False # A method is fine
if not isinstance(sym.node, Var):
return True # Can be a class or anything else

# Now, only `Var` is left, we need to check:
# 1. Private name like in `__prop = 1`
# 1. Mangled name like in `_class__prop = 1`
# 2. Dunder name like `__hash__ = some_hasher`
# 3. Sunder name like `_order_ = 'a, b, c'`
# 4. If it is a method / descriptor like in `method = classmethod(func)`
name = sym.node.name
if (
is_private(sym.node.name)
or is_dunder(sym.node.name)
or is_sunder(sym.node.name)
maybe_mangled(name, base.name)
or is_dunder(name)
or is_sunder(name)
# TODO: make sure that `x = @class/staticmethod(func)`
# and `x = property(prop)` both work correctly.
# Now they are incorrectly counted as enum members.
Expand Down Expand Up @@ -2849,12 +2846,8 @@ def check_multiple_inheritance(self, typ: TypeInfo) -> None:
# Verify that inherited attributes are compatible.
mro = typ.mro[1:]
all_names = {name for base in mro for name in base.names}
# Sort for reproducible message order.
for name in sorted(all_names - typ.names.keys()):
# Sort for reproducible message order.
# Attributes defined in both the type and base are skipped.
# Normal checks for attribute compatibility should catch any problems elsewhere.
if is_private(name):
continue
# Compare the first base defining a name with the rest.
# Remaining bases may not be pairwise compatible as the first base provides
# the used definition.
Expand Down Expand Up @@ -2966,7 +2959,7 @@ class C(B, A[int]): ... # this is unsafe because...
ok = True
# Final attributes can never be overridden, but can override
# non-final read-only attributes.
if is_final_node(second.node) and not is_private(name):
if is_final_node(second.node):
self.msg.cant_override_final(name, base2.name, ctx)
if is_final_node(first.node):
self.check_if_final_var_override_writable(name, second.node, ctx)
Expand Down Expand Up @@ -3436,9 +3429,6 @@ def check_compatibility_all_supers(
):
continue

if is_private(lvalue_node.name):
continue

base_type, base_node = self.lvalue_type_from_base(lvalue_node, base)
custom_setter = is_custom_settable_property(base_node)
if isinstance(base_type, PartialType):
Expand Down Expand Up @@ -3642,8 +3632,6 @@ def check_compatibility_final_super(
"""
if not isinstance(base_node, (Var, FuncBase, Decorator)):
return True
if is_private(node.name):
return True
if base_node.is_final and (node.is_final or not isinstance(base_node, Var)):
# Give this error only for explicit override attempt with `Final`, or
# if we are overriding a final method with variable.
Expand Down Expand Up @@ -8942,11 +8930,6 @@ def is_overlapping_types_for_overload(left: Type, right: Type) -> bool:
)


def is_private(node_name: str) -> bool:
"""Check if node is private to class definition."""
return node_name.startswith("__") and not node_name.endswith("__")


def is_string_literal(typ: Type) -> bool:
strs = try_getting_str_literals_from_type(typ)
return strs is not None and len(strs) == 1
Expand Down
3 changes: 2 additions & 1 deletion mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
get_proper_type,
)
from mypy.typetraverser import TypeTraverserVisitor
from mypy.util import is_dunder, maybe_mangled

if TYPE_CHECKING: # import for forward declaration only
import mypy.checker
Expand Down Expand Up @@ -1177,7 +1178,7 @@ def analyze_enum_class_attribute_access(
if name in EXCLUDED_ENUM_ATTRIBUTES:
return report_missing_attribute(mx.original_type, itype, name, mx)
# Dunders and private names are not Enum members
if name.startswith("__") and name.replace("_", "") != "":
if is_dunder(name) or maybe_mangled(name, itype.type.name):
return None

node = itype.type.get(name)
Expand Down
109 changes: 108 additions & 1 deletion mypy/fastparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import re
import sys
import warnings
from collections.abc import Sequence
from collections.abc import Iterable, Sequence
from typing import Any, Callable, Final, Literal, Optional, TypeVar, Union, cast, overload

from mypy import defaults, errorcodes as codes, message_registry
Expand Down Expand Up @@ -353,6 +353,103 @@ def find_disallowed_expression_in_annotation_scope(expr: ast3.expr | None) -> as
return None


_T_FuncDef = TypeVar("_T_FuncDef", ast3.FunctionDef, ast3.AsyncFunctionDef)


class NameMangler(ast3.NodeTransformer):
"""Mangle (nearly) all private identifiers within a class body (including nested classes)."""

mangled2unmangled: dict[str, str]
_classname_complete: str
_classname_trimmed: str
_mangle_annotations: bool
_unmangled_args: set[str]

def __init__(self, classname: str, mangle_annotations: bool) -> None:
self.mangled2unmangled = {}
self._classname_complete = classname
self._classname_trimmed = classname.lstrip("_")
self._mangle_annotations = mangle_annotations
self._unmangled_args = set()

def _mangle_name(self, name: str) -> str:
if name.startswith("__") and not name.endswith("__"):
mangled = f"_{self._classname_trimmed}{name}"
self.mangled2unmangled[mangled] = name
return mangled
return name

def _mangle_slots(self, node: ast3.ClassDef) -> None:
for assign in node.body:
if isinstance(assign, ast3.Assign):
for target in assign.targets:
if isinstance(target, ast3.Name) and (target.id == "__slots__"):
constants: Iterable[ast3.expr] = ()
if isinstance(values := assign.value, ast3.Constant):
constants = (values,)
elif isinstance(values, (ast3.Tuple, ast3.List)):
constants = values.elts
elif isinstance(values, ast3.Dict):
constants = (key for key in values.keys if key is not None)
for value in constants:
if isinstance(value, ast3.Constant) and isinstance(value.value, str):
value.value = self._mangle_name(value.value)

def visit_ClassDef(self, node: ast3.ClassDef) -> ast3.ClassDef:
if self._classname_complete == node.name:
for stmt in node.body:
self.visit(stmt)
self._mangle_slots(node)
else:
for dec in node.decorator_list:
self.visit(dec)
NameMangler(node.name, self._mangle_annotations).visit(node)
node.name = self._mangle_name(node.name)
return node

def _visit_funcdef(self, node: _T_FuncDef) -> _T_FuncDef:
node.name = self._mangle_name(node.name)
self = NameMangler(self._classname_complete, self._mangle_annotations)
self.visit(node.args)
for dec in node.decorator_list:
self.visit(dec)
if self._mangle_annotations and (node.returns is not None):
self.visit(node.returns)
for stmt in node.body:
self.visit(stmt)
return node

def visit_FunctionDef(self, node: ast3.FunctionDef) -> ast3.FunctionDef:
return self._visit_funcdef(node)

def visit_AsyncFunctionDef(self, node: ast3.AsyncFunctionDef) -> ast3.AsyncFunctionDef:
return self._visit_funcdef(node)

def visit_arg(self, node: ast3.arg) -> ast3.arg:
self._unmangled_args.add(node.arg)
if self._mangle_annotations and (node.annotation is not None):
self.visit(node.annotation)
return node

def visit_AnnAssign(self, node: ast3.AnnAssign) -> ast3.AnnAssign:
self.visit(node.target)
if node.value is not None:
self.visit(node.value)
if self._mangle_annotations:
self.visit(node.annotation)
return node

def visit_Attribute(self, node: ast3.Attribute) -> ast3.Attribute:
node.attr = self._mangle_name(node.attr)
self.generic_visit(node)
return node

def visit_Name(self, node: Name) -> Name:
if node.id not in self._unmangled_args:
node.id = self._mangle_name(node.id)
return node


class ASTConverter:
def __init__(
self,
Expand Down Expand Up @@ -1135,6 +1232,15 @@ def visit_ClassDef(self, n: ast3.ClassDef) -> ClassDef:
if sys.version_info >= (3, 12) and n.type_params:
explicit_type_params = self.translate_type_params(n.type_params)

mangle_annotations = not self.is_stub and not any(
isinstance(i, ImportFrom)
and (i.id == "__future__")
and any(j[0] == "annotations" for j in i.names)
for i in self.imports
)
mangler = NameMangler(n.name, mangle_annotations)
mangler.visit(n)

cdef = ClassDef(
n.name,
self.as_required_block(n.body),
Expand All @@ -1143,6 +1249,7 @@ def visit_ClassDef(self, n: ast3.ClassDef) -> ClassDef:
metaclass=dict(keywords).get("metaclass"),
keywords=keywords,
type_args=explicit_type_params,
mangled2unmangled=mangler.mangled2unmangled,
)
cdef.decorators = self.translate_expr_list(n.decorator_list)
self.set_line(cdef, n)
Expand Down
4 changes: 3 additions & 1 deletion mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,9 @@ def has_no_attr(
)
failed = True
else:
alternatives = set(original_type.type.names.keys())
alternatives: set[str] = set()
for type_ in original_type.type.mro:
alternatives.update(type_.names.keys())
if module_symbol_table is not None:
alternatives |= {
k for k, v in module_symbol_table.items() if v.module_public
Expand Down
8 changes: 6 additions & 2 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import mypy.strconv
from mypy.options import Options
from mypy.util import is_typeshed_file, short_type
from mypy.util import is_dunder, is_typeshed_file, maybe_mangled, short_type
from mypy.visitor import ExpressionVisitor, NodeVisitor, StatementVisitor

if TYPE_CHECKING:
Expand Down Expand Up @@ -1136,6 +1136,7 @@ class ClassDef(Statement):
"has_incompatible_baseclass",
"docstring",
"removed_statements",
"mangled2unmangled",
)

__match_args__ = ("name", "defs")
Expand All @@ -1159,6 +1160,7 @@ class ClassDef(Statement):
has_incompatible_baseclass: bool
# Used by special forms like NamedTuple and TypedDict to store invalid statements
removed_statements: list[Statement]
mangled2unmangled: Final[dict[str, str]]

def __init__(
self,
Expand All @@ -1169,6 +1171,7 @@ def __init__(
metaclass: Expression | None = None,
keywords: list[tuple[str, Expression]] | None = None,
type_args: list[TypeParam] | None = None,
mangled2unmangled: dict[str, str] | None = None,
) -> None:
super().__init__()
self.name = name
Expand All @@ -1186,6 +1189,7 @@ def __init__(
self.has_incompatible_baseclass = False
self.docstring: str | None = None
self.removed_statements = []
self.mangled2unmangled = mangled2unmangled or {}

@property
def fullname(self) -> str:
Expand Down Expand Up @@ -3250,7 +3254,7 @@ def enum_members(self) -> list[str]:
(
isinstance(sym.node, Var)
and name not in EXCLUDED_ENUM_ATTRIBUTES
and not name.startswith("__")
and not (is_dunder(name) or maybe_mangled(name, self.name))
and sym.node.has_explicit_value
and not (
isinstance(
Expand Down
Loading