fluent-codegen 0.2__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluent-codegen
3
- Version: 0.2
3
+ Version: 0.3.0
4
4
  Summary: A Python library for generating Python code via AST construction.
5
5
  Author-email: Luke Plant <luke@lukeplant.me.uk>
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Repository, https://github.com/spookylukey/fluent-codegen
8
8
  Keywords: codegen,code-generation,ast,python,metaprogramming
9
- Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Operating System :: OS Independent
12
12
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -14,7 +14,7 @@ keywords = [
14
14
  "metaprogramming",
15
15
  ]
16
16
  classifiers = [
17
- "Development Status :: 3 - Alpha",
17
+ "Development Status :: 4 - Beta",
18
18
  "Intended Audience :: Developers",
19
19
  "Operating System :: OS Independent",
20
20
  "Programming Language :: Python :: 3 :: Only",
@@ -24,7 +24,7 @@ classifiers = [
24
24
  "Topic :: Software Development :: Code Generators",
25
25
  "Topic :: Software Development :: Libraries :: Python Modules",
26
26
  ]
27
- version = "0.2"
27
+ version = "0.3.0"
28
28
  dependencies = []
29
29
 
30
30
  [project.urls]
@@ -74,7 +74,7 @@ dev = [
74
74
  "pyright==1.1.406",
75
75
  "pytest>=7.4.4",
76
76
  "pytest-cov>=7.0.0",
77
- "ruff>=0.4.0",
77
+ "ruff>=0.15.1",
78
78
  "tox>=4.36.0",
79
79
  "tox-uv>=1.29.0",
80
80
  ]
@@ -10,7 +10,7 @@ import keyword
10
10
  import re
11
11
  import textwrap
12
12
  from abc import ABC, abstractmethod
13
- from collections.abc import Callable, Sequence
13
+ from collections.abc import Callable, Mapping, Sequence
14
14
  from dataclasses import dataclass
15
15
  from typing import ClassVar, Protocol, assert_never, overload, runtime_checkable
16
16
 
@@ -102,8 +102,6 @@ class CodeGenAst(ABC):
102
102
  @abstractmethod
103
103
  def as_ast(self, *, include_comments: bool = False) -> py_ast.AST: ...
104
104
 
105
- child_elements: ClassVar[list[str]]
106
-
107
105
  def as_python_source(self) -> str:
108
106
  """Return the Python source code for this AST node."""
109
107
  node = self.as_ast(include_comments=True)
@@ -120,8 +118,6 @@ class CodeGenAstList(ABC):
120
118
  @abstractmethod
121
119
  def as_ast_list(self, allow_empty: bool = True, *, include_comments: bool = False) -> list[py_ast.stmt]: ...
122
120
 
123
- child_elements: ClassVar[list[str]]
124
-
125
121
  def as_python_source(self) -> str:
126
122
  """Return the Python source code for this AST list."""
127
123
  mod = py_ast.Module(body=self.as_ast_list(include_comments=True), type_ignores=[], **DEFAULT_AST_ARGS_MODULE)
@@ -258,11 +254,9 @@ class SupportsNameAssignment(Protocol):
258
254
  def has_assignment_for_name(self, name: str) -> bool: ...
259
255
 
260
256
 
261
- class _Annotation(Statement):
257
+ class Annotation(Statement):
262
258
  """A bare type annotation without a value, e.g. ``x: int``."""
263
259
 
264
- child_elements = []
265
-
266
260
  def __init__(self, name: str, annotation: Expression):
267
261
  self.name = name
268
262
  self.annotation = annotation
@@ -282,9 +276,7 @@ class _Annotation(Statement):
282
276
  return self.name == name
283
277
 
284
278
 
285
- class _Assignment(Statement):
286
- child_elements = ["value"]
287
-
279
+ class Assignment(Statement):
288
280
  def __init__(self, name: str, value: Expression, /, *, type_hint: Expression | None = None):
289
281
  self.name = name
290
282
  self.value = value
@@ -313,8 +305,6 @@ class _Assignment(Statement):
313
305
 
314
306
 
315
307
  class Block(CodeGenAstList):
316
- child_elements = ["statements"]
317
-
318
308
  def __init__(self, scope: Scope, parent_block: Block | None = None):
319
309
  self.scope = scope
320
310
  # We allow `Expression` here for things like MethodCall which
@@ -459,7 +449,7 @@ class Block(CodeGenAstList):
459
449
  else:
460
450
  self.scope.register_assignment(name)
461
451
 
462
- self.add_statement(_Assignment(name, value, type_hint=type_hint))
452
+ self.add_statement(Assignment(name, value, type_hint=type_hint))
463
453
 
464
454
  def create_annotation(self, name: str, annotation: Expression) -> Name:
465
455
  """
@@ -471,7 +461,7 @@ class Block(CodeGenAstList):
471
461
  """
472
462
  name_obj = self.scope.create_name(name)
473
463
  self.scope.register_assignment(name_obj.name)
474
- self.add_statement(_Annotation(name_obj.name, annotation))
464
+ self.add_statement(Annotation(name_obj.name, annotation))
475
465
  return name_obj
476
466
 
477
467
  def create_field(self, name: str, annotation: Expression, *, default: Expression | None = None) -> Name:
@@ -489,7 +479,7 @@ class Block(CodeGenAstList):
489
479
  if default is not None:
490
480
  name_obj = self.scope.create_name(name)
491
481
  self.scope.register_assignment(name_obj.name)
492
- self.add_statement(_Assignment(name_obj.name, default, type_hint=annotation))
482
+ self.add_statement(Assignment(name_obj.name, default, type_hint=annotation))
493
483
  return name_obj
494
484
  else:
495
485
  return self.create_annotation(name, annotation)
@@ -669,8 +659,6 @@ def _validate_arg_order(args: list[FunctionArg]) -> None:
669
659
 
670
660
 
671
661
  class Function(Scope, Statement):
672
- child_elements = ["body"]
673
-
674
662
  def __init__(
675
663
  self,
676
664
  name: str,
@@ -688,6 +676,11 @@ class Function(Scope, Statement):
688
676
  if args is not None:
689
677
  self.add_args(args)
690
678
 
679
+ @property
680
+ def args(self) -> Sequence[FunctionArg]:
681
+ """Return the function's arguments as a read-only sequence."""
682
+ return tuple(self._args)
683
+
691
684
  def add_args(self, args: Sequence[str | FunctionArg]) -> None:
692
685
  """Add arguments to the function, with the same validation as in __init__."""
693
686
  normalized = _normalize_args(args)
@@ -750,8 +743,6 @@ class Function(Scope, Statement):
750
743
 
751
744
 
752
745
  class Class(Scope, Statement):
753
- child_elements = ["body"]
754
-
755
746
  def __init__(
756
747
  self,
757
748
  name: str,
@@ -780,8 +771,6 @@ class Class(Scope, Statement):
780
771
 
781
772
 
782
773
  class Return(Statement):
783
- child_elements = ["value"]
784
-
785
774
  def __init__(self, value: Expression):
786
775
  self.value = value
787
776
 
@@ -795,8 +784,6 @@ class Return(Statement):
795
784
  class Assert(Statement):
796
785
  """An ``assert`` statement with an optional message."""
797
786
 
798
- child_elements = ["test", "msg"]
799
-
800
787
  def __init__(self, test: Expression, msg: Expression | None = None):
801
788
  self.test = test
802
789
  self.msg = msg
@@ -814,8 +801,6 @@ class Assert(Statement):
814
801
 
815
802
 
816
803
  class If(Statement):
817
- child_elements = ["if_blocks", "conditions", "else_block"]
818
-
819
804
  def __init__(self, parent_scope: Scope, parent_block: Block | None = None):
820
805
  # We model a "compound if statement" as a list of if blocks
821
806
  # (if/elif/elif etc), each with their own condition, with a final else
@@ -867,8 +852,6 @@ class If(Statement):
867
852
 
868
853
 
869
854
  class With(Statement):
870
- child_elements = ["context_expr", "body"]
871
-
872
855
  def __init__(
873
856
  self,
874
857
  context_expr: Expression,
@@ -901,8 +884,6 @@ class With(Statement):
901
884
 
902
885
 
903
886
  class Try(Statement):
904
- child_elements = ["catch_exceptions", "try_block", "except_block", "else_block"]
905
-
906
887
  def __init__(self, catch_exceptions: Sequence[Expression], parent_scope: Scope):
907
888
  self.catch_exceptions = catch_exceptions
908
889
  self.try_block = Block(parent_scope)
@@ -1014,7 +995,7 @@ class Expression(CodeGenAst):
1014
995
  def call(
1015
996
  self,
1016
997
  args: Sequence[Expression] | None = None,
1017
- kwargs: dict[str, Expression] | None = None,
998
+ kwargs: Mapping[str, Expression] | None = None,
1018
999
  ) -> Call:
1019
1000
  return Call(self, args or [], kwargs or {})
1020
1001
 
@@ -1022,7 +1003,7 @@ class Expression(CodeGenAst):
1022
1003
  self,
1023
1004
  attribute: str,
1024
1005
  args: Sequence[Expression] | None = None,
1025
- kwargs: dict[str, Expression] | None = None,
1006
+ kwargs: Mapping[str, Expression] | None = None,
1026
1007
  ) -> Call:
1027
1008
  return self.attr(attribute).call(args, kwargs)
1028
1009
 
@@ -1098,8 +1079,6 @@ class Expression(CodeGenAst):
1098
1079
 
1099
1080
 
1100
1081
  class String(Expression):
1101
- child_elements = []
1102
-
1103
1082
  def __init__(self, string_value: str):
1104
1083
  self.string_value = string_value
1105
1084
 
@@ -1118,8 +1097,6 @@ class String(Expression):
1118
1097
 
1119
1098
 
1120
1099
  class Bool(Expression):
1121
- child_elements = []
1122
-
1123
1100
  def __init__(self, value: bool):
1124
1101
  self.value = value
1125
1102
 
@@ -1131,8 +1108,6 @@ class Bool(Expression):
1131
1108
 
1132
1109
 
1133
1110
  class Bytes(Expression):
1134
- child_elements = []
1135
-
1136
1111
  def __init__(self, value: bytes):
1137
1112
  self.value = value
1138
1113
 
@@ -1144,8 +1119,6 @@ class Bytes(Expression):
1144
1119
 
1145
1120
 
1146
1121
  class Number(Expression):
1147
- child_elements = []
1148
-
1149
1122
  def __init__(self, number: int | float):
1150
1123
  self.number = number
1151
1124
 
@@ -1157,9 +1130,7 @@ class Number(Expression):
1157
1130
 
1158
1131
 
1159
1132
  class List(Expression):
1160
- child_elements = ["items"]
1161
-
1162
- def __init__(self, items: list[Expression]):
1133
+ def __init__(self, items: Sequence[Expression]):
1163
1134
  self.items = items
1164
1135
 
1165
1136
  def as_ast(self, *, include_comments: bool = False) -> py_ast.expr:
@@ -1167,8 +1138,6 @@ class List(Expression):
1167
1138
 
1168
1139
 
1169
1140
  class Tuple(Expression):
1170
- child_elements = ["items"]
1171
-
1172
1141
  def __init__(self, items: Sequence[Expression]):
1173
1142
  self.items = items
1174
1143
 
@@ -1177,8 +1146,6 @@ class Tuple(Expression):
1177
1146
 
1178
1147
 
1179
1148
  class Set(Expression):
1180
- child_elements = ["items"]
1181
-
1182
1149
  def __init__(self, items: Sequence[Expression]):
1183
1150
  self.items = items
1184
1151
 
@@ -1195,8 +1162,6 @@ class Set(Expression):
1195
1162
 
1196
1163
 
1197
1164
  class Dict(Expression):
1198
- child_elements = ["pairs"]
1199
-
1200
1165
  def __init__(self, pairs: Sequence[tuple[Expression, Expression]]):
1201
1166
  self.pairs = pairs
1202
1167
 
@@ -1209,8 +1174,6 @@ class Dict(Expression):
1209
1174
 
1210
1175
 
1211
1176
  class StringJoinBase(Expression):
1212
- child_elements = ["parts"]
1213
-
1214
1177
  def __init__(self, parts: Sequence[Expression]):
1215
1178
  self.parts = parts
1216
1179
 
@@ -1280,8 +1243,6 @@ StringJoin = FStringJoin
1280
1243
 
1281
1244
 
1282
1245
  class Name(Expression):
1283
- child_elements = []
1284
-
1285
1246
  def __init__(self, name: str, scope: Scope):
1286
1247
  if not scope.is_name_in_use(name):
1287
1248
  raise AssertionError(f"Cannot refer to undefined name '{name}'")
@@ -1300,8 +1261,6 @@ class Name(Expression):
1300
1261
 
1301
1262
 
1302
1263
  class Attr(Expression):
1303
- child_elements = ["value"]
1304
-
1305
1264
  def __init__(self, value: Expression, attribute: str) -> None:
1306
1265
  self.value = value
1307
1266
  if not allowable_name(attribute, allow_builtin=True):
@@ -1313,8 +1272,6 @@ class Attr(Expression):
1313
1272
 
1314
1273
 
1315
1274
  class Starred(Expression):
1316
- child_elements = ["value"]
1317
-
1318
1275
  def __init__(self, value: Expression):
1319
1276
  self.value = value
1320
1277
 
@@ -1328,7 +1285,7 @@ class Starred(Expression):
1328
1285
  def function_call(
1329
1286
  function_name: str,
1330
1287
  args: Sequence[Expression],
1331
- kwargs: dict[str, Expression],
1288
+ kwargs: Mapping[str, Expression],
1332
1289
  scope: Scope,
1333
1290
  ) -> Expression:
1334
1291
  if not scope.is_name_in_use(function_name):
@@ -1340,17 +1297,15 @@ def function_call(
1340
1297
 
1341
1298
 
1342
1299
  class Call(Expression):
1343
- child_elements = ["value", "args", "kwargs"]
1344
-
1345
1300
  def __init__(
1346
1301
  self,
1347
1302
  value: Expression,
1348
1303
  args: Sequence[Expression],
1349
- kwargs: dict[str, Expression],
1304
+ kwargs: Mapping[str, Expression],
1350
1305
  ):
1351
1306
  self.value = value
1352
1307
  self.args = list(args)
1353
- self.kwargs = kwargs
1308
+ self.kwargs = dict(kwargs)
1354
1309
 
1355
1310
  def as_ast(self, *, include_comments: bool = False) -> py_ast.expr:
1356
1311
 
@@ -1406,14 +1361,12 @@ def method_call(
1406
1361
  obj: Expression,
1407
1362
  method_name: str,
1408
1363
  args: Sequence[Expression],
1409
- kwargs: dict[str, Expression],
1364
+ kwargs: Mapping[str, Expression],
1410
1365
  ):
1411
1366
  return obj.attr(method_name).call(args=args, kwargs=kwargs)
1412
1367
 
1413
1368
 
1414
1369
  class Subscript(Expression):
1415
- child_elements = ["value", "slice"]
1416
-
1417
1370
  def __init__(self, value: Expression, slice: Expression):
1418
1371
  self.value = value
1419
1372
  self.slice = slice
@@ -1436,8 +1389,6 @@ class NoneExpr(Expression):
1436
1389
 
1437
1390
 
1438
1391
  class BinaryOperator(Expression):
1439
- child_elements = ["left", "right"]
1440
-
1441
1392
  def __init__(self, left: Expression, right: Expression):
1442
1393
  self.left = left
1443
1394
  self.right = right
@@ -1579,19 +1530,47 @@ def simplify(codegen_ast: CodeGenAstType, simplifier: Callable[[CodeGenAstType,
1579
1530
  def rewriting_traverse(
1580
1531
  node: CodeGenAstType | Sequence[CodeGenAstType],
1581
1532
  func: Callable[[CodeGenAstType], CodeGenAstType],
1533
+ _visited: set[int] | None = None,
1582
1534
  ):
1583
1535
  """
1584
- Apply 'func' to node and all sub CodeGenAst nodes
1536
+ Apply 'func' to node and all sub CodeGenAst nodes.
1537
+
1538
+ Discovers child nodes by introspecting instance attributes rather than
1539
+ relying on a manually-maintained list. A *visited* set (keyed by
1540
+ ``id()``) prevents infinite recursion through circular references
1541
+ (e.g. Block.scope → Function → body → Block).
1585
1542
  """
1543
+ if _visited is None:
1544
+ _visited = set()
1586
1545
  if isinstance(node, (CodeGenAst, CodeGenAstList)):
1546
+ node_id = id(node)
1547
+ if node_id in _visited:
1548
+ return
1549
+ _visited.add(node_id)
1587
1550
  new_node = func(node)
1588
1551
  if new_node is not node:
1589
1552
  morph_into(node, new_node)
1590
- for k in node.child_elements:
1591
- rewriting_traverse(getattr(node, k), func)
1553
+ for value in node.__dict__.values():
1554
+ _traverse_value(value, func, _visited)
1592
1555
  elif isinstance(node, (list, tuple)):
1593
1556
  for i in node:
1594
- rewriting_traverse(i, func)
1557
+ rewriting_traverse(i, func, _visited)
1558
+
1559
+
1560
+ def _traverse_value(
1561
+ value: object,
1562
+ func: Callable[[CodeGenAstType], CodeGenAstType],
1563
+ visited: set[int],
1564
+ ) -> None:
1565
+ """Recurse into a single attribute value, handling containers."""
1566
+ if isinstance(value, (CodeGenAst, CodeGenAstList)):
1567
+ rewriting_traverse(value, func, visited)
1568
+ elif isinstance(value, (list, tuple)):
1569
+ for item in value: # type: ignore[reportUnknownVariableType]
1570
+ _traverse_value(item, func, visited) # type: ignore[reportUnknownVariableType]
1571
+ elif isinstance(value, dict):
1572
+ for v in value.values(): # type: ignore[reportUnknownVariableType]
1573
+ _traverse_value(v, func, visited) # type: ignore[reportUnknownVariableType]
1595
1574
 
1596
1575
 
1597
1576
  def morph_into(item: object, new_item: object) -> None:
@@ -0,0 +1,111 @@
1
+ """
2
+ Utility to remove unused assignments from a Function body.
3
+
4
+ Uses ``rewriting_traverse`` to walk the codegen AST. Only handles a single
5
+ function scope — raises ``AssertionError`` if a nested ``Scope`` (e.g. inner
6
+ function or class) is encountered.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .codegen import (
12
+ Assignment,
13
+ Block,
14
+ CodeGenAst,
15
+ CodeGenAstList,
16
+ CodeGenAstType,
17
+ Function,
18
+ Name,
19
+ Scope,
20
+ rewriting_traverse,
21
+ )
22
+
23
+
24
+ def _collect_blocks(func: Function) -> list[Block]:
25
+ """Return every Block reachable from *func*.body, checking for nested scopes."""
26
+ blocks: list[Block] = []
27
+ seen: set[int] = set()
28
+
29
+ def _walk(obj: object) -> None:
30
+ obj_id = id(obj)
31
+ if obj_id in seen:
32
+ return
33
+ seen.add(obj_id)
34
+
35
+ if isinstance(obj, Scope) and obj is not func:
36
+ if type(obj) is not Scope:
37
+ raise AssertionError(f"remove_unused_assignments does not handle nested Scopes ({type(obj).__name__})")
38
+ return
39
+
40
+ if isinstance(obj, Block):
41
+ blocks.append(obj)
42
+
43
+ # Recurse into attributes, finding Block children generically.
44
+ if isinstance(obj, (CodeGenAst, CodeGenAstList)):
45
+ for value in obj.__dict__.values():
46
+ _walk_value(value)
47
+
48
+ def _walk_value(value: object) -> None:
49
+ if isinstance(value, (CodeGenAst, CodeGenAstList, Scope)):
50
+ _walk(value)
51
+ elif isinstance(value, (list, tuple)):
52
+ for item in value: # type: ignore[reportUnknownVariableType]
53
+ _walk_value(item) # type: ignore[reportUnknownVariableType]
54
+
55
+ # Start from statements inside the body (not the Function itself, which is
56
+ # a Scope we need to skip).
57
+ for stmt in func.body.statements:
58
+ _walk_value(stmt)
59
+ # Also include the body block itself.
60
+ blocks.insert(0, func.body)
61
+ return blocks
62
+
63
+
64
+ def _collect_name_references(func: Function) -> set[str]:
65
+ """Return the set of all name strings referenced as ``Name`` expressions."""
66
+ names: set[str] = set()
67
+
68
+ def _visitor(node: CodeGenAstType) -> CodeGenAstType:
69
+ if isinstance(node, Name):
70
+ names.add(node.name)
71
+ return node
72
+
73
+ rewriting_traverse(func.body, _visitor)
74
+ return names
75
+
76
+
77
+ def _assigned_names(blocks: list[Block]) -> set[str]:
78
+ """Return all names that appear on the LHS of an ``_Assignment``."""
79
+ result: set[str] = set()
80
+ for block in blocks:
81
+ for stmt in block.statements:
82
+ if isinstance(stmt, Assignment):
83
+ result.add(stmt.name)
84
+ return result
85
+
86
+
87
+ def _remove_once(func: Function, blocks: list[Block]) -> bool:
88
+ """Remove unused assignment statements. Returns True if anything changed."""
89
+ referenced = _collect_name_references(func)
90
+ assigned = _assigned_names(blocks)
91
+ unused = assigned - referenced
92
+ if not unused:
93
+ return False
94
+
95
+ for block in blocks:
96
+ block.statements = [
97
+ stmt for stmt in block.statements if not (isinstance(stmt, Assignment) and stmt.name in unused)
98
+ ]
99
+ return True
100
+
101
+
102
+ def remove_unused_assignments(func: Function) -> None:
103
+ """
104
+ Remove statements that assign to a name which is never read.
105
+
106
+ Works recursively: if removing ``x = y`` makes ``y`` unused, it will be
107
+ removed too. Raises ``AssertionError`` on nested scopes.
108
+ """
109
+ blocks = _collect_blocks(func)
110
+ while _remove_once(func, blocks):
111
+ pass
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluent-codegen
3
- Version: 0.2
3
+ Version: 0.3.0
4
4
  Summary: A Python library for generating Python code via AST construction.
5
5
  Author-email: Luke Plant <luke@lukeplant.me.uk>
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Repository, https://github.com/spookylukey/fluent-codegen
8
8
  Keywords: codegen,code-generation,ast,python,metaprogramming
9
- Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Operating System :: OS Independent
12
12
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -5,10 +5,12 @@ src/fluent_codegen/__init__.py
5
5
  src/fluent_codegen/ast_compat.py
6
6
  src/fluent_codegen/codegen.py
7
7
  src/fluent_codegen/py.typed
8
+ src/fluent_codegen/remove_unused_assignments.py
8
9
  src/fluent_codegen/utils.py
9
10
  src/fluent_codegen.egg-info/PKG-INFO
10
11
  src/fluent_codegen.egg-info/SOURCES.txt
11
12
  src/fluent_codegen.egg-info/dependency_links.txt
12
13
  src/fluent_codegen.egg-info/top_level.txt
13
14
  tests/test_codegen.py
14
- tests/test_fizzbuzz_example.py
15
+ tests/test_fizzbuzz_example.py
16
+ tests/test_remove_unused.py
@@ -268,6 +268,65 @@ def test_function_add_args_reserves_name():
268
268
  assert_code_equal(ref, "my_arg")
269
269
 
270
270
 
271
+ def test_function_args_property_empty():
272
+ module = codegen.Module()
273
+ func_name = module.scope.reserve_name("myfunc")
274
+ func = codegen.Function(func_name, parent_scope=module.scope)
275
+ assert func.args == ()
276
+
277
+
278
+ def test_function_args_property():
279
+ module = codegen.Module()
280
+ func_name = module.scope.reserve_name("myfunc")
281
+ func = codegen.Function(func_name, args=["a", "b"], parent_scope=module.scope)
282
+ assert len(func.args) == 2
283
+ assert func.args[0].name == "a"
284
+ assert func.args[1].name == "b"
285
+
286
+
287
+ def test_function_args_property_with_function_arg():
288
+ module = codegen.Module()
289
+ func_name = module.scope.reserve_name("myfunc")
290
+ func = codegen.Function(
291
+ func_name,
292
+ args=[
293
+ codegen.FunctionArg.positional("x"),
294
+ "y",
295
+ codegen.FunctionArg.keyword("z", default=codegen.Number(1)),
296
+ ],
297
+ parent_scope=module.scope,
298
+ )
299
+ assert len(func.args) == 3
300
+ assert func.args[0].name == "x"
301
+ assert func.args[0].kind == codegen.ArgKind.POSITIONAL_ONLY
302
+ assert func.args[1].name == "y"
303
+ assert func.args[1].kind == codegen.ArgKind.POSITIONAL_OR_KEYWORD
304
+ assert func.args[2].name == "z"
305
+ assert func.args[2].kind == codegen.ArgKind.KEYWORD_ONLY
306
+
307
+
308
+ def test_function_args_property_after_add_args():
309
+ module = codegen.Module()
310
+ func_name = module.scope.reserve_name("myfunc")
311
+ func = codegen.Function(func_name, args=["a"], parent_scope=module.scope)
312
+ assert len(func.args) == 1
313
+ func.add_args(["b", "c"])
314
+ assert len(func.args) == 3
315
+ assert func.args[2].name == "c"
316
+
317
+
318
+ def test_function_args_property_is_readonly():
319
+ """Mutating the returned tuple should not affect the function's internal args."""
320
+ module = codegen.Module()
321
+ func_name = module.scope.reserve_name("myfunc")
322
+ func = codegen.Function(func_name, args=["a", "b"], parent_scope=module.scope)
323
+ args = func.args
324
+ assert len(args) == 2
325
+ # tuple is immutable, so this should be a fresh copy each time
326
+ assert func.args is not func.args or isinstance(func.args, tuple)
327
+ assert len(func.args) == 2
328
+
329
+
271
330
  def test_function_args_name_reserved_check():
272
331
  module = codegen.Module()
273
332
  module.scope.reserve_function_arg_name("my_arg")
@@ -364,7 +423,7 @@ def test_create_assignment_bad():
364
423
  name = module.scope.reserve_name("x")
365
424
  module.create_assignment(name, codegen.String("a string"))
366
425
  stmt = module.statements[0]
367
- assert isinstance(stmt, codegen._Assignment)
426
+ assert isinstance(stmt, codegen.Assignment)
368
427
  stmt.name = "something with a space"
369
428
  with pytest.raises(AssertionError):
370
429
  as_source_code(module)
@@ -437,19 +496,19 @@ def test_create_annotation_duplicate_name_gets_renamed():
437
496
  def test_create_annotation_bad_name():
438
497
  module = codegen.Module()
439
498
  # Force a bad name through by directly constructing _Annotation
440
- from fluent_codegen.codegen import _Annotation
499
+ from fluent_codegen.codegen import Annotation
441
500
 
442
- stmt = _Annotation("bad name", module.scope.name("int"))
501
+ stmt = Annotation("bad name", module.scope.name("int"))
443
502
  with pytest.raises(AssertionError):
444
503
  as_source_code(stmt)
445
504
 
446
505
 
447
506
  def test_annotation_has_assignment_for_name():
448
- from fluent_codegen.codegen import _Annotation
507
+ from fluent_codegen.codegen import Annotation
449
508
 
450
509
  scope = codegen.Scope()
451
510
  scope.reserve_name("int")
452
- stmt = _Annotation("x", scope.name("int"))
511
+ stmt = Annotation("x", scope.name("int"))
453
512
  assert stmt.has_assignment_for_name("x") is True
454
513
  assert stmt.has_assignment_for_name("y") is False
455
514
 
@@ -1349,6 +1408,440 @@ def test_rewriting_traverse_tuple():
1349
1408
  codegen.rewriting_traverse(t, lambda n: n)
1350
1409
 
1351
1410
 
1411
+ def _collect_traversed_types(root):
1412
+ """Helper: traverse a tree and return list of (class_name) for every node visited."""
1413
+ visited = []
1414
+
1415
+ def visitor(node):
1416
+ visited.append(type(node).__name__)
1417
+ return node
1418
+
1419
+ codegen.rewriting_traverse(root, visitor)
1420
+ return visited
1421
+
1422
+
1423
+ def test_rewriting_traverse_complex_module():
1424
+ """Test traversal visits all nodes in a complex module with nested structures."""
1425
+ module = codegen.Module()
1426
+ scope = module.scope
1427
+
1428
+ # Function with decorators, return type, body containing if/else, assignments, returns
1429
+ scope.reserve_name("my_decorator")
1430
+ decorator = codegen.Name("my_decorator", scope)
1431
+ func, _ = module.create_function(
1432
+ "process",
1433
+ args=["items", "flag"],
1434
+ decorators=[decorator],
1435
+ return_type=codegen.Name("str", scope),
1436
+ )
1437
+ n_items = codegen.Name("items", func)
1438
+ n_flag = codegen.Name("flag", func)
1439
+
1440
+ # Assignment with type hint
1441
+ result_name = func.body.scope.reserve_name("result")
1442
+ func.body.create_assignment(result_name, codegen.String(""), type_hint=codegen.Name("str", func))
1443
+
1444
+ # If/elif/else
1445
+ if_stmt = func.body.create_if()
1446
+ branch1 = if_stmt.create_if_branch(n_flag.eq(codegen.Bool(True)))
1447
+ branch1.create_assignment(
1448
+ result_name,
1449
+ codegen.function_call("str", [n_items], {}, func),
1450
+ allow_multiple=True,
1451
+ )
1452
+ branch2 = if_stmt.create_if_branch(n_flag.eq(codegen.Bool(False)))
1453
+ branch2.create_return(codegen.String("none"))
1454
+ if_stmt.else_block.create_return(codegen.String("default"))
1455
+
1456
+ # Return
1457
+ func.body.create_return(codegen.Name("result", func))
1458
+
1459
+ visited = _collect_traversed_types(module)
1460
+
1461
+ # Verify key node types are all visited
1462
+ assert "Module" in visited
1463
+ assert "Function" in visited
1464
+ assert "Block" in visited
1465
+ assert "If" in visited
1466
+ assert "Equals" in visited
1467
+ assert "String" in visited
1468
+ assert "Bool" in visited
1469
+ assert "Return" in visited
1470
+ assert "Call" in visited
1471
+ assert "Name" in visited
1472
+
1473
+
1474
+ def test_rewriting_traverse_call_with_kwargs():
1475
+ """Test that kwargs dict values in Call nodes are traversed."""
1476
+ scope = codegen.Scope()
1477
+ func_name = scope.create_name("my_func")
1478
+ call = codegen.Call(
1479
+ func_name,
1480
+ [codegen.String("positional")],
1481
+ {"key1": codegen.String("val1"), "key2": codegen.Number(42)},
1482
+ )
1483
+
1484
+ visited = _collect_traversed_types(call)
1485
+ assert "Call" in visited
1486
+ assert "Name" in visited
1487
+ # The positional arg
1488
+ assert visited.count("String") >= 2 # "positional" and "val1"
1489
+ assert "Number" in visited # 42 from kwargs
1490
+
1491
+
1492
+ def test_rewriting_traverse_replaces_in_kwargs():
1493
+ """Test that rewriting_traverse can replace nodes inside Call kwargs."""
1494
+ scope = codegen.Scope()
1495
+ func_name = scope.create_name("f")
1496
+ call = codegen.Call(
1497
+ func_name,
1498
+ [],
1499
+ {"k": codegen.String("old")},
1500
+ )
1501
+
1502
+ def replace_old(node):
1503
+ if isinstance(node, codegen.String) and node.string_value == "old":
1504
+ return codegen.String("new")
1505
+ return node
1506
+
1507
+ codegen.rewriting_traverse(call, replace_old)
1508
+ assert_code_equal(call, "f(k='new')")
1509
+
1510
+
1511
+ def test_rewriting_traverse_function_decorators_and_return_type():
1512
+ """Test that decorators and return_type on Function are traversed."""
1513
+ scope = codegen.Scope()
1514
+ decorator = codegen.String("deco_marker")
1515
+ ret_type = codegen.String("ret_marker")
1516
+ func = codegen.Function("myfunc", parent_scope=scope, decorators=[decorator], return_type=ret_type)
1517
+
1518
+ visited = _collect_traversed_types(func)
1519
+ assert visited.count("String") == 2 # decorator + return_type
1520
+
1521
+
1522
+ def test_rewriting_traverse_replaces_in_decorator():
1523
+ """Test replacing a decorator expression via traverse."""
1524
+ scope = codegen.Scope()
1525
+ scope.reserve_name("old_deco")
1526
+ scope.reserve_name("new_deco")
1527
+ func = codegen.Function(
1528
+ "myfunc",
1529
+ parent_scope=scope,
1530
+ decorators=[codegen.Name("old_deco", scope)],
1531
+ )
1532
+
1533
+ def replace_deco(node):
1534
+ if isinstance(node, codegen.Name) and node.name == "old_deco":
1535
+ return codegen.Name("new_deco", scope)
1536
+ return node
1537
+
1538
+ codegen.rewriting_traverse(func, replace_deco)
1539
+ decorator = func.decorators[0]
1540
+ assert isinstance(decorator, codegen.Name)
1541
+ assert decorator.name == "new_deco"
1542
+
1543
+
1544
+ def test_rewriting_traverse_assignment_type_hint():
1545
+ """Test that type_hint on _Assignment is traversed."""
1546
+ module = codegen.Module()
1547
+ name = module.scope.reserve_name("x")
1548
+ module.create_assignment(name, codegen.String("hello"), type_hint=codegen.Name("str", module.scope))
1549
+
1550
+ visited = _collect_traversed_types(module)
1551
+ # The type hint Name("str") should be visited
1552
+ assert visited.count("Name") >= 1
1553
+
1554
+
1555
+ def test_rewriting_traverse_annotation():
1556
+ """Test that annotation Expression on _Annotation is traversed."""
1557
+ module = codegen.Module()
1558
+ name = module.scope.reserve_name("x")
1559
+ module.create_annotation(name, codegen.Name("int", module.scope))
1560
+
1561
+ visited = _collect_traversed_types(module)
1562
+ # Module -> Block(statements) -> _Annotation -> annotation(Name)
1563
+ assert "Name" in visited
1564
+
1565
+
1566
+ def test_rewriting_traverse_with_statement():
1567
+ """Test traversal into With statement's context_expr, target, and body."""
1568
+ module = codegen.Module()
1569
+ module.scope.reserve_name("ctx_manager")
1570
+ module.scope.reserve_name("f")
1571
+ ctx = codegen.function_call("ctx_manager", [codegen.String("file.txt")], {}, module.scope)
1572
+ with_stmt = module.create_with(ctx, target=codegen.Name("f", module.scope))
1573
+ with_stmt.body.create_return(codegen.String("done"))
1574
+
1575
+ visited = _collect_traversed_types(module)
1576
+ assert "With" in visited
1577
+ assert "Call" in visited
1578
+ assert "Return" in visited
1579
+
1580
+
1581
+ def test_rewriting_traverse_try_except():
1582
+ """Test traversal into Try blocks."""
1583
+ module = codegen.Module()
1584
+ exc = codegen.Name("ValueError", module.scope)
1585
+ try_stmt = codegen.Try([exc], module.scope)
1586
+ try_stmt.try_block.create_return(codegen.String("tried"))
1587
+ try_stmt.except_block.create_return(codegen.String("caught"))
1588
+ try_stmt.else_block.create_return(codegen.String("else"))
1589
+ module.add_statement(try_stmt)
1590
+
1591
+ visited = _collect_traversed_types(module)
1592
+ assert "Try" in visited
1593
+ assert visited.count("Block") >= 3 # try, except, else blocks
1594
+ assert visited.count("Return") >= 3
1595
+ assert visited.count("String") >= 3
1596
+
1597
+
1598
+ def test_rewriting_traverse_dict_expression():
1599
+ """Test traversal into Dict pairs (tuples of expressions)."""
1600
+ d = codegen.Dict(
1601
+ [
1602
+ (codegen.String("k1"), codegen.Number(1)),
1603
+ (codegen.String("k2"), codegen.Number(2)),
1604
+ ]
1605
+ )
1606
+
1607
+ visited = _collect_traversed_types(d)
1608
+ assert visited.count("String") == 2
1609
+ assert visited.count("Number") == 2
1610
+
1611
+
1612
+ def test_rewriting_traverse_nested_binary_ops():
1613
+ """Test deep nesting of binary operators."""
1614
+ # (1 + 2) * (3 - 4)
1615
+ expr = codegen.Number(1).add(codegen.Number(2)).mul(codegen.Number(3).sub(codegen.Number(4)))
1616
+
1617
+ visited = _collect_traversed_types(expr)
1618
+ assert visited.count("Number") == 4
1619
+ assert "Mul" in visited
1620
+ assert "Add" in visited
1621
+ assert "Sub" in visited
1622
+
1623
+
1624
+ def test_rewriting_traverse_subscript():
1625
+ """Test traversal into Subscript value and slice."""
1626
+ scope = codegen.Scope()
1627
+ scope.reserve_name("items")
1628
+ expr = codegen.Name("items", scope).subscript(codegen.Number(0))
1629
+
1630
+ visited = _collect_traversed_types(expr)
1631
+ assert "Subscript" in visited
1632
+ assert "Name" in visited
1633
+ assert "Number" in visited
1634
+
1635
+
1636
+ def test_rewriting_traverse_attr():
1637
+ """Test traversal into Attr value."""
1638
+ scope = codegen.Scope()
1639
+ scope.reserve_name("obj")
1640
+ expr = codegen.Name("obj", scope).attr("prop")
1641
+
1642
+ visited = _collect_traversed_types(expr)
1643
+ assert "Attr" in visited
1644
+ assert "Name" in visited
1645
+
1646
+
1647
+ def test_rewriting_traverse_starred():
1648
+ """Test traversal into Starred value."""
1649
+ scope = codegen.Scope()
1650
+ scope.reserve_name("args")
1651
+ expr = codegen.Name("args", scope).starred()
1652
+
1653
+ visited = _collect_traversed_types(expr)
1654
+ assert "Starred" in visited
1655
+ assert "Name" in visited
1656
+
1657
+
1658
+ def test_rewriting_traverse_list_tuple_set():
1659
+ """Test traversal into List, Tuple, Set items."""
1660
+ for cls in [codegen.List, codegen.Tuple, codegen.Set]:
1661
+ coll = cls([codegen.String("a"), codegen.Number(1)])
1662
+ visited = _collect_traversed_types(coll)
1663
+ assert "String" in visited
1664
+ assert "Number" in visited
1665
+
1666
+
1667
+ def test_rewriting_traverse_class_with_bases_and_decorators():
1668
+ """Test traversal into Class body, bases, and decorators."""
1669
+ module = codegen.Module()
1670
+ module.scope.reserve_name("BaseClass")
1671
+ module.scope.reserve_name("my_class_deco")
1672
+ base = codegen.Name("BaseClass", module.scope)
1673
+ deco = codegen.Name("my_class_deco", module.scope)
1674
+ cls, _ = module.create_class("MyClass", bases=[base], decorators=[deco])
1675
+
1676
+ visited = _collect_traversed_types(module)
1677
+ assert "Class" in visited
1678
+ assert "Block" in visited
1679
+ # bases and decorators should be visited
1680
+ assert visited.count("Name") >= 2
1681
+
1682
+
1683
+ def test_rewriting_traverse_assert():
1684
+ """Test traversal into Assert test and msg."""
1685
+ module = codegen.Module()
1686
+ module.create_assert(codegen.Bool(True), codegen.String("oops"))
1687
+
1688
+ visited = _collect_traversed_types(module)
1689
+ assert "Assert" in visited
1690
+ assert "Bool" in visited
1691
+ assert "String" in visited
1692
+
1693
+
1694
+ def test_rewriting_traverse_fstring():
1695
+ """Test traversal into FStringJoin parts."""
1696
+ scope = codegen.Scope()
1697
+ scope.reserve_name("world")
1698
+ fstr = codegen.FStringJoin([codegen.String("hello "), codegen.Name("world", scope)])
1699
+
1700
+ visited = _collect_traversed_types(fstr)
1701
+ assert "FStringJoin" in visited
1702
+ assert "String" in visited
1703
+ assert "Name" in visited
1704
+
1705
+
1706
+ def test_rewriting_traverse_deeply_nested_codegen_tree():
1707
+ """
1708
+ Build a complex, deeply-nested codegen tree and verify that rewriting_traverse
1709
+ visits every single CodeGenAst/CodeGenAstList node by replacing all String
1710
+ values with 'REPLACED'.
1711
+ """
1712
+ module = codegen.Module()
1713
+ scope = module.scope
1714
+
1715
+ # Import
1716
+ module.create_import("os")
1717
+ module.create_import_from(from_="sys", import_="argv")
1718
+
1719
+ # Class with decorator and base
1720
+ scope.reserve_name("dataclass")
1721
+ class_deco = codegen.Name("dataclass", scope)
1722
+ base = codegen.Name("object", scope)
1723
+ cls, _ = module.create_class("Config", bases=[base], decorators=[class_deco])
1724
+ field_name = cls.body.scope.reserve_name("name")
1725
+ cls.body.create_assignment(field_name, codegen.String("default_name"))
1726
+
1727
+ # Function with kwargs call, nested if, try/except, with, assert
1728
+ scope.reserve_name("process")
1729
+ func, _ = module.create_function(
1730
+ "run",
1731
+ args=["config"],
1732
+ return_type=codegen.Name("bool", scope),
1733
+ )
1734
+ config = codegen.Name("config", func)
1735
+
1736
+ # Assignment with function call using kwargs
1737
+ result_name = func.body.scope.reserve_name("result")
1738
+ func.body.create_assignment(
1739
+ result_name,
1740
+ codegen.function_call(
1741
+ "process",
1742
+ [config.attr("name")],
1743
+ {"timeout": codegen.Number(30)},
1744
+ func,
1745
+ ),
1746
+ )
1747
+
1748
+ # Nested if
1749
+ if_stmt = func.body.create_if()
1750
+ branch = if_stmt.create_if_branch(codegen.Name("result", func).eq(codegen.String("ok")))
1751
+ branch.create_return(codegen.Bool(True))
1752
+
1753
+ # Try/except in else
1754
+ try_stmt = codegen.Try([codegen.Name("Exception", func)], func)
1755
+ try_stmt.try_block.create_return(codegen.Bool(False))
1756
+ try_stmt.except_block.create_return(codegen.Bool(False))
1757
+ if_stmt.else_block.add_statement(try_stmt)
1758
+
1759
+ # With statement
1760
+ func.reserve_name("open_log")
1761
+ ctx_expr = codegen.function_call("open_log", [codegen.String("log.txt")], {}, func)
1762
+ with_block = func.body.create_with(ctx_expr)
1763
+ with_block.body.create_return(codegen.String("logged"))
1764
+
1765
+ # Assert
1766
+ func.body.create_assert(
1767
+ codegen.Name("result", func).ne(codegen.NoneExpr()),
1768
+ codegen.String("must not be None"),
1769
+ )
1770
+
1771
+ # Now do a rewriting_traverse that replaces all Strings
1772
+ string_count = [0]
1773
+
1774
+ def count_and_replace_strings(node):
1775
+ if isinstance(node, codegen.String):
1776
+ string_count[0] += 1
1777
+ return codegen.String("REPLACED")
1778
+ return node
1779
+
1780
+ codegen.rewriting_traverse(module, count_and_replace_strings)
1781
+
1782
+ # We should have found all string literals in the tree
1783
+ assert string_count[0] >= 5 # default_name, ok, log.txt, logged, must not be None
1784
+
1785
+ # Verify ALL strings are now REPLACED in the generated source
1786
+ source = module.as_python_source()
1787
+ for original in ["default_name", "'ok'", "log.txt", "logged", "must not be None"]:
1788
+ assert original not in source, f"String '{original}' should have been replaced but found in: {source}"
1789
+ assert "REPLACED" in source
1790
+
1791
+
1792
+ def test_rewriting_traverse_replace_number_in_nested_kwargs():
1793
+ """
1794
+ Specifically test that numbers deep inside kwargs of a Call inside a
1795
+ function body can be found and replaced.
1796
+ """
1797
+ module = codegen.Module()
1798
+ module.scope.reserve_name("setup")
1799
+ func, _ = module.create_function("f", args=[])
1800
+ func.body.create_assignment(
1801
+ func.body.scope.reserve_name("x"),
1802
+ codegen.function_call(
1803
+ "setup",
1804
+ [],
1805
+ {"timeout": codegen.Number(99), "retries": codegen.Number(3)},
1806
+ func,
1807
+ ),
1808
+ )
1809
+
1810
+ def double_numbers(node):
1811
+ if isinstance(node, codegen.Number):
1812
+ return codegen.Number(node.number * 2)
1813
+ return node
1814
+
1815
+ codegen.rewriting_traverse(module, double_numbers)
1816
+ source = module.as_python_source()
1817
+ assert "198" in source # 99 * 2
1818
+ assert "6" in source # 3 * 2
1819
+ assert "99" not in source
1820
+
1821
+
1822
+ def test_rewriting_traverse_none_expr():
1823
+ """Test that NoneExpr nodes are visited (they previously lacked child_elements)."""
1824
+ scope = codegen.Scope()
1825
+ scope.reserve_name("x")
1826
+ expr = codegen.Name("x", scope).eq(codegen.NoneExpr())
1827
+
1828
+ visited = _collect_traversed_types(expr)
1829
+ assert "NoneExpr" in visited
1830
+ assert "Equals" in visited
1831
+ assert "Name" in visited
1832
+
1833
+
1834
+ def test_rewriting_traverse_import_nodes():
1835
+ """Test that Import and ImportFrom nodes are visited (they previously lacked child_elements)."""
1836
+ module = codegen.Module()
1837
+ module.create_import("os")
1838
+ module.create_import_from(from_="sys", import_="argv")
1839
+
1840
+ visited = _collect_traversed_types(module)
1841
+ assert "Import" in visited
1842
+ assert "ImportFrom" in visited
1843
+
1844
+
1352
1845
  # --- auto() tests ---
1353
1846
 
1354
1847
 
@@ -0,0 +1,166 @@
1
+ import pytest
2
+
3
+ from fluent_codegen.codegen import (
4
+ Function,
5
+ Name,
6
+ Number,
7
+ Scope,
8
+ String,
9
+ constants,
10
+ )
11
+ from fluent_codegen.remove_unused_assignments import remove_unused_assignments
12
+
13
+
14
+ def make_func(parent_scope=None):
15
+ """Helper: create a Function named 'f' with no args."""
16
+ if parent_scope is None:
17
+ parent_scope = Scope()
18
+ parent_scope.reserve_name("f")
19
+ func = Function("f", args=[], parent_scope=parent_scope)
20
+ return func
21
+
22
+
23
+ def source(func):
24
+ return func.as_python_source().strip()
25
+
26
+
27
+ # ── positive cases (something gets removed) ──────────────────────────
28
+
29
+
30
+ def test_remove_simple_unused():
31
+ func = make_func()
32
+ x = func.body.scope.create_name("x")
33
+ func.body.create_assignment(x, Number(1))
34
+ func.body.create_return(String("done"))
35
+
36
+ remove_unused_assignments(func)
37
+
38
+ assert source(func) == "def f():\n return 'done'"
39
+
40
+
41
+ def test_cascading_removal():
42
+ """x = y, y = 1; x unused ⇒ remove x = y ⇒ y now unused ⇒ remove y = 1."""
43
+ func = make_func()
44
+ y = func.body.scope.create_name("y")
45
+ func.body.create_assignment(y, Number(1))
46
+ x = func.body.scope.create_name("x")
47
+ func.body.create_assignment(x, Name("y", func.body.scope))
48
+ func.body.create_return(String("done"))
49
+
50
+ remove_unused_assignments(func)
51
+
52
+ assert source(func) == "def f():\n return 'done'"
53
+
54
+
55
+ def test_remove_unused_in_if_block():
56
+ """Unused assignment inside an if-branch is removed."""
57
+ func = make_func()
58
+ x = func.body.scope.create_name("x")
59
+ if_stmt = func.body.create_if()
60
+ branch = if_stmt.create_if_branch(constants.True_)
61
+ branch.create_assignment(x, Number(42))
62
+ func.body.create_return(String("done"))
63
+
64
+ remove_unused_assignments(func)
65
+
66
+ # The assignment in the if-branch should be gone; branch body is now empty
67
+ src = source(func)
68
+ assert "x = 42" not in src
69
+
70
+
71
+ def test_remove_unused_in_try_block():
72
+ """Unused assignment inside a try block is removed."""
73
+ from fluent_codegen.codegen import Try
74
+
75
+ func = make_func()
76
+ x = func.body.scope.create_name("x")
77
+ scope = func.body.scope
78
+ scope.reserve_name("Exception", is_builtin=True)
79
+ try_stmt = Try(
80
+ catch_exceptions=[Name("Exception", scope)],
81
+ parent_scope=scope,
82
+ )
83
+ try_stmt.try_block.create_assignment(x, Number(1))
84
+ func.body.add_statement(try_stmt)
85
+ func.body.create_return(String("ok"))
86
+
87
+ remove_unused_assignments(func)
88
+
89
+ src = source(func)
90
+ assert "x = 1" not in src
91
+
92
+
93
+ # ── negative cases (nothing should be removed) ───────────────────────
94
+
95
+
96
+ def test_keep_used_assignment():
97
+ func = make_func()
98
+ x = func.body.scope.create_name("x")
99
+ func.body.create_assignment(x, Number(1))
100
+ func.body.create_return(Name("x", func.body.scope))
101
+
102
+ remove_unused_assignments(func)
103
+
104
+ assert source(func) == "def f():\n x = 1\n return x"
105
+
106
+
107
+ def test_keep_used_in_nested_block():
108
+ """x assigned at top level, used inside an if-branch — keep it."""
109
+ func = make_func()
110
+ x = func.body.scope.create_name("x")
111
+ func.body.create_assignment(x, Number(1))
112
+ if_stmt = func.body.create_if()
113
+ branch = if_stmt.create_if_branch(constants.True_)
114
+ branch.create_return(Name("x", func.body.scope))
115
+
116
+ remove_unused_assignments(func)
117
+
118
+ src = source(func)
119
+ assert "x = 1" in src
120
+
121
+
122
+ def test_keep_function_args():
123
+ """Function arguments should never be removed even if unused in body."""
124
+ scope = Scope()
125
+ scope.reserve_name("g")
126
+ func = Function("g", args=["a"], parent_scope=scope)
127
+ func.body.create_return(String("hi"))
128
+
129
+ remove_unused_assignments(func)
130
+
131
+ assert source(func) == "def g(a):\n return 'hi'"
132
+
133
+
134
+ def test_keep_when_used_in_another_assignment_value():
135
+ """y = 1; x = y; return x ⇒ y is used (in x's value), so keep both."""
136
+ func = make_func()
137
+ y = func.body.scope.create_name("y")
138
+ func.body.create_assignment(y, Number(1))
139
+ x = func.body.scope.create_name("x")
140
+ func.body.create_assignment(x, Name("y", func.body.scope))
141
+ func.body.create_return(Name("x", func.body.scope))
142
+
143
+ remove_unused_assignments(func)
144
+
145
+ src = source(func)
146
+ assert "y = 1" in src
147
+ assert "x = y" in src
148
+
149
+
150
+ # ── error case: nested scope ─────────────────────────────────────────
151
+
152
+
153
+ def test_raises_on_nested_function():
154
+ func = make_func()
155
+ func.body.create_function("inner", args=[])
156
+
157
+ with pytest.raises(AssertionError, match="nested.*[Ss]cope"):
158
+ remove_unused_assignments(func)
159
+
160
+
161
+ def test_raises_on_nested_class():
162
+ func = make_func()
163
+ func.body.create_class("MyClass")
164
+
165
+ with pytest.raises(AssertionError, match="nested.*[Ss]cope"):
166
+ remove_unused_assignments(func)
File without changes
File without changes
File without changes