From 8e0c6e24f740b5cdf21aa8a3cbdfb0d1c98fd50d Mon Sep 17 00:00:00 2001 From: Aryaz Eghbali Date: Thu, 17 Jul 2025 10:44:39 +0000 Subject: [PATCH 1/2] Fixes indented block position by PositionProvider (#1380) --- libcst/_nodes/statement.py | 29 +++++++--- .../metadata/tests/test_position_provider.py | 56 +++++++++++++++++++ 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/libcst/_nodes/statement.py b/libcst/_nodes/statement.py index 1aba38d30..b180dfb33 100644 --- a/libcst/_nodes/statement.py +++ b/libcst/_nodes/statement.py @@ -712,14 +712,29 @@ def _codegen_impl(self, state: CodegenState) -> None: state.increase_indent(state.default_indent if indent is None else indent) if self.body: - with state.record_syntactic_position( - self, start_node=self.body[0], end_node=self.body[-1] + if ( + isinstance(self.body[0], (FunctionDef, ClassDef)) + and self.body[0].decorators ): - for stmt in self.body: - # IndentedBlock is responsible for adjusting the current indentation level, - # but its children are responsible for actually adding that indentation to - # the token list. - stmt._codegen(state) + # If the first statement is a function or class definition, we need to + # use the position of the first decorator instead of the function/class definition. + with state.record_syntactic_position( + self, start_node=self.body[0].decorators[0], end_node=self.body[-1] + ): + for stmt in self.body: + # IndentedBlock is responsible for adjusting the current indentation level, + # but its children are responsible for actually adding that indentation to + # the token list. + stmt._codegen(state) + else: + with state.record_syntactic_position( + self, start_node=self.body[0], end_node=self.body[-1] + ): + for stmt in self.body: + # IndentedBlock is responsible for adjusting the current indentation level, + # but its children are responsible for actually adding that indentation to + # the token list. + stmt._codegen(state) else: # Empty indented blocks are not syntactically valid in Python unless # they contain a 'pass' statement, so add one here. diff --git a/libcst/metadata/tests/test_position_provider.py b/libcst/metadata/tests/test_position_provider.py index c479837e2..793e5f361 100644 --- a/libcst/metadata/tests/test_position_provider.py +++ b/libcst/metadata/tests/test_position_provider.py @@ -83,6 +83,62 @@ def visit_Pass(self, node: cst.Pass) -> None: wrapper = MetadataWrapper(parse_module("pass")) wrapper.visit_batched([ABatchable()]) + def test_indented_block_starting_with_decorated_function_def(self) -> None: + """ + Tests that the position provider correctly computes positions in an indented block + starting with a decorated function definition. + """ + test = self + + class IndentedBlockVisitor(CSTVisitor): + METADATA_DEPENDENCIES = (PositionProvider,) + + def visit_IndentedBlock(self, node: cst.IndentedBlock) -> None: + test.assertEqual( + self.get_metadata(PositionProvider, node), + CodeRange((3, 4), (5, 15)), + ) + + wrapper = MetadataWrapper( + parse_module( + """ # Empty line +def foo(): + @decorator + def func(): return 42 + return func +""" + ) + ) + wrapper.visit(IndentedBlockVisitor()) + + def test_indented_block_starting_with_decorated_class_def(self) -> None: + """ + Tests that the position provider correctly computes positions in an indented block + starting with a decorated class definition. + """ + test = self + + class IndentedBlockVisitor(CSTVisitor): + METADATA_DEPENDENCIES = (PositionProvider,) + + def visit_IndentedBlock(self, node: cst.IndentedBlock) -> None: + test.assertEqual( + self.get_metadata(PositionProvider, node), + CodeRange((3, 4), (5, 18)), + ) + + wrapper = MetadataWrapper( + parse_module( + """ # Empty line +def foo(): + @decorator + class MyClass: pass + return MyClass +""" + ) + ) + wrapper.visit(IndentedBlockVisitor()) + class PositionProvidingCodegenStateTest(UnitTest): def test_codegen_initial_position(self) -> None: From 8235546931143e74dac8f5c6842829d37e3850a2 Mon Sep 17 00:00:00 2001 From: Aryaz Eghbali Date: Tue, 5 Aug 2025 16:17:02 +0000 Subject: [PATCH 2/2] Fixed type error --- libcst/_nodes/statement.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/libcst/_nodes/statement.py b/libcst/_nodes/statement.py index b180dfb33..3eefda2d3 100644 --- a/libcst/_nodes/statement.py +++ b/libcst/_nodes/statement.py @@ -712,14 +712,15 @@ def _codegen_impl(self, state: CodegenState) -> None: state.increase_indent(state.default_indent if indent is None else indent) if self.body: + first_statement = self.body[0] if ( - isinstance(self.body[0], (FunctionDef, ClassDef)) - and self.body[0].decorators + isinstance(first_statement, (FunctionDef, ClassDef)) + and first_statement.decorators ): # If the first statement is a function or class definition, we need to # use the position of the first decorator instead of the function/class definition. with state.record_syntactic_position( - self, start_node=self.body[0].decorators[0], end_node=self.body[-1] + self, start_node=first_statement.decorators[0], end_node=self.body[-1] ): for stmt in self.body: # IndentedBlock is responsible for adjusting the current indentation level, @@ -728,7 +729,7 @@ def _codegen_impl(self, state: CodegenState) -> None: stmt._codegen(state) else: with state.record_syntactic_position( - self, start_node=self.body[0], end_node=self.body[-1] + self, start_node=first_statement, end_node=self.body[-1] ): for stmt in self.body: # IndentedBlock is responsible for adjusting the current indentation level,