@@ -31,6 +31,21 @@ def test_does_not_collect_builtin_names(self) -> None:
3131 assert "print" not in renameable
3232 assert "len" not in renameable
3333
34+ def test_does_not_collect_builtin_names_used_only_in_calls (self ) -> None :
35+ """Regression #10: ``int(…)``, ``str(…)``, etc. use Load context, not Store."""
36+ renameable = _collect (
37+ textwrap .dedent (
38+ """\
39+ class A:
40+ def __init__(self):
41+ self.fps = int(25)
42+ self.tag = str(99)
43+ """
44+ )
45+ )
46+ assert "int" not in renameable
47+ assert "str" not in renameable
48+
3449 def test_does_not_collect_imported_name (self ) -> None :
3550 renameable = _collect ("import os\n path = os.getcwd()" )
3651 assert "os" not in renameable
@@ -107,6 +122,36 @@ def test_builtin_names_not_renamed(self) -> None:
107122 result = _apply (source )
108123 assert "len" in result
109124
125+ def test_builtin_type_converters_in_calls_not_renamed (self ) -> None :
126+ """Regression #10: call sites like ``int(25)`` must stay as builtins.
127+
128+ Only bound names are renamed; ``int`` / ``str`` in call position are
129+ :class:`ast.Load`, not collected, and must not become random ids.
130+ """
131+ source = textwrap .dedent (
132+ """\
133+ class A:
134+ def __init__(self):
135+ self.fps = int(25)
136+ self.tag = str(99)
137+ """
138+ )
139+ result = _apply (source )
140+ assert "int(25)" in result
141+ assert "str(99)" in result
142+ tree = ast .parse (result )
143+ class_def = tree .body [0 ]
144+ assert isinstance (class_def , ast .ClassDef )
145+ init = class_def .body [0 ]
146+ assert isinstance (init , ast .FunctionDef )
147+ for stmt in init .body :
148+ assert isinstance (stmt , ast .Assign )
149+ call = stmt .value
150+ assert isinstance (call , ast .Call )
151+ func = call .func
152+ assert isinstance (func , ast .Name )
153+ assert func .id in ("int" , "str" )
154+
110155 def test_import_name_not_renamed (self ) -> None :
111156 source = "import os\n path = os.getcwd()"
112157 result = _apply (source )
@@ -117,6 +162,55 @@ def test_dunder_not_renamed(self) -> None:
117162 result = _apply (source )
118163 assert "__name__" in result
119164
165+ def test_string_literal_unchanged_when_it_contains_renamed_identifier (self ) -> None :
166+ """Only :class:`ast.Name` nodes rename; string contents must not be touched.
167+
168+ A naive textual substitution could turn ``'my name is evil'`` into
169+ ``'my <obfuscated> is evil'`` when the variable ``name`` is renamed.
170+ """
171+ source = "name = 'my name is evil'\n "
172+ result = _apply (source )
173+ tree = ast .parse (result )
174+ assign = tree .body [0 ]
175+ assert isinstance (assign , ast .Assign )
176+ assert isinstance (assign .targets [0 ], ast .Name )
177+ assert assign .targets [0 ].id != "name"
178+ val = assign .value
179+ assert isinstance (val , ast .Constant )
180+ assert val .value == "my name is evil"
181+
182+ def test_name_main_guard_with_imports_preserves_name_dunder (self ) -> None :
183+ """Regression: __name__ in `if __name__ == '__main__'` must stay intact.
184+
185+ It is only ever loaded here, is excluded as a dunder/builtin from the
186+ rename map, and must not become a random identifier (would break the
187+ main guard).
188+ """
189+ source = textwrap .dedent (
190+ """\
191+ from file1 import f1
192+ from file2 import f2
193+
194+ if __name__ == "__main__":
195+ f1()
196+ f2()
197+ print("f3")
198+ """
199+ )
200+ result = _apply (source )
201+ assert "__name__" in result
202+ assert "__main__" in result
203+ # Imported symbols are not renamed; calls must still match imports.
204+ assert "f1()" in result
205+ assert "f2()" in result
206+ tree = ast .parse (result )
207+ if_node = tree .body [2 ]
208+ assert isinstance (if_node , ast .If )
209+ test = if_node .test
210+ assert isinstance (test , ast .Compare )
211+ assert isinstance (test .left , ast .Name )
212+ assert test .left .id == "__name__"
213+
120214 def test_function_name_is_renamed (self ) -> None :
121215 source = "def compute(): return 42"
122216 result = _apply (source )
0 commit comments